Token authorization is ready for MVP
All checks were successful
ci/woodpecker/push/build Pipeline was successful

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-05-15 12:53:58 +00:00
parent 35c6689a2c
commit cfa666e0a2
18 changed files with 917 additions and 61 deletions

View File

@@ -48,16 +48,23 @@ func (a *AccountsServer) RefreshToken(ctx context.Context, in *empty.Empty) (*em
return nil, status.Error(codes.Unauthenticated, "Invalid session")
}
accessToken, _, err := a.authorizationCtrl.GenerateToken(session.UserID, controllers.TokenTypeAccess)
accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: claims.UserID,
TokenType: controllers.TokenTypeAccess,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(session.UserID, controllers.TokenTypeRefresh)
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: claims.UserID,
TokenType: controllers.TokenTypeRefresh,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
newSession := &controllers.Session{UserID: session.UserID}
if err := a.authorizationCtrl.SaveSession(ctx, tokenID, newSession); err != nil {

View File

@@ -34,12 +34,20 @@ func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRe
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't create a user")
}
accessToken, _, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeAccess)
accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: id,
TokenType: controllers.TokenTypeAccess,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeRefresh)
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: id,
TokenType: controllers.TokenTypeRefresh,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
@@ -70,12 +78,20 @@ func (a *PublicAccountService) SignUp(ctx context.Context, in *accounts.SignUpRe
return nil, status.Error(codes.Aborted, "Couldn't create a user")
}
accessToken, _, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeAccess)
accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: id,
TokenType: controllers.TokenTypeAccess,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(id, controllers.TokenTypeRefresh)
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: id,
TokenType: controllers.TokenTypeRefresh,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}

64
api/v1/public_tokens.go Normal file
View File

@@ -0,0 +1,64 @@
package v1
import (
"context"
"errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// var _ tokens.PublicTokensServiceServer = (*PublicTokensServer)(nil)
type PublicTokensServer struct {
tokens.UnimplementedPublicTokensServiceServer
tokenCtrl *controllers.TokenController
authorizationCtrl *controllers.AuthController
}
func NewPublicTokensServer(
tokenCtrl *controllers.TokenController,
authorizationCtrl *controllers.AuthController,
) *PublicTokensServer {
return &PublicTokensServer{
tokenCtrl: tokenCtrl,
authorizationCtrl: authorizationCtrl,
}
}
func (srv *PublicTokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) {
tokenAuthRes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token)
if err != nil {
if errors.Is(err, controllers.ErrBadToken) {
return nil, status.Error(codes.Unauthenticated, "Token is not valid")
}
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
jwtData := &controllers.JWTData{
UserID: tokenAuthRes.UserID,
TokenType: controllers.TokenTypeAccess,
TokenAud: controllers.TokenAudToken,
Scope: tokenAuthRes.Scope,
}
accessToken, _, err := srv.authorizationCtrl.GenerateToken(jwtData)
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
header := metadata.New(map[string]string{
"X-Access-Token": accessToken,
})
if err := grpc.SetHeader(ctx, header); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't set metadata")
}
return &emptypb.Empty{}, nil
}

View File

@@ -4,6 +4,8 @@ import (
"context"
test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
func NewTestServer() *TestServer {
@@ -18,6 +20,15 @@ func (t *TestServer) Pong(ctx context.Context, in *test.PongRequest) (*test.Pong
return &test.PongResponse{}, nil
}
func (t *TestServer) PongStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PongStreamResponse]) error {
for i := 0; i < 10; i++ {
if err := stream.Send(&test.PongStreamResponse{Dummy: "test"}); err != nil {
return err
}
}
return nil
}
func NewPublicTestServer() *PublicTestServer {
return &PublicTestServer{}
}
@@ -29,3 +40,12 @@ type PublicTestServer struct {
func (t *PublicTestServer) Ping(ctx context.Context, in *test.PingRequest) (*test.PingResponse, error) {
return &test.PingResponse{}, nil
}
func (t *PublicTestServer) PingStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PingStreamResponse]) error {
for i := 0; i < 10; i++ {
if err := stream.Send(&test.PingStreamResponse{Dummy: "test"}); err != nil {
return err
}
}
return nil
}

View File

