diff --git a/backend/internal/api/module.go b/backend/internal/api/module.go index 17154208..68da6cdb 100644 --- a/backend/internal/api/module.go +++ b/backend/internal/api/module.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "gorm.io/gorm" + "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/oidc" ) @@ -39,19 +40,19 @@ func (m *Module) AllowedScopesForAudience(ctx context.Context, tx *gorm.DB, clie } // DescribePermissions implements the OIDC module's APIAccessProvider interface -func (m *Module) DescribePermissions(ctx context.Context, audience string, keys []string) ([]oidc.PermissionInfo, error) { +func (m *Module) DescribePermissions(ctx context.Context, audience string, keys []string) ([]dto.ScopeInfoDto, error) { permissions, err := m.service.DescribePermissions(ctx, audience, keys) if err != nil { return nil, err } - infos := make([]oidc.PermissionInfo, len(permissions)) + infos := make([]dto.ScopeInfoDto, len(permissions)) for i, permission := range permissions { description := "" if permission.Description != nil { description = *permission.Description } - infos[i] = oidc.PermissionInfo{Key: permission.Key, Name: permission.Name, Description: description} + infos[i] = dto.ScopeInfoDto{Key: permission.Key, Name: permission.Name, Description: description} } return infos, nil diff --git a/backend/internal/oidc/api_resource.go b/backend/internal/oidc/api_resource.go index d6078e25..e2e9d854 100644 --- a/backend/internal/oidc/api_resource.go +++ b/backend/internal/oidc/api_resource.go @@ -5,6 +5,7 @@ import ( "slices" "github.com/ory/fosite" + "github.com/pocket-id/pocket-id/backend/internal/dto" "gorm.io/gorm" ) @@ -24,13 +25,6 @@ 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 { @@ -40,7 +34,7 @@ type APIAccessProvider interface { 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) + DescribePermissions(ctx context.Context, audience string, keys []string) ([]dto.ScopeInfoDto, 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 diff --git a/backend/internal/oidc/api_resource_test.go b/backend/internal/oidc/api_resource_test.go index 07baf32f..b19e7df7 100644 --- a/backend/internal/oidc/api_resource_test.go +++ b/backend/internal/oidc/api_resource_test.go @@ -8,6 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/dto" ) // fakeAPIAccess implements APIAccessProvider from an audience -> subject type -> allowed-scopes map. @@ -50,16 +52,16 @@ func (f fakeAPIAccess) AllowedScopesForAudience(_ context.Context, _ *gorm.DB, _ return bySubject[subjectType], true, nil } -func (f fakeAPIAccess) DescribePermissions(_ context.Context, audience string, keys []string) ([]PermissionInfo, error) { +func (f fakeAPIAccess) DescribePermissions(_ context.Context, audience string, keys []string) ([]dto.ScopeInfoDto, error) { bySubject, ok := f.allowed[audience] if !ok { return nil, nil } - var infos []PermissionInfo + var infos []dto.ScopeInfoDto for _, key := range keys { for _, allowed := range bySubject { if slices.Contains(allowed, key) { - infos = append(infos, PermissionInfo{Key: key, Name: key}) + infos = append(infos, dto.ScopeInfoDto{Key: key, Name: key}) break } } diff --git a/backend/internal/oidc/authorization_service.go b/backend/internal/oidc/authorization_service.go index a2f88a72..7eeb88a0 100644 --- a/backend/internal/oidc/authorization_service.go +++ b/backend/internal/oidc/authorization_service.go @@ -12,6 +12,7 @@ import ( "github.com/ory/fosite" "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/internal/dto" "github.com/pocket-id/pocket-id/backend/internal/model" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" "github.com/pocket-id/pocket-id/backend/internal/utils" @@ -525,7 +526,7 @@ func (s *authorizationService) buildInteractionForUser(ctx context.Context, inte } // Always serialize a possibly empty array rather than null if scopeInfo == nil { - scopeInfo = []scopeInfoDto{} + scopeInfo = []dto.ScopeInfoDto{} } result.ScopeInfo = scopeInfo @@ -534,13 +535,13 @@ func (s *authorizationService) buildInteractionForUser(ctx context.Context, inte // resolveScopeInfo resolves display names and descriptions for the requested non-standard scopes, looked up against the API targeted by the request's RFC 8707 resource // Standard identity scopes are rendered by the client -func (s *authorizationService) resolveScopeInfo(ctx context.Context, interactionSession InteractionSession) ([]scopeInfoDto, error) { +func (s *authorizationService) resolveScopeInfo(ctx context.Context, interactionSession InteractionSession) ([]dto.ScopeInfoDto, error) { return s.resolveScopeInfoForRequest(ctx, interactionSession.Parameters["resource"], interactionSession.Scopes) } // resolveScopeInfoForRequest resolves display names and descriptions for the requested non-standard scopes against the API identified by resource // The browser and device consent flows share it so both show friendly permission names instead of raw scope keys -func (s *authorizationService) resolveScopeInfoForRequest(ctx context.Context, resource string, scopes []string) ([]scopeInfoDto, error) { +func (s *authorizationService) resolveScopeInfoForRequest(ctx context.Context, resource string, scopes []string) ([]dto.ScopeInfoDto, error) { if s.apiAccess == nil { return nil, nil } @@ -564,12 +565,7 @@ func (s *authorizationService) resolveScopeInfoForRequest(ctx context.Context, r return nil, err } - scopeInfo := make([]scopeInfoDto, len(infos)) - for i, info := range infos { - scopeInfo[i] = scopeInfoDto(info) - } - - return scopeInfo, nil + return infos, nil } func (s *authorizationService) completeInteractionStep(ctx context.Context, interactionSessionID, userID string, step interactionStep, reauthenticationToken string, authenticationTime time.Time, meta requestMeta) (completeInteractionResponse, error) { diff --git a/backend/internal/oidc/device_service.go b/backend/internal/oidc/device_service.go index c5b0a5b3..6ea04232 100644 --- a/backend/internal/oidc/device_service.go +++ b/backend/internal/oidc/device_service.go @@ -50,6 +50,7 @@ func (s *deviceService) createDeviceAuthorization(ctx context.Context, req *http return nil, request, err } + // Validate the requested scopes and resolve the resource indicator to an audience and the subset of requested scopes that may be granted client := request.GetClient().(Client) resource, err := request.GetResource() if err != nil { @@ -192,13 +193,8 @@ func (s *deviceService) getDeviceCodeInfo(ctx context.Context, userCode, userID return nil, err } // Always serialize a possibly empty array rather than null - scopeInfoDtos := make([]dto.ScopeInfoDto, len(scopeInfo)) - for i, info := range scopeInfo { - scopeInfoDtos[i] = dto.ScopeInfoDto{ - Key: info.Key, - Name: info.Name, - Description: info.Description, - } + if scopeInfo == nil { + scopeInfo = []dto.ScopeInfoDto{} } return &dto.DeviceCodeInfoDto{ @@ -211,7 +207,7 @@ func (s *deviceService) getDeviceCodeInfo(ctx context.Context, userCode, userID RequiresReauthentication: client.RequiresReauthentication, }, Scope: request.GetRequestedScopes(), - ScopeInfo: scopeInfoDtos, + ScopeInfo: scopeInfo, AuthorizationRequired: authorizationRequired, ReauthenticationRequired: client.RequiresReauthentication, }, nil diff --git a/backend/internal/oidc/interaction_session_dto.go b/backend/internal/oidc/interaction_session_dto.go index 8ccb102a..7450b1db 100644 --- a/backend/internal/oidc/interaction_session_dto.go +++ b/backend/internal/oidc/interaction_session_dto.go @@ -16,18 +16,12 @@ const ( type interactionSessionForUser struct { ID string `json:"id"` Scopes []string `json:"scopes"` - ScopeInfo []scopeInfoDto `json:"scopeInfo"` + ScopeInfo []dto.ScopeInfoDto `json:"scopeInfo"` Client dto.OidcClientMetaDataDto `json:"client"` CurrentStep interactionStep `json:"currentStep,omitempty"` RequiredSteps []interactionStep `json:"requiredSteps"` } -type scopeInfoDto struct { - Key string `json:"key"` - Name string `json:"name"` - Description string `json:"description,omitempty"` -} - type completeInteractionRequest struct { Step interactionStep `json:"step"` }