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: 9 additions & 0 deletions api/discovery.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ paths:
code_challenge_methods_supported:
- S256
authorization_response_iss_parameter_supported: true
authorization_grant_profiles_supported:
- "urn:ietf:params:oauth:grant-profile:id-jag"

/.well-known/openid-configuration:
get:
Expand Down Expand Up @@ -116,6 +118,8 @@ paths:
- email_verified
claims_parameter_supported: true
authorization_response_iss_parameter_supported: true
authorization_grant_profiles_supported:
- "urn:ietf:params:oauth:grant-profile:id-jag"
"500":
description: Internal server error — metadata could not be generated.

Expand Down Expand Up @@ -187,6 +191,11 @@ components:
authorization_response_iss_parameter_supported:
type: boolean
description: Whether the `iss` parameter is included in authorization responses (RFC 9207).
authorization_grant_profiles_supported:
type: array
items:
type: string
description: Authorization grant profiles supported, e.g. the ID-JAG profile (`urn:ietf:params:oauth:grant-profile:id-jag`).

OIDCProviderMetadata:
allOf:
Expand Down
5 changes: 5 additions & 0 deletions backend/internal/oauth/oauth2/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ const (
CIBAMaxExpiresInSeconds = 600
)

const (
// SupportedAuthorizationGrantProfileIDJAG is the constant for supported authorization grant profile ID-JAG.
SupportedAuthorizationGrantProfileIDJAG = "urn:ietf:params:oauth:grant-profile:id-jag"
)

