mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 04:03:47 +03:00
124 lines
5.2 KiB
Go
124 lines
5.2 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
|
|
"github.com/ory/fosite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// 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 SubjectType = "user"
|
|
// SubjectTypeClient covers client access: the client credentials grant, where the client acts as itself without a user
|
|
SubjectTypeClient SubjectType = "client"
|
|
)
|
|
|
|
var standardScopes = fosite.Arguments{"openid", "profile", "email", "groups", "offline_access"}
|
|
|
|
func isStandardScope(scope string) bool {
|
|
return slices.Contains(standardScopes, scope)
|
|
}
|
|
|
|
// PermissionInfo is the display information for an API permission used to show friendly names and descriptions on the consent screen
|
|
type PermissionInfo struct {
|
|
Key string
|
|
Name string
|
|
Description string
|
|
}
|
|
|
|
// APIAccessProvider is implemented by the api feature module
|
|
// It lets the OIDC module widen per-client scope and audience validation and resolve RFC 8707 resources to the permission keys a client may be granted
|
|
type APIAccessProvider interface {
|
|
// ClientAPIScopes returns the custom-API permission keys and the distinct API audiences a client is allowed to request across all subject types
|
|
ClientAPIScopes(ctx context.Context, tx *gorm.DB, clientID string) (scopes []string, audiences []string, err error)
|
|
// AllowedScopesForAudience returns the permission keys the client is allowed for the API identified by the given audience and subject type, and whether such an API exists
|
|
AllowedScopesForAudience(ctx context.Context, tx *gorm.DB, clientID, audience string, subjectType SubjectType) (scopes []string, apiExists bool, err error)
|
|
// DescribePermissions returns the display information for the given permission keys of the API identified by audience
|
|
// Unknown keys are omitted
|
|
DescribePermissions(ctx context.Context, audience string, keys []string) ([]PermissionInfo, error)
|
|
}
|
|
|
|
// 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, tx *gorm.DB, provider APIAccessProvider, clientID, resource string, requestedScopes []string, subjectType SubjectType) (audience string, grantedScopes []string, err error) {
|
|
grantable := make(map[string]struct{}, len(standardScopes))
|
|
for _, scope := range standardScopes {
|
|
grantable[scope] = struct{}{}
|
|
}
|
|
|
|
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, tx, 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
|
|
// The unit-separator delimiter is collision-free because audiences are validated as URIs and permission keys are restricted to RFC 6749 scope-token characters, so neither can contain it
|
|
// Standard identity scopes stay bare for backward compatibility with existing consents
|
|
func consentScopeKey(audience, scope string) string {
|
|
if isStandardScope(scope) {
|
|
return scope
|
|
}
|
|
return audience + "\x1f" + scope
|
|
}
|
|
|
|
func consentScopeKeys(audience string, scopes []string) []string {
|
|
keys := make([]string, len(scopes))
|
|
for i, scope := range scopes {
|
|
keys[i] = consentScopeKey(audience, scope)
|
|
}
|
|
return keys
|
|
}
|