refactor: move Pocket ID specific logic from Fosite into Pocket ID repo

This commit is contained in:
Elias Schneider
2026-07-06 23:34:33 +02:00
parent 34e9a6d198
commit 9a94aa0694
6 changed files with 63 additions and 29 deletions

View File

@@ -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

View File

@@ -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=

View File

@@ -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

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}
}