// GetSupportedResponseTypes returns all supported OAuth2 response types.
func GetSupportedResponseTypes(oauthConfig oauthconfig.Config) []string {
allowedResponseTypes := oauthConfig.OAuth.AllowedResponseTypes
Expand Down
101 changes: 101 additions & 0 deletions backend/internal/oauth/oauth2/discovery/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,4 +498,105 @@ func (suite *DiscoveryTestSuite) TestOIDCDiscovery_DeduplicatesAlgorithms() {
assert.Contains(suite.T(), algs, "RS256")
}

func (suite *DiscoveryTestSuite) TestAuthorizationGrantProfilesSupported_JWTBearerEnabled() {
suite.cryptoMock.EXPECT().GetPublicKeys(mock.Anything, providers.PublicKeyFilter{}).
Return([]providers.PublicKeyInfo{{KeyID: "k1", Algorithm: string(cryptolib.AlgorithmRS256)}}, nil)

meta, err := suite.discoveryService.GetOIDCMetadata(context.Background())
assert.NoError(suite.T(), err)

// Default config allows the JWT Bearer grant type, so the ID-JAG profile should be advertised.
assert.Contains(suite.T(), meta.GrantTypesSupported, string(providers.GrantTypeJWTBearer))
assert.Contains(
suite.T(), meta.AuthorizationGrantProfilesSupported, constants.SupportedAuthorizationGrantProfileIDJAG)
}

func (suite *DiscoveryTestSuite) TestAuthorizationGrantProfilesSupported_JWTBearerDisabled() {
testConfig := &config.Config{
Server: engineconfig.ServerConfig{Hostname: "localhost", Port: 8080},
JWT: engineconfig.JWTConfig{Issuer: "https://auth.example.com"},
OAuth: engineconfig.OAuthConfig{
AllowedGrantTypes: []string{"client_credentials", "refresh_token"},
},
}
cryptoMock := cryptomock.NewRuntimeCryptoProviderMock(suite.T())
cryptoMock.EXPECT().GetSupportedSigningAlgorithms().Return(suite.oauthCfg.OAuth.DPoP.AllowedAlgs).Maybe()
cryptoMock.EXPECT().GetSupportedEncryptionAlgorithms().
Return([]string{string(cryptolib.AlgorithmRSAOAEP256)}).Maybe()
cryptoMock.EXPECT().GetPublicKeys(mock.Anything, providers.PublicKeyFilter{}).
Return([]providers.PublicKeyInfo{{KeyID: "k1", Algorithm: string(cryptolib.AlgorithmRS256)}}, nil)

svc := newDiscoveryService(cryptoMock, newTestJWEService(cryptoMock), oauthCfgFromServerConfig(testConfig))
meta, err := svc.GetOIDCMetadata(context.Background())
assert.NoError(suite.T(), err)

assert.NotContains(suite.T(), meta.GrantTypesSupported, string(providers.GrantTypeJWTBearer))
assert.Empty(suite.T(), meta.AuthorizationGrantProfilesSupported)

body, err := json.Marshal(meta)
assert.NoError(suite.T(), err)
assert.NotContains(suite.T(), string(body), "authorization_grant_profiles_supported")
}

func (suite *DiscoveryTestSuite) TestAuthorizationGrantProfilesSupported_AdvertisedOverHTTP() {
suite.cryptoMock.EXPECT().GetPublicKeys(mock.Anything, providers.PublicKeyFilter{}).
Return([]providers.PublicKeyInfo{{KeyID: "k1", Algorithm: string(cryptolib.AlgorithmRS256)}}, nil)

req := httptest.NewRequest("GET", "/.well-known/openid-configuration", nil)
w := httptest.NewRecorder()
suite.handler.HandleOIDCDiscovery(w, req)
assert.Equal(suite.T(), http.StatusOK, w.Code)

var metadata OIDCProviderMetadata
err := json.NewDecoder(w.Body).Decode(&metadata)
assert.NoError(suite.T(), err)
assert.Equal(
suite.T(),
[]string{constants.SupportedAuthorizationGrantProfileIDJAG},
metadata.AuthorizationGrantProfilesSupported,
)
}

func (suite *DiscoveryTestSuite) TestAuthorizationGrantProfilesSupported_OnOAuth2AuthorizationServerMetadata() {
req := httptest.NewRequest("GET", "/.well-known/oauth-authorization-server", nil)
w := httptest.NewRecorder()

suite.handler.HandleOAuth2AuthorizationServerMetadata(w, req)

assert.Equal(suite.T(), http.StatusOK, w.Code)

var metadata OAuth2AuthorizationServerMetadata
err := json.NewDecoder(w.Body).Decode(&metadata)
assert.NoError(suite.T(), err)

// Default config allows the JWT Bearer grant type, so the ID-JAG profile should be
// advertised on the OAuth 2.0 Authorization Server Metadata endpoint too.
assert.Contains(suite.T(), metadata.GrantTypesSupported, string(providers.GrantTypeJWTBearer))
assert.Equal(
suite.T(),
[]string{constants.SupportedAuthorizationGrantProfileIDJAG},
metadata.AuthorizationGrantProfilesSupported,
)
}

func (suite *DiscoveryTestSuite) TestAuthorizationGrantProfilesSupported_NotOnOAuth2ServerMetadataWhenUnsupported() {
testConfig := &config.Config{
Server: engineconfig.ServerConfig{Hostname: "localhost", Port: 8080},
JWT: engineconfig.JWTConfig{Issuer: "https://auth.example.com"},
OAuth: engineconfig.OAuthConfig{
AllowedGrantTypes: []string{"client_credentials", "refresh_token"},
},
}
svc := newDiscoveryService(
suite.cryptoMock, newTestJWEService(suite.cryptoMock), oauthCfgFromServerConfig(testConfig))
handler := newDiscoveryHandler(svc)

req := httptest.NewRequest("GET", "/.well-known/oauth-authorization-server", nil)
w := httptest.NewRecorder()
handler.HandleOAuth2AuthorizationServerMetadata(w, req)

assert.Equal(suite.T(), http.StatusOK, w.Code)
assert.NotContains(suite.T(), w.Body.String(), "authorization_grant_profiles_supported")
}

func boolPtr(b bool) *bool { return &b }
1 change: 1 addition & 0 deletions backend/internal/oauth/oauth2/discovery/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type OAuth2AuthorizationServerMetadata struct {
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported,omitempty"`
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported"`
DPoPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"`
AuthorizationGrantProfilesSupported []string `json:"authorization_grant_profiles_supported,omitempty"`
}

// OIDCProviderMetadata represents OpenID Connect Provider Metadata (OIDC Discovery 1.0)
Expand Down
11 changes: 11 additions & 0 deletions backend/internal/oauth/oauth2/discovery/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func (ds *discoveryService) GetOAuth2AuthorizationServerMetadata(
CodeChallengeMethodsSupported: ds.getSupportedCodeChallengeMethods(),
AuthorizationResponseIssParameterSupported: true,
DPoPSigningAlgValuesSupported: ds.getSupportedDPoPSigningAlgs(),
AuthorizationGrantProfilesSupported: ds.getSupportedAuthorizationGrantProfiles(),
}

if slices.Contains(metadata.GrantTypesSupported, string(providers.GrantTypeCIBA)) {
Expand Down Expand Up @@ -244,3 +245,13 @@ func (ds *discoveryService) getSupportedClaims() []string {

return uniqueClaims
}

func (ds *discoveryService) getSupportedAuthorizationGrantProfiles() []string {
supportedProfiles := make([]string, 0)
// support Identity Assertion JWT Authorization Grant profile if the JWT Bearer grant type is supported
if slices.Contains(ds.getSupportedGrantTypes(), string(providers.GrantTypeJWTBearer)) {
supportedProfiles = append(supportedProfiles, string(constants.SupportedAuthorizationGrantProfileIDJAG))
}

return supportedProfiles
}
Loading