Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/cmd/server/servicemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ func registerServices(mux *http.ServeMux, cacheManager cache.CacheManagerInterfa
// CORS origins come from the server-config cors section.
cors.InitializeDynamicMatcher(serverConfigService)

// Decorate the resource provider so an empty identifier resolves the configured default resource
// server. Keeps the default-resource-server policy server-side: OAuth, CIBA, PAR, grant handlers,
// and the flow executor depend only on providers.ResourceServerProvider, not on serverConfigService.
// The base resourceService is still used for resource-management APIs and server-config validation.
resourceServerProvider := resource.NewDefaultAwareResourceServerProvider(resourceService, serverConfigService)

flowConfig := flowconfig.FromServerRuntime()
sessionService, sessionCfg := initSessionService(ctx, serverConfigService, runtime.Config.Server.Identifier, logger)
flowConfig.Session = sessionCfg
Expand Down Expand Up @@ -345,6 +351,7 @@ func registerServices(mux *http.ServeMux, cacheManager cache.CacheManagerInterfa
GoogleSvc: googleAuthnService,
OpenID4VPVerifierSvc: openid4vpSvc,
SessionService: sessionService,
ResourceService: resourceServerProvider,
},
interceptor.InterceptorDependencies{},
flowConfig,
Expand Down Expand Up @@ -450,7 +457,7 @@ func registerServices(mux *http.ServeMux, cacheManager cache.CacheManagerInterfa
// Initialize OAuth services.
err = oauth.Initialize(mux, actorProvider, authnProvider, jwtService, jweService,
flowExecService, observabilitySvc, runtimeCryptoSvc, ouService, attributeCacheService, authZService,
resourceService, serverConfigService, i18nService, idpService, dpopVerifier,
resourceServerProvider, i18nService, idpService, dpopVerifier,
runtimeStoreProvider, transactioner, oauthCfg)
fatalOnError(ctx, logger, err, "Failed to initialize OAuth services")

Expand Down
5 changes: 5 additions & 0 deletions backend/internal/flow/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ const (
RuntimeKeyClientID = "clientId"
// RuntimeKeyRequestedPermissions holds the space-separated permission scopes requested by the OAuth client.
RuntimeKeyRequestedPermissions = "requested_permissions"
// RuntimeKeyResourceServerIdentifier holds the identifier of the single resource server the request is
// bound to. When set, the authorization executor resolves it and scopes its permission evaluation to
// that resource server. Using the identifier (not the internal ID) keeps the executor contract the
// same for OAuth requests and direct flow executions.
RuntimeKeyResourceServerIdentifier = "resource_server_identifier"
// RuntimeKeyConsentedPermissions holds the space-separated permission scopes the user has consented to
// release to the client, as produced by the ConsentExecutor.
RuntimeKeyConsentedPermissions = "consented_permissions"
Expand Down
71 changes: 60 additions & 11 deletions backend/internal/flow/executor/authz_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"

"github.com/thunder-id/thunderid/internal/entityprovider"
"github.com/thunder-id/thunderid/internal/flow/common"
"github.com/thunder-id/thunderid/internal/flow/core"
"github.com/thunder-id/thunderid/internal/system/log"
"github.com/thunder-id/thunderid/internal/system/utils"
Expand All @@ -39,10 +40,11 @@ const (
// during flow execution. It enriches the flow context with authorized permissions.
type authorizationExecutor struct {
providers.Executor
authzService providers.AuthorizationProvider
entityProvider entityprovider.EntityProviderInterface
authnProvider providers.AuthnProviderManager
logger *log.Logger
authzService providers.AuthorizationProvider
entityProvider entityprovider.EntityProviderInterface
authnProvider providers.AuthnProviderManager
resourceService providers.ResourceServerProvider
logger *log.Logger
}

var _ providers.Executor = (*authorizationExecutor)(nil)
Expand All @@ -53,6 +55,7 @@ func newAuthorizationExecutor(
authZService providers.AuthorizationProvider,
entityProvider entityprovider.EntityProviderInterface,
authnProvider providers.AuthnProviderManager,
resourceService providers.ResourceServerProvider,
) *authorizationExecutor {
logger := log.GetLogger().With(log.String(log.LoggerKeyComponentName, authzLoggerComponentName),
log.String(log.LoggerKeyExecutorName, ExecutorNameAuthorization))
Expand All @@ -61,11 +64,12 @@ func newAuthorizationExecutor(
[]providers.Input{}, []providers.Input{}, &providers.ExecutorMeta{})

return &authorizationExecutor{
Executor: base,
authzService: authZService,
entityProvider: entityProvider,
authnProvider: authnProvider,
logger: logger,
Executor: base,
authzService: authZService,
entityProvider: entityProvider,
authnProvider: authnProvider,
resourceService: resourceService,
logger: logger,
}
}

Expand Down Expand Up @@ -110,6 +114,22 @@ func (a *authorizationExecutor) Execute(ctx *providers.NodeContext) (*providers.
return execResp, nil
}

// Resolve the single resource server the permission scopes are evaluated against: the OAuth layer
// seeds it in runtime data; a direct /flow/execute request (which does not go through the
// authorization endpoint) may supply it as an input; otherwise fall back to the configured default
// resource server. Permission evaluation must be scoped to a resource server, so when none can be
// resolved the requested permission scopes are dropped rather than evaluated unscoped (which could
// authorize a permission the user only holds on a different resource server).
resourceServerID := a.resolveResourceServerID(ctx)
if resourceServerID == "" {
logger.Debug(ctx.Context,
"No resource server bound to the request; dropping requested permission scopes",
log.Int("permissionCount", len(requestedPerms)))
setAuthorizedPermissions(execResp, []string{})
execResp.Status = providers.ExecComplete
return execResp, nil
}

logger.Debug(ctx.Context, "Determined required permissions", log.Int("count", len(requestedPerms)))

// Extract user ID and group IDs
Expand All @@ -125,7 +145,7 @@ func (a *authorizationExecutor) Execute(ctx *providers.NodeContext) (*providers.
log.Int("permissionCount", len(requestedPerms)))

authzResp, svcErr := a.authzService.EvaluateAccessBatch(ctx.Context,
a.buildAccessEvaluationsRequest(userID, groupIDs, requestedPerms))
a.buildAccessEvaluationsRequest(userID, groupIDs, requestedPerms, resourceServerID))
if svcErr != nil {
logger.Error(ctx.Context, "Authorization service call failed",
log.String("error", svcErr.Error.DefaultValue))
Expand All @@ -143,6 +163,33 @@ func (a *authorizationExecutor) Execute(ctx *providers.NodeContext) (*providers.
return execResp, nil
}

// resolveResourceServerID determines the internal ID of the single resource server that permission
// scopes are evaluated against. The binding is communicated as a resource server identifier: the OAuth
// layer seeds it in runtime data, and a direct /flow/execute request (which does not go through the
// authorization endpoint) may supply it as an input. The identifier is resolved to its internal ID
// through the provider; an empty identifier asks a default-aware provider to resolve the deployment's
// configured default resource server. Returns "" when none can be resolved (unknown identifier, no
// default configured, or no resource provider available, for example the embedded engine).
func (a *authorizationExecutor) resolveResourceServerID(ctx *providers.NodeContext) string {
identifier := ctx.RuntimeData[common.RuntimeKeyResourceServerIdentifier]
if identifier == "" {
identifier = ctx.UserInputs[common.RuntimeKeyResourceServerIdentifier]
}
if a.resourceService == nil {
a.logger.Debug(ctx.Context,
"No resource server service available; dropping requested permission scopes")
return ""
}
rs, svcErr := a.resourceService.GetResourceServerByIdentifier(ctx.Context, identifier)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we handle server errors here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will bring with a followup PR

if svcErr != nil {
a.logger.Debug(ctx.Context,
"Resource server did not resolve; dropping requested permission scopes",
log.String("identifier", identifier))
return ""
}
return rs.ID
}

// extractRequestedPermissions extracts requested permissions from the context.
func extractRequestedPermissions(ctx *providers.NodeContext) []string {
requestedPermissions := ctx.RuntimeData[requestedPermissionsKey]
Expand All @@ -163,6 +210,7 @@ func (a *authorizationExecutor) buildAccessEvaluationsRequest(
entityID string,
groupIDs []string,
requestedPermissions []string,
resourceServerID string,
) providers.AccessEvaluationsRequest {
evaluations := make([]providers.AccessEvaluationRequest, 0, len(requestedPermissions))
for _, permission := range requestedPermissions {
Expand All @@ -171,7 +219,8 @@ func (a *authorizationExecutor) buildAccessEvaluationsRequest(
ID: entityID,
GroupIDs: groupIDs,
},
Permission: providers.Permission{Name: permission},
ResourceServer: providers.AccessEvaluationResourceServer{ID: resourceServerID},
Permission: providers.Permission{Name: permission},
})
}
return providers.AccessEvaluationsRequest{Evaluations: evaluations}
Expand Down
Loading
Loading