Files
pocket-id-pocket-id/backend/internal/oidc/access_token_scope.go
2026-07-06 12:25:02 -07:00

60 lines
2.6 KiB
Go

package oidc
import (
"context"
"slices"
"github.com/ory/fosite"
fositeoauth2 "github.com/ory/fosite/handler/oauth2"
)
// isIdentityScope reports whether the scope is an OIDC identity scope whose presence lets a token be presented to Pocket ID's own identity endpoints such as /userinfo
// offline_access is deliberately excluded: it only requests a refresh token and is not tied to any resource server
func isIdentityScope(scope string) bool {
return scope != "offline_access" && isStandardScope(scope)
}
// hasIdentityScope reports whether any of the granted scopes is an OIDC identity scope
func hasIdentityScope(scopes fosite.Arguments) bool {
return slices.ContainsFunc(scopes, isIdentityScope)
}
// withIdentityAudience returns a view of the requester whose granted audience additionally includes the issuer when the token carries an identity scope
// This lets an access token that was granted an identity scope be presented to Pocket ID's own identity endpoints such as /userinfo, even when it was also audienced to a custom API
// The issuer is added only to the materialized access token and never to the underlying grant, so a refresh does not have to re-whitelist the issuer against the client and a machine token whose identity scopes were stripped never receives it
func withIdentityAudience(requester fosite.Requester, issuer string) fosite.Requester {
if issuer == "" || !hasIdentityScope(requester.GetGrantedScopes()) {
return requester
}
granted := requester.GetGrantedAudience()
if granted.Has(issuer) {
return requester
}
audience := make(fosite.Arguments, 0, len(granted)+1)
audience = append(audience, granted...)
audience = append(audience, issuer)
return identityAudienceRequester{Requester: requester, grantedAudience: audience}
}
// identityAudienceRequester overrides only the granted audience of the wrapped requester
type identityAudienceRequester struct {
fosite.Requester
grantedAudience fosite.Arguments
}
func (r identityAudienceRequester) GetGrantedAudience() fosite.Arguments {
return r.grantedAudience
}
// identityAudienceAccessTokenStrategy wraps the access token strategy so an access token granted an identity scope also lists the issuer in its audience, keeping the self-contained JWT consistent with what is persisted for introspection and userinfo
type identityAudienceAccessTokenStrategy struct {
fositeoauth2.CoreStrategy
issuer string
}
func (s identityAudienceAccessTokenStrategy) GenerateAccessToken(ctx context.Context, requester fosite.Requester) (string, string, error) {
return s.CoreStrategy.GenerateAccessToken(ctx, withIdentityAudience(requester, s.issuer))
}