Fix advertised scopes for oauth protected resources
Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -289,7 +289,7 @@ Scopes are namespace- or product-level only — no resource segments (e.g. `v1:p
|
|||||||
**Discovery:**
|
**Discovery:**
|
||||||
|
|
||||||
- Authorization server (RFC 8414): `scopes_supported` on `/.well-known/oauth-authorization-server` lists OIDC + all API scopes; `protected_resources` links to the resource metadata document
|
- Authorization server (RFC 8414): `scopes_supported` on `/.well-known/oauth-authorization-server` lists OIDC + all API scopes; `protected_resources` links to the resource metadata document
|
||||||
- Protected resource (RFC 9728): `scopes_supported` on `/.well-known/oauth-protected-resource` lists `openid` plus API scopes
|
- Protected resource (RFC 9728): `scopes_supported` on `/.well-known/oauth-protected-resource` lists `openid` plus write API scopes only (no `:read` suffix); matches CIMD client registration
|
||||||
|
|
||||||
**Enforcement:** OAuth2 bearer-token requests carry the validated access token on the request context (`pkg/iam/oauth2/request_context.go`). Before IAM policy evaluation, `iam.Authorizer` checks registered `oauth2scope.Registry` mappings via `Registry.Allows`. Each domain package exports `OAuth2ScopeMappings` in its `oauth2_scopes.go`; `probod` registers all domain mappings on the shared registry before `iam.NewService`. The check uses explicit scope→action lists — no `:read` / `:get` heuristics at enforcement time. Session, personal API key, and SCIM auth skip the check (no access token on context). Unmapped IAM actions **deny** OAuth requests (fail closed). Enforcement reads scopes from the access token directly.
|
**Enforcement:** OAuth2 bearer-token requests carry the validated access token on the request context (`pkg/iam/oauth2/request_context.go`). Before IAM policy evaluation, `iam.Authorizer` checks registered `oauth2scope.Registry` mappings via `Registry.Allows`. Each domain package exports `OAuth2ScopeMappings` in its `oauth2_scopes.go`; `probod` registers all domain mappings on the shared registry before `iam.NewService`. The check uses explicit scope→action lists — no `:read` / `:get` heuristics at enforcement time. Session, personal API key, and SCIM auth skip the check (no access token on context). Unmapped IAM actions **deny** OAuth requests (fail closed). Enforcement reads scopes from the access token directly.
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ func TestOAuth2_ProtectedResourceMetadata(t *testing.T) {
|
|||||||
assert.Contains(t, metadata.AuthorizationServers, expectedResource)
|
assert.Contains(t, metadata.AuthorizationServers, expectedResource)
|
||||||
assert.Contains(t, metadata.BearerMethodsSupported, "header")
|
assert.Contains(t, metadata.BearerMethodsSupported, "header")
|
||||||
assert.Contains(t, metadata.ScopesSupported, "openid")
|
assert.Contains(t, metadata.ScopesSupported, "openid")
|
||||||
assert.Contains(t, metadata.ScopesSupported, "v1:document:read")
|
assert.Contains(t, metadata.ScopesSupported, "v1:document")
|
||||||
|
assert.NotContains(t, metadata.ScopesSupported, "v1:document:read")
|
||||||
assert.NotContains(t, metadata.ScopesSupported, "profile")
|
assert.NotContains(t, metadata.ScopesSupported, "profile")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ type ProtectedResourceMetadata struct {
|
|||||||
func NewProtectedResourceMetadata(
|
func NewProtectedResourceMetadata(
|
||||||
resource uri.URI,
|
resource uri.URI,
|
||||||
authorizationServer uri.URI,
|
authorizationServer uri.URI,
|
||||||
registeredScopes []coredata.OAuth2Scope,
|
writeScopes []coredata.OAuth2Scope,
|
||||||
) *ProtectedResourceMetadata {
|
) *ProtectedResourceMetadata {
|
||||||
return &ProtectedResourceMetadata{
|
return &ProtectedResourceMetadata{
|
||||||
Resource: resource,
|
Resource: resource,
|
||||||
@@ -39,6 +39,6 @@ func NewProtectedResourceMetadata(
|
|||||||
BearerMethodsSupported: []string{
|
BearerMethodsSupported: []string{
|
||||||
"header",
|
"header",
|
||||||
},
|
},
|
||||||
ScopesSupported: protectedResourceScopes(registeredScopes),
|
ScopesSupported: protectedResourceScopes(writeScopes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,19 +32,21 @@ func TestNewProtectedResourceMetadata(t *testing.T) {
|
|||||||
reg := oauth2scope.NewRegistry().Register(
|
reg := oauth2scope.NewRegistry().Register(
|
||||||
map[coredata.OAuth2Scope][]string{
|
map[coredata.OAuth2Scope][]string{
|
||||||
probo.ScopeV1DocumentRead: {"core:document:get"},
|
probo.ScopeV1DocumentRead: {"core:document:get"},
|
||||||
|
probo.ScopeV1Document: {"core:document:create"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
resource := uri.URI("https://app.example.com")
|
resource := uri.URI("https://app.example.com")
|
||||||
authorizationServer := uri.URI("https://app.example.com")
|
authorizationServer := uri.URI("https://app.example.com")
|
||||||
|
|
||||||
metadata := oauth2.NewProtectedResourceMetadata(resource, authorizationServer, reg.RegisteredScopes())
|
metadata := oauth2.NewProtectedResourceMetadata(resource, authorizationServer, reg.AllWriteScopes())
|
||||||
require.NotNil(t, metadata)
|
require.NotNil(t, metadata)
|
||||||
|
|
||||||
assert.Equal(t, resource, metadata.Resource)
|
assert.Equal(t, resource, metadata.Resource)
|
||||||
assert.Equal(t, []uri.URI{authorizationServer}, metadata.AuthorizationServers)
|
assert.Equal(t, []uri.URI{authorizationServer}, metadata.AuthorizationServers)
|
||||||
assert.Equal(t, []string{"header"}, metadata.BearerMethodsSupported)
|
assert.Equal(t, []string{"header"}, metadata.BearerMethodsSupported)
|
||||||
assert.Contains(t, metadata.ScopesSupported, oauth2.ScopeOpenID)
|
assert.Contains(t, metadata.ScopesSupported, oauth2.ScopeOpenID)
|
||||||
assert.Contains(t, metadata.ScopesSupported, probo.ScopeV1DocumentRead)
|
assert.Contains(t, metadata.ScopesSupported, probo.ScopeV1Document)
|
||||||
|
assert.NotContains(t, metadata.ScopesSupported, probo.ScopeV1DocumentRead)
|
||||||
assert.NotContains(t, metadata.ScopesSupported, oauth2.ScopeProfile)
|
assert.NotContains(t, metadata.ScopesSupported, oauth2.ScopeProfile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ func authorizationServerScopes(registeredScopes []coredata.OAuth2Scope) []coreda
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func protectedResourceScopes(registeredScopes []coredata.OAuth2Scope) []coredata.OAuth2Scope {
|
func protectedResourceScopes(writeScopes []coredata.OAuth2Scope) []coredata.OAuth2Scope {
|
||||||
return slices.Concat(
|
return slices.Concat(
|
||||||
[]coredata.OAuth2Scope{ScopeOpenID},
|
[]coredata.OAuth2Scope{ScopeOpenID},
|
||||||
registeredScopes,
|
writeScopes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (s *Service) OAuth2ServerMetadata(endpoints oauth2.Endpoints) *oauth2.Serve
|
|||||||
|
|
||||||
// OAuth2ProtectedResourceMetadata returns the RFC 9728 protected resource metadata document.
|
// OAuth2ProtectedResourceMetadata returns the RFC 9728 protected resource metadata document.
|
||||||
func (s *Service) OAuth2ProtectedResourceMetadata(resource uri.URI) *oauth2.ProtectedResourceMetadata {
|
func (s *Service) OAuth2ProtectedResourceMetadata(resource uri.URI) *oauth2.ProtectedResourceMetadata {
|
||||||
return oauth2.NewProtectedResourceMetadata(resource, resource, s.OAuth2ScopeRegistry.RegisteredScopes())
|
return oauth2.NewProtectedResourceMetadata(resource, resource, s.OAuth2ScopeRegistry.AllWriteScopes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) IsSignUpEnabled() bool {
|
func (s *Service) IsSignUpEnabled() bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user