From 9a94aa0694abda5bdd7c9380cbadc1f080a8027e Mon Sep 17 00:00:00 2001 From: Elias Schneider Date: Mon, 6 Jul 2026 23:34:33 +0200 Subject: [PATCH] refactor: move Pocket ID specific logic from Fosite into Pocket ID repo --- backend/go.mod | 2 +- backend/go.sum | 4 +- backend/internal/oidc/api_resource.go | 80 +++++++++++++------ .../internal/oidc/authorization_service.go | 2 +- backend/internal/oidc/device_service.go | 2 +- backend/internal/oidc/token_handler.go | 2 +- 6 files changed, 63 insertions(+), 29 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index afe0c4bc..48713965 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -248,4 +248,4 @@ require ( modernc.org/memory v1.11.0 // indirect ) -replace github.com/ory/fosite => github.com/pocket-id/fosite v0.0.0-20260706141717-ee376ba4821b +replace github.com/ory/fosite => github.com/pocket-id/fosite v0.0.0-20260706205234-b54f41b16dc0 diff --git a/backend/go.sum b/backend/go.sum index b0e02011..446071ca 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -426,8 +426,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pocket-id/fosite v0.0.0-20260706141717-ee376ba4821b h1:/y/hIU6OzNiFWFd2eXSAR4wgbgU592TP5hXuszoPPjY= -github.com/pocket-id/fosite v0.0.0-20260706141717-ee376ba4821b/go.mod h1:KeQ7tTIBm3DyeBnKcKLnPbSdrd6ttM6w3TD3yy9x8rM= +github.com/pocket-id/fosite v0.0.0-20260706205234-b54f41b16dc0 h1:eD/P6k1xIxWDHXXGl4b7MyGBq4T1uq6w4DEWNuUHJew= +github.com/pocket-id/fosite v0.0.0-20260706205234-b54f41b16dc0/go.mod h1:KeQ7tTIBm3DyeBnKcKLnPbSdrd6ttM6w3TD3yy9x8rM= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= diff --git a/backend/internal/oidc/api_resource.go b/backend/internal/oidc/api_resource.go index 3efbd561..64324026 100644 --- a/backend/internal/oidc/api_resource.go +++ b/backend/internal/oidc/api_resource.go @@ -8,13 +8,14 @@ import ( "gorm.io/gorm" ) -type SubjectType = fosite.ResourceIndicatorSubjectType +// SubjectType identifies the subject the requested access token will represent +type SubjectType string const ( // SubjectTypeUser covers user-delegated access: every flow whose access token acts on behalf of an end user - SubjectTypeUser = fosite.ResourceOwnerSubject + SubjectTypeUser SubjectType = "user" // SubjectTypeClient covers client access: the client credentials grant, where the client acts as itself without a user - SubjectTypeClient = fosite.ClientSubject + SubjectTypeClient SubjectType = "client" ) var standardScopes = fosite.Arguments{"openid", "profile", "email", "groups", "offline_access"} @@ -42,32 +43,65 @@ type APIAccessProvider interface { DescribePermissions(ctx context.Context, audience string, keys []string) ([]PermissionInfo, error) } -type resourceAccessProvider struct { - provider APIAccessProvider -} - -func fositeResourceAccess(provider APIAccessProvider) fosite.ResourceIndicatorAccessProvider { - if provider == nil { - return nil - } - return resourceAccessProvider{provider: provider} -} - -func (p resourceAccessProvider) AllowedScopesForResource(ctx context.Context, client fosite.Client, resource string, subjectType fosite.ResourceIndicatorSubjectType) (fosite.Arguments, bool, error) { - scopes, exists, err := p.provider.AllowedScopesForAudience(ctx, client.GetID(), resource, subjectType) - return scopes, exists, err -} - // resolveResource maps an RFC 8707 resource, which may be empty, to the audience to stamp on the issued token and the subset of requestedScopes that may be granted // An empty resource is a plain login token bound to the requesting client and yields only identity scopes // The subject type selects which of the client's grants apply: user-delegated flows only see user grants, the client credentials grant only sees client grants func resolveResource(ctx context.Context, provider APIAccessProvider, clientID, resource string, requestedScopes []string, subjectType SubjectType) (audience string, grantedScopes []string, err error) { - grant, err := fosite.ResolveResourceIndicatorGrant(ctx, fositeResourceAccess(provider), &fosite.DefaultClient{ID: clientID}, resource, requestedScopes, standardScopes, subjectType) - if err != nil { - return "", nil, err + grantable := make(map[string]struct{}, len(standardScopes)) + for _, scope := range standardScopes { + grantable[scope] = struct{}{} } - return grant.Audience, grant.Scopes, nil + if resource == "" { + // A plain login token is audienced to the requesting client + audience = clientID + } else { + if !fosite.IsValidResourceIndicatorURI(resource) || provider == nil { + return "", nil, fosite.ErrInvalidTarget.WithHintf("The requested resource '%s' is invalid, missing, unknown, or malformed.", resource) + } + + allowedScopes, apiExists, err := provider.AllowedScopesForAudience(ctx, clientID, resource, subjectType) + if err != nil { + return "", nil, err + } + if !apiExists { + return "", nil, fosite.ErrInvalidTarget.WithHintf("The requested resource '%s' is invalid, missing, unknown, or malformed.", resource) + } + if len(allowedScopes) == 0 { + return "", nil, fosite.ErrAccessDenied.WithHintf("The OAuth 2.0 Client is not allowed to access resource '%s'.", resource) + } + + audience = resource + for _, scope := range allowedScopes { + grantable[scope] = struct{}{} + } + // A client credentials request without explicit scopes gets everything the client is granted for the API + if subjectType == SubjectTypeClient && len(requestedScopes) == 0 { + requestedScopes = allowedScopes + } + } + + granted := make(fosite.Arguments, 0, len(requestedScopes)) + for _, scope := range requestedScopes { + if _, ok := grantable[scope]; !ok { + return "", nil, fosite.ErrInvalidScope.WithHintf("The scope '%s' is not available for the requested resource.", scope) + } + if !granted.Has(scope) { + granted = append(granted, scope) + } + } + + return audience, granted, nil +} + +// grantResourceIndicator applies a resolved resource audience and scopes to the request +func grantResourceIndicator(requester fosite.Requester, audience string, scopes []string) { + for _, scope := range scopes { + requester.GrantScope(scope) + } + if audience != "" { + requester.GrantAudience(audience) + } } // consentScopeKey qualifies a custom-API scope by its audience so the same permission key on two different APIs is consented to separately diff --git a/backend/internal/oidc/authorization_service.go b/backend/internal/oidc/authorization_service.go index c41b7f95..7302dc9a 100644 --- a/backend/internal/oidc/authorization_service.go +++ b/backend/internal/oidc/authorization_service.go @@ -270,7 +270,7 @@ func (s *authorizationService) authorizeAuthenticated(ctx context.Context, req a session := s.buildAuthorizedSession(req, interactionSession, authenticationTime) - req.requester.GrantResourceIndicator(audience, grantedScopes) + grantResourceIndicator(req.requester, audience, grantedScopes) authorizationEvent := model.AuditLogEventClientAuthorization if !hasAlreadyAuthorizedClient { diff --git a/backend/internal/oidc/device_service.go b/backend/internal/oidc/device_service.go index 2e4ec219..81235bff 100644 --- a/backend/internal/oidc/device_service.go +++ b/backend/internal/oidc/device_service.go @@ -96,7 +96,7 @@ func (s *deviceService) acceptDeviceCode(ctx context.Context, userCode, userID, if err != nil { return err } - request.GrantResourceIndicator(audience, grantedScopes) + grantResourceIndicator(request, audience, grantedScopes) return withTx(ctx, s.db, func(ctx context.Context) error { if client.RequiresReauthentication { diff --git a/backend/internal/oidc/token_handler.go b/backend/internal/oidc/token_handler.go index 44c83ea2..c01df191 100644 --- a/backend/internal/oidc/token_handler.go +++ b/backend/internal/oidc/token_handler.go @@ -74,7 +74,7 @@ func (h *tokenHandler) token(c *gin.Context) { accessReq.GrantedScope = grantedScopes accessReq.GrantedAudience = nil } - accessRequest.GrantResourceIndicator(audience, grantedScopes) + grantResourceIndicator(accessRequest, audience, grantedScopes) } }