@@ -1,9 +1,262 @@
package v1
import (
"context"
"errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
// var _ tokens.TokensServiceServer = (*TokensServer)(nil)
type TokensServer struct {
tokens.UnimplementedTokensServiceServer
tokenCtrl *controllers.TokenController
authorizationCtrl *controllers.AuthController
}
func NewTokensServer(
tokenCtrl *controllers.TokenController,
authorizationCtrl *controllers.AuthController,
) *TokensServer {
return &TokensServer{
tokenCtrl: tokenCtrl,
authorizationCtrl: authorizationCtrl,
}
}
// CreateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateTokenRequest) (*tokens.CreateTokenResponse, error) {
claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if in.TokenPermissions == nil {
return nil, status.Error(codes.InvalidArgument, "Permissions must be set")
}
permissions := map[string][]string{}
for service, methods := range in.TokenPermissions.Permissions {
permissions[service] = methods.GetMethods()
}
tokenData := &controllers.TokenData{
Name: in.TokenMetadata.GetName(),
UserID: claims.UserID,
ExpiresAt: in.TokenMetadata.ExpiresAt.AsTime(),
Scopes: permissions,
}
token, err := srv.tokenCtrl.Create(ctx, tokenData)
if err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't create a token")
}
return &tokens.CreateTokenResponse{
TokenValue: &tokens.TokenValue{Token: token},
}, nil
}
// ForceTokenExpiration implements [v1.TokensServiceServer].
func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) {
claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
}
if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't create a token")
}
return &emptypb.Empty{}, nil
}
// GetToken implements [v1.TokensServiceServer].
func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) {
claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
}
token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID)
if err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
return &tokens.GetTokenResponse{
TokenUuid: &tokens.TokenUUID{
Uuid: token.UUID,
},
TokenMetadata: &tokens.TokenMetadata{
Name: token.Name,
ExpiresAt: timestamppb.New(token.ExpiresAt),
LastUsedAt: timestamppb.New(token.LastUsedAt),
GeneratedAt: timestamppb.New(token.GeneratedAt),
CreatedAt: timestamppb.New(token.CreatedAt),
RevokedAt: timestamppb.New(token.RevokedAt),
},
TokenPermissions: &tokens.TokenPermissions{},
}, nil
}
// ListTokens implements [v1.TokensServiceServer].
func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListTokensResponse]) error {
claims, err := srv.authorizationCtrl.ClaimsFromContext(stream.Context())
if err != nil {
return status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return status.Error(codes.Aborted, "Context is invalid")
}
tokensRes, err := srv.tokenCtrl.List(stream.Context(), claims.UserID)
if err != nil {
if errors.Is(err, controllers.ErrServerError) {
return status.Error(codes.Internal, "Something is broken on our side")
}
return status.Error(codes.Aborted, "Couldn't list tokens")
}
for _, tokenRes := range tokensRes {
if err := stream.Send(&tokens.ListTokensResponse{
TokenUuid: &tokens.TokenUUID{
Uuid: tokenRes.UUID,
},
TokenMetadata: &tokens.TokenMetadata{
Name: tokenRes.Name,
ExpiresAt: timestamppb.New(tokenRes.ExpiresAt),
LastUsedAt: timestamppb.New(tokenRes.LastUsedAt),
GeneratedAt: timestamppb.New(tokenRes.GeneratedAt),
CreatedAt: timestamppb.New(tokenRes.CreatedAt),
RevokedAt: timestamppb.New(tokenRes.RevokedAt),
},
}); err != nil {
return status.Error(codes.Aborted, "Couldn't send data")
}
}
return nil
}
// RegenerateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) {
claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
}
tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid())
if err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
return &tokens.RegenerateTokenResponse{
TokenValue: &tokens.TokenValue{
Token: tokenVal,
},
}, nil
}
// UpdateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) {
claims, err := srv.authorizationCtrl.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
}
if in.TokenPermissions == nil {
return nil, status.Error(codes.InvalidArgument, "Permissions must be set")
}
permissions := map[string][]string{}
for service, methods := range in.TokenPermissions.Permissions {
permissions[service] = methods.GetMethods()
}
tokenData := &controllers.TokenData{
Name: in.TokenMetadata.Name,
Scopes: permissions,
}
if err := srv.tokenCtrl.Update(ctx, tokenData); err != nil {
if errors.Is(err, controllers.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
return &tokens.UpdateTokenResponse{
TokenUuid: &tokens.TokenUUID{},
TokenMetadata: &tokens.TokenMetadata{},
TokenPermissions: &tokens.TokenPermissions{},
}, nil
}
// ListPermissions implements [v1.TokensServiceServer].
func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListPermissionsResponse]) error {
data := srv.tokenCtrl.ListPermissions(stream.Context())
for key, data := range data {
result := &tokens.ListPermissionsResponse{
Permissions: &tokens.TokenPermissions{
Permissions: map[string]*tokens.MethodList{
key: {Methods: data},
},
},
}
if err := stream.Send(result); err != nil {
return status.Error(codes.Aborted, "Couldn't send data to the client")
}
}
return nil
}