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
18 changes: 4 additions & 14 deletions backend/internal/agent/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1651,20 +1651,15 @@ func translateOAuthValidationError(err error) *tidcommon.ServiceError {
Key: "error.agentservice.private_key_jwt_cannot_have_client_secret_description",
DefaultValue: "private_key_jwt authentication method cannot have a client secret",
})
case errors.Is(err, inboundclient.ErrOAuthClientSecretCannotHaveCertificate):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.agentservice.client_secret_cannot_have_certificate_description",
DefaultValue: "client_secret authentication methods cannot have a certificate",
})
case errors.Is(err, inboundclient.ErrOAuthNoneAuthRequiresPublicClient):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.agentservice.none_auth_method_requires_public_client_description",
DefaultValue: "'none' authentication method requires the client to be a public client",
})
case errors.Is(err, inboundclient.ErrOAuthNoneAuthCannotHaveCertOrSecret):
case errors.Is(err, inboundclient.ErrOAuthNoneAuthCannotHaveSecret):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.agentservice.none_auth_method_cannot_have_cert_or_secret_description",
DefaultValue: "'none' authentication method cannot have a certificate or client secret",
Key: "error.agentservice.none_auth_method_cannot_have_secret_description",
DefaultValue: "'none' authentication method cannot have a client secret",
})
case errors.Is(err, inboundclient.ErrOAuthClientCredentialsCannotUseNoneAuth):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Expand Down Expand Up @@ -1730,11 +1725,6 @@ func translateUserInfoValidationError(err error) *tidcommon.ServiceError {
Key: "error.agentservice.userinfo_unsupported_response_type_description",
DefaultValue: "userinfo responseType is not supported",
})
case errors.Is(err, inboundclient.ErrOAuthUserInfoJWSRequiresSigningAlg):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.agentservice.userinfo_jws_requires_signing_alg_description",
DefaultValue: "signingAlg is required when userinfo responseType is JWS",
})
case errors.Is(err, inboundclient.ErrOAuthUserInfoJWERequiresEncryption):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.agentservice.userinfo_jwe_requires_encryption_description",
Expand All @@ -1743,7 +1733,7 @@ func translateUserInfoValidationError(err error) *tidcommon.ServiceError {
case errors.Is(err, inboundclient.ErrOAuthUserInfoNestedJWTRequiresAll):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.agentservice.userinfo_nested_jwt_requires_all_description",
DefaultValue: "signingAlg, encryptionAlg, and encryptionEnc are required " +
DefaultValue: "encryptionAlg and encryptionEnc are required " +
"when userinfo responseType is NESTED_JWT",
})
case errors.Is(err, inboundclient.ErrOAuthUserInfoAlgRequiresResponseType):
Expand Down
9 changes: 2 additions & 7 deletions backend/internal/agent/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1397,15 +1397,12 @@ func (suite *AgentServiceTestSuite) TestTranslateOAuthValidationError() {
{"PrivateKeyJWTCannotHaveClientSecret", inboundclient.ErrOAuthPrivateKeyJWTCannotHaveClientSecret,
ErrorInvalidOAuthConfiguration.Code,
"error.agentservice.private_key_jwt_cannot_have_client_secret_description"},
{"ClientSecretCannotHaveCertificate", inboundclient.ErrOAuthClientSecretCannotHaveCertificate,
ErrorInvalidOAuthConfiguration.Code,
"error.agentservice.client_secret_cannot_have_certificate_description"},
{"NoneAuthRequiresPublicClient", inboundclient.ErrOAuthNoneAuthRequiresPublicClient,
ErrorInvalidOAuthConfiguration.Code,
"error.agentservice.none_auth_method_requires_public_client_description"},
{"NoneAuthCannotHaveCertOrSecret", inboundclient.ErrOAuthNoneAuthCannotHaveCertOrSecret,
{"NoneAuthCannotHaveSecret", inboundclient.ErrOAuthNoneAuthCannotHaveSecret,
ErrorInvalidOAuthConfiguration.Code,
"error.agentservice.none_auth_method_cannot_have_cert_or_secret_description"},
"error.agentservice.none_auth_method_cannot_have_secret_description"},
{"ClientCredentialsCannotUseNoneAuth", inboundclient.ErrOAuthClientCredentialsCannotUseNoneAuth,
ErrorInvalidOAuthConfiguration.Code,
"error.agentservice.client_credentials_cannot_use_none_auth_description"},
Expand Down Expand Up @@ -1453,8 +1450,6 @@ func (suite *AgentServiceTestSuite) TestTranslateUserInfoValidationError() {
"error.agentservice.userinfo_jwks_uri_not_ssrf_safe_description"},
{"UnsupportedResponseType", inboundclient.ErrOAuthUserInfoUnsupportedResponseType,
"error.agentservice.userinfo_unsupported_response_type_description"},
{"JWSRequiresSigningAlg", inboundclient.ErrOAuthUserInfoJWSRequiresSigningAlg,
"error.agentservice.userinfo_jws_requires_signing_alg_description"},
{"JWERequiresEncryption", inboundclient.ErrOAuthUserInfoJWERequiresEncryption,
"error.agentservice.userinfo_jwe_requires_encryption_description"},
{"NestedJWTRequiresAll", inboundclient.ErrOAuthUserInfoNestedJWTRequiresAll,
Expand Down
18 changes: 4 additions & 14 deletions backend/internal/application/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1384,20 +1384,15 @@ func translateOAuthValidationError(err error) *tidcommon.ServiceError {
Key: "error.applicationservice.private_key_jwt_cannot_have_client_secret_description",
DefaultValue: "private_key_jwt authentication method cannot have a client secret",
})
case errors.Is(err, inboundclient.ErrOAuthClientSecretCannotHaveCertificate):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.applicationservice.client_secret_cannot_have_certificate_description",
DefaultValue: "client_secret authentication methods cannot have a certificate",
})
case errors.Is(err, inboundclient.ErrOAuthNoneAuthRequiresPublicClient):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.applicationservice.none_auth_method_requires_public_client_description",
DefaultValue: "'none' authentication method requires the client to be a public client",
})
case errors.Is(err, inboundclient.ErrOAuthNoneAuthCannotHaveCertOrSecret):
case errors.Is(err, inboundclient.ErrOAuthNoneAuthCannotHaveSecret):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.applicationservice.none_auth_method_cannot_have_cert_or_secret_description",
DefaultValue: "'none' authentication method cannot have a certificate or client secret",
Key: "error.applicationservice.none_auth_method_cannot_have_secret_description",
DefaultValue: "'none' authentication method cannot have a client secret",
})
case errors.Is(err, inboundclient.ErrOAuthClientCredentialsCannotUseNoneAuth):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Expand Down Expand Up @@ -1464,11 +1459,6 @@ func translateUserInfoValidationError(err error) *tidcommon.ServiceError {
Key: "error.applicationservice.userinfo_unsupported_response_type_description",
DefaultValue: "userinfo responseType is not supported",
})
case errors.Is(err, inboundclient.ErrOAuthUserInfoJWSRequiresSigningAlg):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.applicationservice.userinfo_jws_requires_signing_alg_description",
DefaultValue: "signingAlg is required when userinfo responseType is JWS",
})
case errors.Is(err, inboundclient.ErrOAuthUserInfoJWERequiresEncryption):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.applicationservice.userinfo_jwe_requires_encryption_description",
Expand All @@ -1477,7 +1467,7 @@ func translateUserInfoValidationError(err error) *tidcommon.ServiceError {
case errors.Is(err, inboundclient.ErrOAuthUserInfoNestedJWTRequiresAll):
return tidcommon.CustomServiceError(ErrorInvalidOAuthConfiguration, tidcommon.I18nMessage{
Key: "error.applicationservice.userinfo_nested_jwt_requires_all_description",
DefaultValue: "signingAlg, encryptionAlg, and encryptionEnc are required " +
DefaultValue: "encryptionAlg and encryptionEnc are required " +
"when userinfo responseType is NESTED_JWT",
})
case errors.Is(err, inboundclient.ErrOAuthUserInfoAlgRequiresResponseType):
Expand Down
17 changes: 3 additions & 14 deletions backend/internal/application/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3489,23 +3489,17 @@ func (suite *ServiceTestSuite) TestTranslateOAuthValidationError() {
wantCode: ErrorInvalidOAuthConfiguration.Code,
wantDescKey: "error.applicationservice.private_key_jwt_cannot_have_client_secret_description",
},
{
name: "ClientSecretCannotHaveCertificate",
err: inboundclient.ErrOAuthClientSecretCannotHaveCertificate,
wantCode: ErrorInvalidOAuthConfiguration.Code,
wantDescKey: "error.applicationservice.client_secret_cannot_have_certificate_description",
},
{
name: "NoneAuthRequiresPublicClient",
err: inboundclient.ErrOAuthNoneAuthRequiresPublicClient,
wantCode: ErrorInvalidOAuthConfiguration.Code,
wantDescKey: "error.applicationservice.none_auth_method_requires_public_client_description",
},
{
name: "NoneAuthCannotHaveCertOrSecret",
err: inboundclient.ErrOAuthNoneAuthCannotHaveCertOrSecret,
name: "NoneAuthCannotHaveSecret",
err: inboundclient.ErrOAuthNoneAuthCannotHaveSecret,
wantCode: ErrorInvalidOAuthConfiguration.Code,
wantDescKey: "error.applicationservice.none_auth_method_cannot_have_cert_or_secret_description",
wantDescKey: "error.applicationservice.none_auth_method_cannot_have_secret_description",
},
{
name: "ClientCredentialsCannotUseNoneAuth",
Expand Down Expand Up @@ -3585,11 +3579,6 @@ func (suite *ServiceTestSuite) TestTranslateUserInfoValidationError() {
err: inboundclient.ErrOAuthUserInfoUnsupportedResponseType,
wantDescKey: "error.applicationservice.userinfo_unsupported_response_type_description",
},
{
name: "JWSRequiresSigningAlg",
err: inboundclient.ErrOAuthUserInfoJWSRequiresSigningAlg,
wantDescKey: "error.applicationservice.userinfo_jws_requires_signing_alg_description",
},
{
name: "JWERequiresEncryption",
err: inboundclient.ErrOAuthUserInfoJWERequiresEncryption,
Expand Down
12 changes: 4 additions & 8 deletions backend/internal/inboundclient/error_constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,10 @@ var (
ErrOAuthCertificateRequiresClientID = errors.New("certificate requires an OAuth client ID")
// ErrOAuthPrivateKeyJWTCannotHaveClientSecret is returned when private_key_jwt is used with a client secret.
ErrOAuthPrivateKeyJWTCannotHaveClientSecret = errors.New("private_key_jwt cannot have a client secret")
// ErrOAuthClientSecretCannotHaveCertificate is returned when client-secret auth is used with a certificate.
ErrOAuthClientSecretCannotHaveCertificate = errors.New("client secret auth cannot have a certificate")
// ErrOAuthNoneAuthRequiresPublicClient is returned when none auth method is used without a public client.
ErrOAuthNoneAuthRequiresPublicClient = errors.New("none auth method requires a public client")
// ErrOAuthNoneAuthCannotHaveCertOrSecret is returned when none auth method is used with a certificate or secret.
ErrOAuthNoneAuthCannotHaveCertOrSecret = errors.New("none auth method cannot have certificate or secret")
// ErrOAuthNoneAuthCannotHaveSecret is returned when none auth method is used with a client secret.
ErrOAuthNoneAuthCannotHaveSecret = errors.New("none auth method cannot have a client secret")
// ErrOAuthClientCredentialsCannotUseNoneAuth is returned when client_credentials uses none auth method.
ErrOAuthClientCredentialsCannotUseNoneAuth = errors.New("client_credentials cannot use none auth method")
// ErrOAuthClientJWTBearerCannotUseNoneAuth is returned when the jwt-bearer grant uses none auth method.
Expand Down Expand Up @@ -148,14 +146,12 @@ var (
ErrOAuthUserInfoJWKSURINotSSRFSafe = errors.New("userinfo JWKS URI must be a publicly reachable HTTPS URL")
// ErrOAuthUserInfoUnsupportedResponseType is returned when an unsupported userinfo response type is specified.
ErrOAuthUserInfoUnsupportedResponseType = errors.New("unsupported userinfo response type")
// ErrOAuthUserInfoJWSRequiresSigningAlg is returned when responseType is JWS but signingAlg is not set.
ErrOAuthUserInfoJWSRequiresSigningAlg = errors.New("signingAlg is required when userinfo responseType is JWS")
// ErrOAuthUserInfoJWERequiresEncryption is returned when responseType is JWE but encryption fields are missing.
ErrOAuthUserInfoJWERequiresEncryption = errors.New(
"encryptionAlg and encryptionEnc are required when userinfo responseType is JWE")
// ErrOAuthUserInfoNestedJWTRequiresAll is returned when responseType is NESTED_JWT but fields are missing.
// ErrOAuthUserInfoNestedJWTRequiresAll is returned when responseType is NESTED_JWT but encryption is missing.
ErrOAuthUserInfoNestedJWTRequiresAll = errors.New(
"signingAlg, encryptionAlg, and encryptionEnc are required when userinfo responseType is NESTED_JWT")
"encryptionAlg and encryptionEnc are required when userinfo responseType is NESTED_JWT")
// ErrOAuthUserInfoAlgRequiresResponseType is returned when algorithm fields
// are set without an explicit responseType.
ErrOAuthUserInfoAlgRequiresResponseType = errors.New(
Expand Down
20 changes: 6 additions & 14 deletions backend/internal/inboundclient/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,15 +864,13 @@ func validateUserInfoConfig(p *providers.OAuthProfile) error {
if cfg.ResponseType != "" {
switch cfg.ResponseType {
case providers.UserInfoResponseTypeJWS:
if cfg.SigningAlg == "" {
return ErrOAuthUserInfoJWSRequiresSigningAlg
}
// Signing uses only the server's signing key as of now.
case providers.UserInfoResponseTypeJWE:
if cfg.EncryptionAlg == "" || cfg.EncryptionEnc == "" {
return ErrOAuthUserInfoJWERequiresEncryption
}
case providers.UserInfoResponseTypeNESTEDJWT:
if cfg.SigningAlg == "" || cfg.EncryptionAlg == "" || cfg.EncryptionEnc == "" {
if cfg.EncryptionAlg == "" || cfg.EncryptionEnc == "" {
return ErrOAuthUserInfoNestedJWTRequiresAll
}
case providers.UserInfoResponseTypeJSON:
Expand Down Expand Up @@ -1049,11 +1047,6 @@ func validateTokenEndpointAuthMethod(p *providers.OAuthProfile, hasClientSecret
return err
}
hasCert := p.Certificate != nil && p.Certificate.Type != ""
userInfoNeedsCert := p.UserInfo != nil && p.UserInfo.EncryptionAlg != ""
idTokenNeedsCert := p.Token != nil && p.Token.IDToken != nil &&
(p.Token.IDToken.ResponseType == providers.IDTokenResponseTypeJWE ||
p.Token.IDToken.ResponseType == providers.IDTokenResponseTypeNESTEDJWT)
needsCert := userInfoNeedsCert || idTokenNeedsCert

switch providers.TokenEndpointAuthMethod(p.TokenEndpointAuthMethod) {
case providers.TokenEndpointAuthMethodPrivateKeyJWT:
Expand All @@ -1064,15 +1057,14 @@ func validateTokenEndpointAuthMethod(p *providers.OAuthProfile, hasClientSecret
return ErrOAuthPrivateKeyJWTCannotHaveClientSecret
}
case providers.TokenEndpointAuthMethodClientSecretBasic, providers.TokenEndpointAuthMethodClientSecretPost:
if hasCert && !needsCert {
return ErrOAuthClientSecretCannotHaveCertificate
}
// A certificate is allowed: it carries the client's public key for token encryption
// (JWE / NESTED_JWT), independent of how the client authenticates.
case providers.TokenEndpointAuthMethodNone:
if !p.PublicClient {
return ErrOAuthNoneAuthRequiresPublicClient
}
if (hasCert && !needsCert) || hasClientSecret {
return ErrOAuthNoneAuthCannotHaveCertOrSecret
if hasClientSecret {
return ErrOAuthNoneAuthCannotHaveSecret
}
if slices.Contains(p.GrantTypes, string(providers.GrantTypeClientCredentials)) {
return ErrOAuthClientCredentialsCannotUseNoneAuth
Expand Down
39 changes: 31 additions & 8 deletions backend/internal/inboundclient/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,13 +629,14 @@ func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpoint_CertAllowe
assert.NoError(suite.T(), validateTokenEndpointAuthMethod(p, true))
}

func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpoint_CertRejectedWhenUserInfoDoesNotNeedIt() {
func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpoint_CertAllowedUnderClientSecret() {
// A certificate is allowed under client_secret auth even without encryption configured: it may
// be staged before enabling an encrypted token format, and an unused certificate is harmless.
p := &providers.OAuthProfile{
TokenEndpointAuthMethod: "client_secret_basic",
Certificate: &inboundmodel.Certificate{Type: cert.CertificateTypeJWKS, Value: "{}"},
}
err := validateTokenEndpointAuthMethod(p, true)
assert.ErrorIs(suite.T(), err, ErrOAuthClientSecretCannotHaveCertificate)
assert.NoError(suite.T(), validateTokenEndpointAuthMethod(p, true))
}

func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_PrivateKeyJWTHappy() {
Expand Down Expand Up @@ -667,14 +668,24 @@ func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_
assert.ErrorIs(suite.T(), err, ErrOAuthNoneAuthRequiresPublicClient)
}

func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_NoneRejectsCertOrSecret() {
func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_NoneAllowsCert() {
// A certificate is allowed under none auth (e.g. to encrypt tokens to a public client's key);
// only a client secret is rejected.
p := &providers.OAuthProfile{
TokenEndpointAuthMethod: "none",
PublicClient: true,
Certificate: &inboundmodel.Certificate{Type: cert.CertificateTypeJWKS, Value: "{}"},
}
err := validateTokenEndpointAuthMethod(p, false)
assert.ErrorIs(suite.T(), err, ErrOAuthNoneAuthCannotHaveCertOrSecret)
assert.NoError(suite.T(), validateTokenEndpointAuthMethod(p, false))
}

func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_NoneRejectsSecret() {
p := &providers.OAuthProfile{
TokenEndpointAuthMethod: "none",
PublicClient: true,
}
err := validateTokenEndpointAuthMethod(p, true)
assert.ErrorIs(suite.T(), err, ErrOAuthNoneAuthCannotHaveSecret)
}

func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_NoneClientCredentialsRejected() {
Expand Down Expand Up @@ -757,6 +768,18 @@ func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_NestedJWT
assert.NoError(suite.T(), validateUserInfoConfig(p))
}

func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_NestedJWTWithoutSigningAlg() {
p := &providers.OAuthProfile{
Certificate: &inboundmodel.Certificate{Type: cert.CertificateTypeJWKS, Value: "{}"},
UserInfo: &providers.UserInfoConfig{
ResponseType: providers.UserInfoResponseTypeNESTEDJWT,
EncryptionAlg: "RSA-OAEP-256",
EncryptionEnc: "A256GCM",
},
}
assert.NoError(suite.T(), validateUserInfoConfig(p))
}

// validateUserInfoConfig — error paths

func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_UnsupportedSigningAlg() {
Expand Down Expand Up @@ -811,11 +834,11 @@ func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_JWKSURISS
assert.ErrorIs(suite.T(), validateUserInfoConfig(p), ErrOAuthUserInfoJWKSURINotSSRFSafe)
}

func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_JWSMissingSigningAlg() {
func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_JWSWithoutSigningAlg() {
p := &providers.OAuthProfile{
UserInfo: &providers.UserInfoConfig{ResponseType: providers.UserInfoResponseTypeJWS},
}
assert.ErrorIs(suite.T(), validateUserInfoConfig(p), ErrOAuthUserInfoJWSRequiresSigningAlg)
assert.NoError(suite.T(), validateUserInfoConfig(p))
}

func (suite *InboundClientServiceTestSuite) TestValidateUserInfoConfig_JWEMissingEncryption() {
Expand Down
Loading
Loading