diff --git a/backend/.mockery.public.yml b/backend/.mockery.public.yml index 53694d0eef..e51ed8c61a 100644 --- a/backend/.mockery.public.yml +++ b/backend/.mockery.public.yml @@ -574,6 +574,36 @@ packages: structname: '{{.InterfaceName}}Mock' pkgname: consentprovidermock filename: "{{.InterfaceName}}_mock.go" + ResourceServerProvider: + config: + dir: tests/mocks/resourceserverprovidermock + structname: '{{.InterfaceName}}Mock' + pkgname: resourceserverprovidermock + filename: "{{.InterfaceName}}_mock.go" + OrganizationUnitProvider: + config: + dir: tests/mocks/ouprovidermock + structname: '{{.InterfaceName}}Mock' + pkgname: ouprovidermock + filename: "{{.InterfaceName}}_mock.go" + DesignProvider: + config: + dir: tests/mocks/designprovidermock + structname: '{{.InterfaceName}}Mock' + pkgname: designprovidermock + filename: "{{.InterfaceName}}_mock.go" + I18nProvider: + config: + dir: tests/mocks/i18nprovidermock + structname: '{{.InterfaceName}}Mock' + pkgname: i18nprovidermock + filename: "{{.InterfaceName}}_mock.go" + IDPProvider: + config: + dir: tests/mocks/idpprovidermock + structname: '{{.InterfaceName}}Mock' + pkgname: idpprovidermock + filename: "{{.InterfaceName}}_mock.go" AuthorizationProvider: config: dir: tests/mocks/authzmock @@ -604,12 +634,6 @@ packages: structname: '{{.InterfaceName}}Mock' pkgname: attestationprovidermock filename: "{{.InterfaceName}}_mock.go" - IDPProvider: - config: - dir: tests/mocks/idpprovidermock - structname: '{{.InterfaceName}}Mock' - pkgname: idpprovidermock - filename: "{{.InterfaceName}}_mock.go" RuntimeStoreProvider: config: dir: tests/mocks/runtimestoreprovidermock diff --git a/backend/cmd/server/config/default.json b/backend/cmd/server/config/default.json index b319a25345..fffd3e787f 100644 --- a/backend/cmd/server/config/default.json +++ b/backend/cmd/server/config/default.json @@ -133,6 +133,7 @@ "validity_period": 600 }, "dcr": { + "enabled" : true, "insecure": false }, "par": { @@ -146,7 +147,16 @@ "allowed_algs": ["ES256", "PS256", "ES384", "ES512", "EdDSA", "RS256"], "max_jti_length": 256 }, - "allow_wildcard_redirect_uri": false + "allow_wildcard_redirect_uri": false, + "allowed_auth_methods" :["client_secret_basic", "client_secret_post", "private_key_jwt", "none"], + "allowed_response_types" : ["code"], + "allowed_grant_types" : ["client_credentials", "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange", "urn:openid:params:grant-type:ciba", "urn:ietf:params:oauth:grant-type:jwt-bearer"], + "token_revocation" : { + "enabled" : true + }, + "logout" : { + "enabled" : true + } }, "flow": { "default_auth_flow_handle": "default-flow", diff --git a/backend/cmd/server/servicemanager.go b/backend/cmd/server/servicemanager.go index b8f248b93d..82b86b347d 100644 --- a/backend/cmd/server/servicemanager.go +++ b/backend/cmd/server/servicemanager.go @@ -454,9 +454,11 @@ func registerServices(mux *http.ServeMux, cacheManager cache.CacheManagerInterfa runtimeStoreProvider, transactioner, oauthCfg) fatalOnError(ctx, logger, err, "Failed to initialize OAuth services") - // Register OAuth2 DCR service. - err = dcr.Initialize(mux, applicationService, ouService, i18nService, oauthCfg) - fatalOnError(ctx, logger, err, "Failed to initialize OAuth2 DCR service") + if oauthCfg.OAuth.DCR.IsEnabled() { + // Register OAuth2 DCR service. + err = dcr.Initialize(mux, applicationService, ouService, i18nService, oauthCfg) + fatalOnError(ctx, logger, err, "Failed to initialize OAuth2 DCR service") + } // Register the health service. healthSvc := healthcheckservice.Initialize(dbprovider.GetDBProvider(), dbprovider.GetRedisProvider()) diff --git a/backend/internal/application/service_test.go b/backend/internal/application/service_test.go index 9f3e2e822d..617c10cb7c 100644 --- a/backend/internal/application/service_test.go +++ b/backend/internal/application/service_test.go @@ -1132,6 +1132,10 @@ func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_NilOAut } func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_WithDefaults() { + config.ResetServerRuntime() + require.NoError(suite.T(), config.InitializeServerRuntime("/tmp/test", &config.Config{})) + defer config.ResetServerRuntime() + app := &model.ApplicationDTO{ Name: "Test App", OUID: testOUID, @@ -1162,6 +1166,10 @@ func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_WithDef } func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_WithResponseTypeDefault() { + config.ResetServerRuntime() + require.NoError(suite.T(), config.InitializeServerRuntime("/tmp/test", &config.Config{})) + defer config.ResetServerRuntime() + app := &model.ApplicationDTO{ Name: "Test App", OUID: testOUID, @@ -1187,6 +1195,10 @@ func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_WithRes } func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_WithGrantTypeButNoResponseType() { + config.ResetServerRuntime() + require.NoError(suite.T(), config.InitializeServerRuntime("/tmp/test", &config.Config{})) + defer config.ResetServerRuntime() + app := &model.ApplicationDTO{ Name: "Test App", OUID: testOUID, @@ -1276,6 +1288,10 @@ func (suite *ServiceTestSuite) TestEnrichApplicationWithCertificate_Success() { } func (suite *ServiceTestSuite) TestValidateOAuthParamsForCreateAndUpdate_PublicClientSuccess() { + config.ResetServerRuntime() + require.NoError(suite.T(), config.InitializeServerRuntime("/tmp/test", &config.Config{})) + defer config.ResetServerRuntime() + app := &model.ApplicationDTO{ Name: "Test App", OUID: testOUID, @@ -3323,14 +3339,16 @@ func (suite *ServiceTestSuite) TestTranslateOAuthValidationError() { wantDescKey: "error.applicationservice.auth_code_requires_redirect_uris_description", }, { - name: "InvalidGrantType", - err: inboundclient.ErrOAuthInvalidGrantType, - wantCode: ErrorInvalidGrantType.Code, + name: "InvalidGrantType", + err: inboundclient.ErrOAuthInvalidGrantType, + wantCode: ErrorInvalidGrantType.Code, + wantDescKey: "error.applicationservice.invalid_grant_type_description", }, { - name: "InvalidResponseType", - err: inboundclient.ErrOAuthInvalidResponseType, - wantCode: ErrorInvalidResponseType.Code, + name: "InvalidResponseType", + err: inboundclient.ErrOAuthInvalidResponseType, + wantCode: ErrorInvalidResponseType.Code, + wantDescKey: "error.applicationservice.invalid_response_type_description", }, { name: "ClientCredentialsCannotUseResponseTypes", @@ -3363,9 +3381,10 @@ func (suite *ServiceTestSuite) TestTranslateOAuthValidationError() { wantDescKey: "error.applicationservice.response_types_require_authorization_code_description", }, { - name: "InvalidTokenEndpointAuthMethod", - err: inboundclient.ErrOAuthInvalidTokenEndpointAuthMethod, - wantCode: ErrorInvalidTokenEndpointAuthMethod.Code, + name: "InvalidTokenEndpointAuthMethod", + err: inboundclient.ErrOAuthInvalidTokenEndpointAuthMethod, + wantCode: ErrorInvalidTokenEndpointAuthMethod.Code, + wantDescKey: "error.applicationservice.invalid_token_endpoint_auth_method_description", }, { name: "PrivateKeyJWTRequiresCertificate", diff --git a/backend/internal/application/tools.go b/backend/internal/application/tools.go index adf8d27496..01f6592ec2 100644 --- a/backend/internal/application/tools.go +++ b/backend/internal/application/tools.go @@ -28,6 +28,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/thunder-id/thunderid/internal/application/model" + oauthconfig "github.com/thunder-id/thunderid/internal/oauth/config" oauth2const "github.com/thunder-id/thunderid/internal/oauth/oauth2/constants" "github.com/thunder-id/thunderid/internal/system/mcp/tool" ) @@ -340,11 +341,13 @@ func (t *applicationTools) getApplicationTemplates( // getCommonSchemaModifiers returns the common schema modifiers for ApplicationDTO. func getCommonSchemaModifiers() []func(*jsonschema.Schema) { + oauthCfg := oauthconfig.FromServerRuntime() return []func(*jsonschema.Schema){ - tool.WithEnum("inbound_auth_config.config", "grant_types", oauth2const.GetSupportedGrantTypes()), - tool.WithEnum("inbound_auth_config.config", "response_types", oauth2const.GetSupportedResponseTypes()), + tool.WithEnum("inbound_auth_config.config", "grant_types", oauth2const.GetSupportedGrantTypes(oauthCfg)), + tool.WithEnum("inbound_auth_config.config", "response_types", + oauth2const.GetSupportedResponseTypes(oauthCfg)), tool.WithEnum("inbound_auth_config.config", "token_endpoint_auth_method", - oauth2const.GetSupportedTokenEndpointAuthMethods()), + oauth2const.GetSupportedTokenEndpointAuthMethods(oauthCfg)), tool.WithEnum("inbound_auth_config", "type", []string{string(providers.OAuthInboundAuthType)}), } } diff --git a/backend/internal/application/tools_test.go b/backend/internal/application/tools_test.go index d981515df3..ac3b928fc1 100644 --- a/backend/internal/application/tools_test.go +++ b/backend/internal/application/tools_test.go @@ -28,8 +28,10 @@ import ( "github.com/stretchr/testify/suite" "github.com/thunder-id/thunderid/internal/application/model" + "github.com/thunder-id/thunderid/internal/system/config" "github.com/thunder-id/thunderid/internal/system/mcp/tool" tidcommon "github.com/thunder-id/thunderid/pkg/thunderidengine/common" + engineconfig "github.com/thunder-id/thunderid/pkg/thunderidengine/config" "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" ) @@ -41,6 +43,31 @@ func TestApplicationToolsTestSuite(t *testing.T) { suite.Run(t, new(ApplicationToolsTestSuite)) } +func (suite *ApplicationToolsTestSuite) SetupTest() { + config.ResetServerRuntime() + cfg := &config.Config{ + Server: engineconfig.ServerConfig{ + Identifier: "test-dep", + Hostname: "thunderid.io", + Port: 443, + PublicURL: "https://thunderid.io", + }, + Database: config.DatabaseConfig{ + RuntimeTransient: config.DataSource{Type: "sqlite"}, + }, + JWT: engineconfig.JWTConfig{ + Issuer: "https://thunderid.io", + ValidityPeriod: 3600, + }, + } + err := config.InitializeServerRuntime("/tmp/test-application-tools", cfg) + suite.Require().NoError(err) +} + +func (suite *ApplicationToolsTestSuite) TearDownTest() { + config.ResetServerRuntime() +} + func (suite *ApplicationToolsTestSuite) TestNewApplicationTools() { mockService := NewApplicationServiceInterfaceMock(suite.T()) tools := &applicationTools{appService: mockService} diff --git a/backend/internal/inboundclient/service.go b/backend/internal/inboundclient/service.go index 739849eca3..54dedd52c0 100644 --- a/backend/internal/inboundclient/service.go +++ b/backend/internal/inboundclient/service.go @@ -985,15 +985,13 @@ func containsInvalidWildcardSegment(p string) bool { // validateGrantAndResponseTypes validates grant types, response types, and their combinations. func validateGrantAndResponseTypes(p *providers.OAuthProfile) error { - for _, grantType := range p.GrantTypes { - if !providers.GrantType(grantType).IsValid() { - return ErrOAuthInvalidGrantType - } + err := validateWithAllowedGrantTypes(p.GrantTypes) + if err != nil { + return err } - for _, responseType := range p.ResponseTypes { - if !providers.ResponseType(responseType).IsValid() { - return ErrOAuthInvalidResponseType - } + err = validateWithAllowedResponseTypes(p.ResponseTypes) + if err != nil { + return err } if len(p.GrantTypes) == 1 && slices.Contains(p.GrantTypes, string(providers.GrantTypeClientCredentials)) && @@ -1023,9 +1021,9 @@ func validateGrantAndResponseTypes(p *providers.OAuthProfile) error { // validateTokenEndpointAuthMethod validates the token endpoint auth method against cert and secret state. func validateTokenEndpointAuthMethod(p *providers.OAuthProfile, hasClientSecret bool) error { - method := providers.TokenEndpointAuthMethod(p.TokenEndpointAuthMethod) - if !method.IsValid() { - return ErrOAuthInvalidTokenEndpointAuthMethod + err := validateWithAllowedTokenEndpointAuthMethod(p.TokenEndpointAuthMethod) + if err != nil { + return err } hasCert := p.Certificate != nil && p.Certificate.Type != "" userInfoNeedsCert := p.UserInfo != nil && p.UserInfo.EncryptionAlg != "" @@ -1034,7 +1032,7 @@ func validateTokenEndpointAuthMethod(p *providers.OAuthProfile, hasClientSecret p.Token.IDToken.ResponseType == providers.IDTokenResponseTypeNESTEDJWT) needsCert := userInfoNeedsCert || idTokenNeedsCert - switch method { + switch providers.TokenEndpointAuthMethod(p.TokenEndpointAuthMethod) { case providers.TokenEndpointAuthMethodPrivateKeyJWT: if !hasCert { return ErrOAuthPrivateKeyJWTRequiresCertificate @@ -1069,6 +1067,49 @@ func validateTokenEndpointAuthMethod(p *providers.OAuthProfile, hasClientSecret return nil } +// validateAllowedGrantTypes rejects grant types not permitted by the deployment's configured +// oauth.allowed_grant_types allow-list. An empty allow-list permits all grant types. +func validateWithAllowedGrantTypes(grantTypes []string) error { + allowed := config.GetServerRuntime().Config.OAuth.AllowedGrantTypes + for _, grantType := range grantTypes { + if !providers.GrantType(grantType).IsValid() { + return ErrOAuthInvalidGrantType + } + if len(allowed) > 0 && !slices.Contains(allowed, grantType) { + return ErrOAuthInvalidGrantType + } + } + return nil +} + +// validateAllowedResponseTypes rejects response types not permitted by the deployment's configured +// oauth.allowed_response_types allow-list. An empty allow-list permits all response types. +func validateWithAllowedResponseTypes(responseTypes []string) error { + allowed := config.GetServerRuntime().Config.OAuth.AllowedResponseTypes + for _, responseType := range responseTypes { + if !providers.ResponseType(responseType).IsValid() { + return ErrOAuthInvalidResponseType + } + if len(allowed) > 0 && !slices.Contains(allowed, responseType) { + return ErrOAuthInvalidResponseType + } + } + return nil +} + +// validateAllowedTokenEndpointAuthMethod rejects a token endpoint auth method not permitted by the +// deployment's configured oauth.allowed_auth_methods allow-list. An empty allow-list permits all methods. +func validateWithAllowedTokenEndpointAuthMethod(method string) error { + if !providers.TokenEndpointAuthMethod(method).IsValid() { + return ErrOAuthInvalidTokenEndpointAuthMethod + } + allowed := config.GetServerRuntime().Config.OAuth.AllowedAuthMethods + if len(allowed) == 0 || slices.Contains(allowed, method) { + return nil + } + return ErrOAuthInvalidTokenEndpointAuthMethod +} + // validatePublicClient validates constraints required for public clients. func validatePublicClient(p *providers.OAuthProfile) error { if providers.TokenEndpointAuthMethod(p.TokenEndpointAuthMethod) != providers.TokenEndpointAuthMethodNone { diff --git a/backend/internal/inboundclient/service_test.go b/backend/internal/inboundclient/service_test.go index 7bce7cc854..c388f2e3b8 100644 --- a/backend/internal/inboundclient/service_test.go +++ b/backend/internal/inboundclient/service_test.go @@ -1808,6 +1808,113 @@ func (suite *InboundClientServiceTestSuite) TestValidateGrantAndResponseTypes_Ha assert.NoError(suite.T(), validateGrantAndResponseTypes(p)) } +// ----- allow-list enforcement (validateWithAllowed*) ----- + +func (suite *InboundClientServiceTestSuite) configureAllowedGrantTypes(allowed []string) { + sysconfig.ResetServerRuntime() + cfg := &sysconfig.Config{} + cfg.OAuth.AllowedGrantTypes = allowed + suite.Require().NoError(sysconfig.InitializeServerRuntime("/tmp/test", cfg)) +} + +func (suite *InboundClientServiceTestSuite) configureAllowedResponseTypes(allowed []string) { + sysconfig.ResetServerRuntime() + cfg := &sysconfig.Config{} + cfg.OAuth.AllowedResponseTypes = allowed + suite.Require().NoError(sysconfig.InitializeServerRuntime("/tmp/test", cfg)) +} + +func (suite *InboundClientServiceTestSuite) configureAllowedAuthMethods(allowed []string) { + sysconfig.ResetServerRuntime() + cfg := &sysconfig.Config{} + cfg.OAuth.AllowedAuthMethods = allowed + suite.Require().NoError(sysconfig.InitializeServerRuntime("/tmp/test", cfg)) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedGrantTypes_EmptyAllowList_PermitsAny() { + err := validateWithAllowedGrantTypes([]string{"authorization_code", "client_credentials"}) + assert.NoError(suite.T(), err) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedGrantTypes_NotInAllowList_Rejected() { + suite.configureAllowedGrantTypes([]string{"client_credentials"}) + err := validateWithAllowedGrantTypes([]string{"authorization_code"}) + assert.ErrorIs(suite.T(), err, ErrOAuthInvalidGrantType) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedGrantTypes_InAllowList_Allowed() { + suite.configureAllowedGrantTypes([]string{"authorization_code", "refresh_token"}) + err := validateWithAllowedGrantTypes([]string{"authorization_code"}) + assert.NoError(suite.T(), err) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedGrantTypes_InvalidGrantTypeStillRejected() { + // An allow-list entry does not bypass the underlying IsValid() check. + suite.configureAllowedGrantTypes([]string{"bogus_grant"}) + err := validateWithAllowedGrantTypes([]string{"bogus_grant"}) + assert.ErrorIs(suite.T(), err, ErrOAuthInvalidGrantType) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedResponseTypes_EmptyAllowList_PermitsAny() { + err := validateWithAllowedResponseTypes([]string{"code"}) + assert.NoError(suite.T(), err) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedResponseTypes_NotInAllowList_Rejected() { + suite.configureAllowedResponseTypes([]string{"code"}) + err := validateWithAllowedResponseTypes([]string{"id_token"}) + assert.ErrorIs(suite.T(), err, ErrOAuthInvalidResponseType) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedResponseTypes_InAllowList_Allowed() { + suite.configureAllowedResponseTypes([]string{"code"}) + err := validateWithAllowedResponseTypes([]string{"code"}) + assert.NoError(suite.T(), err) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedTokenEndpointAuthMethod_EmptyAllowList_PermitsAny() { + err := validateWithAllowedTokenEndpointAuthMethod("client_secret_basic") + assert.NoError(suite.T(), err) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedTokenEndpointAuthMethod_NotInAllowList_Rejected() { + suite.configureAllowedAuthMethods([]string{"client_secret_basic"}) + err := validateWithAllowedTokenEndpointAuthMethod("none") + assert.ErrorIs(suite.T(), err, ErrOAuthInvalidTokenEndpointAuthMethod) +} + +func (suite *InboundClientServiceTestSuite) TestValidateWithAllowedTokenEndpointAuthMethod_InAllowList_Allowed() { + suite.configureAllowedAuthMethods([]string{"client_secret_basic"}) + err := validateWithAllowedTokenEndpointAuthMethod("client_secret_basic") + assert.NoError(suite.T(), err) +} + +// ----- allow-list enforcement wired into the higher-level validators ----- + +func (suite *InboundClientServiceTestSuite) TestValidateGrantAndResponseTypes_GrantTypeNotAllowed() { + suite.configureAllowedGrantTypes([]string{"client_credentials"}) + p := &providers.OAuthProfile{ + GrantTypes: []string{"authorization_code"}, + } + assert.ErrorIs(suite.T(), validateGrantAndResponseTypes(p), ErrOAuthInvalidGrantType) +} + +func (suite *InboundClientServiceTestSuite) TestValidateGrantAndResponseTypes_ResponseTypeNotAllowed() { + suite.configureAllowedResponseTypes([]string{"code"}) + p := &providers.OAuthProfile{ + GrantTypes: []string{"authorization_code"}, + ResponseTypes: []string{"id_token"}, + } + assert.ErrorIs(suite.T(), validateGrantAndResponseTypes(p), ErrOAuthInvalidResponseType) +} + +func (suite *InboundClientServiceTestSuite) TestValidateTokenEndpointAuthMethod_NotAllowed() { + suite.configureAllowedAuthMethods([]string{"client_secret_basic"}) + p := &providers.OAuthProfile{TokenEndpointAuthMethod: "private_key_jwt"} + err := validateTokenEndpointAuthMethod(p, false) + assert.ErrorIs(suite.T(), err, ErrOAuthInvalidTokenEndpointAuthMethod) +} + // ----- validatePublicClient branch coverage ----- func (suite *InboundClientServiceTestSuite) TestValidatePublicClient_NonNoneAuthMethod() { diff --git a/backend/internal/oauth/init.go b/backend/internal/oauth/init.go index 8002317b38..eb72d7cfa1 100644 --- a/backend/internal/oauth/init.go +++ b/backend/internal/oauth/init.go @@ -21,6 +21,7 @@ package oauth import ( "net/http" + "slices" "github.com/thunder-id/thunderid/internal/attributecache" "github.com/thunder-id/thunderid/internal/flow/flowexec" @@ -79,25 +80,38 @@ func Initialize( resolver := jwksresolver.Initialize(httpClient) scopeValidator := scope.Initialize() discoveryService := discovery.Initialize(mux, runtimeCrypto, cfg) - // The enforcement service (revocation read path) is built before the token service so it can be - // injected into the validator, which enforces the deny list as the final step of every validation. - enforcementService, refreshTokenRevoker := revocation.Initialize( - mux, jwtService, actorProvider, authnProvider, discoveryService, observabilitySvc) + + var enforcementService revocation.EnforcementServiceInterface + var refreshTokenRevoker revocation.RefreshTokenRevokerInterface + if cfg.OAuth.TokenRevocation.Enabled { + // The enforcement service (revocation read path) is built before the token service so it can be + // injected into the validator, which enforces the deny list as the final step of every validation. + enforcementService, refreshTokenRevoker = revocation.Initialize( + mux, jwtService, actorProvider, authnProvider, discoveryService, observabilitySvc) + } + tokenBuilder, tokenValidator := tokenservice.Initialize( cfg, jwtService, jweService, resolver, idpService, enforcementService) parService := par.Initialize(mux, actorProvider, authnProvider, jwtService, discoveryService, resourceService, dpopVerifier, cfg, runtimeStore) - cibaService := ciba.Initialize(mux, jwtService, actorProvider, authnProvider, flowExecService, - discoveryService, resourceService, serverConfigService, cfg) oauth2AuthzService, err := oauth2authz.Initialize(mux, actorProvider, resourceService, jwtService, flowExecService, parService, cfg, runtimeStore, transactioner) if err != nil { return err } + + var cibaService ciba.CIBAServiceInterface + if len(cfg.OAuth.AllowedGrantTypes) == 0 || + slices.Contains(cfg.OAuth.AllowedGrantTypes, string(providers.GrantTypeCIBA)) { + cibaService = ciba.Initialize(mux, jwtService, actorProvider, authnProvider, flowExecService, + discoveryService, resourceService, serverConfigService, cfg) + } + grantHandlerProvider := granthandlers.Initialize( jwtService, oauth2AuthzService, tokenBuilder, tokenValidator, attributeCacheSvc, ouService, authzService, actorProvider, resourceService, serverConfigService, cibaService, refreshTokenRevoker, cfg) + token.Initialize(mux, jwtService, actorProvider, authnProvider, grantHandlerProvider, scopeValidator, observabilitySvc, discoveryService, dpopVerifier, cfg) introspect.Initialize(mux, jwtService, actorProvider, authnProvider, discoveryService, tokenValidator) @@ -105,6 +119,9 @@ func Initialize( tokenValidator, actorProvider, attributeCacheSvc, discoveryService, dpopVerifier, cfg) callback.Initialize(mux, oauth2AuthzService, cibaService, cfg) - oauth2logout.Initialize(mux, jwtService, actorProvider, flowExecService, runtimeStore, cfg) + + if cfg.OAuth.Logout.Enabled { + oauth2logout.Initialize(mux, jwtService, actorProvider, flowExecService, runtimeStore, cfg) + } return nil } diff --git a/backend/internal/oauth/oauth2/callback/callback.go b/backend/internal/oauth/oauth2/callback/callback.go index 870e6ccb4b..7850eed824 100644 --- a/backend/internal/oauth/oauth2/callback/callback.go +++ b/backend/internal/oauth/oauth2/callback/callback.go @@ -123,6 +123,11 @@ func (d *callbackDispatcher) handleFlowCallback(w http.ResponseWriter, r *http.R utils.WriteSuccessResponse(ctx, w, http.StatusOK, oauth2authz.AuthZPostResponse{RedirectURI: redirectURI}) case string(providers.GrantTypeCIBA): + if d.cibaService == nil { + utils.WriteJSONError(ctx, w, oauth2const.ErrorInvalidRequest, + "Unsupported callback type", http.StatusBadRequest, nil) + return + } cibaErr := d.cibaService.HandleCallback(ctx, req.AuthID, req.Assertion) if cibaErr != nil { statusCode := http.StatusBadRequest diff --git a/backend/internal/oauth/oauth2/callback/callback_test.go b/backend/internal/oauth/oauth2/callback/callback_test.go index ffa42ae868..8c44772caf 100644 --- a/backend/internal/oauth/oauth2/callback/callback_test.go +++ b/backend/internal/oauth/oauth2/callback/callback_test.go @@ -253,6 +253,21 @@ func (suite *CallbackDispatcherTestSuite) TestHandleFlowCallback_CIBA_Error() { suite.Equal(oauth2const.ErrorAccessDenied, body["error"]) } +func (suite *CallbackDispatcherTestSuite) TestHandleFlowCallback_CIBA_NilCIBAService_ReturnsBadRequest() { + // When the CIBA grant type is not in allowed_grant_types, cibaService is nil. A CIBA + // callback must be rejected gracefully instead of panicking on the nil service. + suite.dispatcher = newCallbackDispatcher(testhelpers.OAuthConfig(), suite.mockAuthZ, nil) + + w := suite.postCallback( + `{"authId":"auth-req-1","assertion":"ciba-assertion","type":"urn:openid:params:grant-type:ciba"}`) + + suite.Equal(http.StatusBadRequest, w.Code) + var body map[string]string + suite.NoError(json.NewDecoder(w.Body).Decode(&body)) + suite.Equal(oauth2const.ErrorInvalidRequest, body["error"]) + suite.Contains(body["error_description"], "Unsupported callback type") +} + // --- handleFlowCallback: unsupported type --- func (suite *CallbackDispatcherTestSuite) TestHandleFlowCallback_UnsupportedType_ReturnsBadRequest() { diff --git a/backend/internal/oauth/oauth2/constants/constants.go b/backend/internal/oauth/oauth2/constants/constants.go index 17ef8c6582..2cc2e25743 100644 --- a/backend/internal/oauth/oauth2/constants/constants.go +++ b/backend/internal/oauth/oauth2/constants/constants.go @@ -22,6 +22,7 @@ package constants import ( "errors" + oauthconfig "github.com/thunder-id/thunderid/internal/oauth/config" "github.com/thunder-id/thunderid/internal/oauth/oauth2/model" "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" ) @@ -325,7 +326,11 @@ const ( ) // GetSupportedResponseTypes returns all supported OAuth2 response types. -func GetSupportedResponseTypes() []string { +func GetSupportedResponseTypes(oauthConfig oauthconfig.Config) []string { + allowedResponseTypes := oauthConfig.OAuth.AllowedResponseTypes + if len(allowedResponseTypes) > 0 { + return allowedResponseTypes + } result := make([]string, len(providers.SupportedResponseTypes)) for i, rt := range providers.SupportedResponseTypes { result[i] = string(rt) @@ -334,7 +339,11 @@ func GetSupportedResponseTypes() []string { } // GetSupportedGrantTypes returns all supported OAuth2 grant types. -func GetSupportedGrantTypes() []string { +func GetSupportedGrantTypes(oauthConfig oauthconfig.Config) []string { + allowedGrantTypes := oauthConfig.OAuth.AllowedGrantTypes + if len(allowedGrantTypes) > 0 { + return allowedGrantTypes + } result := make([]string, len(providers.SupportedGrantTypes)) for i, gt := range providers.SupportedGrantTypes { result[i] = string(gt) @@ -343,7 +352,11 @@ func GetSupportedGrantTypes() []string { } // GetSupportedTokenEndpointAuthMethods returns all supported token endpoint authentication methods. -func GetSupportedTokenEndpointAuthMethods() []string { +func GetSupportedTokenEndpointAuthMethods(oauthConfig oauthconfig.Config) []string { + allowedAuthMethods := oauthConfig.OAuth.AllowedAuthMethods + if len(allowedAuthMethods) > 0 { + return allowedAuthMethods + } result := make([]string, len(providers.SupportedTokenEndpointAuthMethods)) for i, tam := range providers.SupportedTokenEndpointAuthMethods { result[i] = string(tam) diff --git a/backend/internal/oauth/oauth2/discovery/discovery_test.go b/backend/internal/oauth/oauth2/discovery/discovery_test.go index 21f911bd85..a2886730be 100644 --- a/backend/internal/oauth/oauth2/discovery/discovery_test.go +++ b/backend/internal/oauth/oauth2/discovery/discovery_test.go @@ -87,6 +87,9 @@ func (suite *DiscoveryTestSuite) SetupTest() { "urn:thunder:acr:generated-code": {"OTP"}, }, }, + DCR: engineconfig.DCRConfig{Enabled: boolPtr(true)}, + TokenRevocation: engineconfig.OAuthTokenRevocationConfig{Enabled: true}, + Logout: engineconfig.LogoutConfig{Enabled: true}, }, } _ = config.InitializeServerRuntime("test", testConfig) @@ -172,6 +175,7 @@ func (suite *DiscoveryTestSuite) TestOIDCDiscovery() { assert.NotEmpty(suite.T(), metadata.UserInfoEndpoint) assert.NotEmpty(suite.T(), metadata.ScopesSupported) assert.Contains(suite.T(), metadata.ScopesSupported, "openid") + assert.NotEmpty(suite.T(), metadata.EndSessionEndpoint) // Verify OIDC-specific fields assert.Contains(suite.T(), metadata.SubjectTypesSupported, constants.SubjectTypePublic) @@ -221,6 +225,32 @@ func (suite *DiscoveryTestSuite) TestDPoPSigningAlgValuesOmittedWhenUnconfigured assert.NotContains(suite.T(), string(body), "dpop_signing_alg_values_supported") } +func (suite *DiscoveryTestSuite) TestDCRRevocationLogoutEndpointsOmittedWhenDisabled() { + config.ResetServerRuntime() + testConfig := &config.Config{ + Server: engineconfig.ServerConfig{Hostname: "localhost", Port: 8080}, + JWT: engineconfig.JWTConfig{Issuer: "https://auth.example.com"}, + } + _ = config.InitializeServerRuntime("test", testConfig) + defer config.ResetServerRuntime() + + svc := newDiscoveryService(suite.cryptoMock, oauthCfgFromServerConfig(testConfig)) + oauth2Meta := svc.GetOAuth2AuthorizationServerMetadata(context.Background()) + assert.Empty(suite.T(), oauth2Meta.RegistrationEndpoint) + assert.Empty(suite.T(), oauth2Meta.RevocationEndpoint) + + suite.cryptoMock.EXPECT().GetPublicKeys(mock.Anything, kmprovider.PublicKeyFilter{}). + Return([]kmprovider.PublicKeyInfo{{KeyID: "k1", Algorithm: cryptolib.AlgorithmRS256}}, nil) + oidcMeta, err := svc.GetOIDCMetadata(context.Background()) + assert.NoError(suite.T(), err) + assert.Empty(suite.T(), oidcMeta.EndSessionEndpoint) + + body, err := json.Marshal(oauth2Meta) + assert.NoError(suite.T(), err) + assert.NotContains(suite.T(), string(body), "registration_endpoint") + assert.NotContains(suite.T(), string(body), "revocation_endpoint") +} + // TestGrantTypeIsValid tests the GrantType.IsValid() method // This is a standalone test for constants - doesn't require discovery service setup func TestGrantTypeIsValid(t *testing.T) { @@ -267,7 +297,7 @@ func TestTokenEndpointAuthMethodIsValid(t *testing.T) { // TestGetSupportedResponseTypes tests the GetSupportedResponseTypes function // This is a standalone test for constants - doesn't require discovery service setup func TestGetSupportedResponseTypes(t *testing.T) { - supported := constants.GetSupportedResponseTypes() + supported := constants.GetSupportedResponseTypes(oauthconfig.Config{}) assert.NotNil(t, supported) assert.Equal(t, 1, len(supported)) @@ -275,10 +305,17 @@ func TestGetSupportedResponseTypes(t *testing.T) { assert.Equal(t, []string{"code"}, supported) } +func TestGetSupportedResponseTypes_ConfiguredAllowList(t *testing.T) { + cfg := oauthconfig.Config{ + OAuth: engineconfig.OAuthConfig{AllowedResponseTypes: []string{"code"}}, + } + assert.Equal(t, []string{"code"}, constants.GetSupportedResponseTypes(cfg)) +} + // TestGetSupportedGrantTypes tests the GetSupportedGrantTypes function // This is a standalone test for constants - doesn't require discovery service setup func TestGetSupportedGrantTypes(t *testing.T) { - supported := constants.GetSupportedGrantTypes() + supported := constants.GetSupportedGrantTypes(oauthconfig.Config{}) assert.NotNil(t, supported) assert.Equal(t, 6, len(supported)) @@ -292,10 +329,17 @@ func TestGetSupportedGrantTypes(t *testing.T) { assert.NotContains(t, supported, "implicit") } +func TestGetSupportedGrantTypes_ConfiguredAllowList(t *testing.T) { + cfg := oauthconfig.Config{ + OAuth: engineconfig.OAuthConfig{AllowedGrantTypes: []string{"client_credentials", "refresh_token"}}, + } + assert.Equal(t, []string{"client_credentials", "refresh_token"}, constants.GetSupportedGrantTypes(cfg)) +} + // TestGetSupportedTokenEndpointAuthMethods tests the GetSupportedTokenEndpointAuthMethods function // This is a standalone test for constants - doesn't require discovery service setup func TestGetSupportedTokenEndpointAuthMethods(t *testing.T) { - supported := constants.GetSupportedTokenEndpointAuthMethods() + supported := constants.GetSupportedTokenEndpointAuthMethods(oauthconfig.Config{}) assert.NotNil(t, supported) assert.Equal(t, 4, len(supported)) @@ -306,6 +350,13 @@ func TestGetSupportedTokenEndpointAuthMethods(t *testing.T) { assert.NotContains(t, supported, "client_secret_jwt") } +func TestGetSupportedTokenEndpointAuthMethods_ConfiguredAllowList(t *testing.T) { + cfg := oauthconfig.Config{ + OAuth: engineconfig.OAuthConfig{AllowedAuthMethods: []string{"client_secret_basic"}}, + } + assert.Equal(t, []string{"client_secret_basic"}, constants.GetSupportedTokenEndpointAuthMethods(cfg)) +} + // TestGetSupportedSubjectTypes tests the GetSupportedSubjectTypes function // This is a standalone test for constants - doesn't require discovery service setup func TestGetSupportedSubjectTypes(t *testing.T) { @@ -439,3 +490,5 @@ func (suite *DiscoveryTestSuite) TestOIDCDiscovery_DeduplicatesAlgorithms() { assert.Equal(suite.T(), 1, len(algs)) assert.Contains(suite.T(), algs, "RS256") } + +func boolPtr(b bool) *bool { return &b } diff --git a/backend/internal/oauth/oauth2/discovery/service.go b/backend/internal/oauth/oauth2/discovery/service.go index 60bab07749..fbcc52c4f2 100644 --- a/backend/internal/oauth/oauth2/discovery/service.go +++ b/backend/internal/oauth/oauth2/discovery/service.go @@ -30,6 +30,7 @@ import ( "github.com/thunder-id/thunderid/internal/oauth/oauth2/pkce" kmprovider "github.com/thunder-id/thunderid/internal/system/kmprovider/common" "github.com/thunder-id/thunderid/internal/system/log" + "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" ) // DiscoveryServiceInterface defines the interface for discovery services @@ -63,14 +64,9 @@ func (ds *discoveryService) GetOAuth2AuthorizationServerMetadata( AuthorizationEndpoint: ds.getAuthorizationEndpoint(), TokenEndpoint: ds.getTokenEndpoint(), JWKSUri: ds.getJWKSUri(), - RegistrationEndpoint: ds.getRegistrationEndpoint(), IntrospectionEndpoint: ds.getIntrospectionEndpoint(), - RevocationEndpoint: ds.getRevocationEndpoint(), PushedAuthorizationRequestEndpoint: ds.getPAREndpoint(), RequirePushedAuthorizationRequests: ds.isGlobalPARRequired(), - BackchannelAuthenticationEndpoint: ds.getBackchannelAuthenticationEndpoint(), - BackchannelTokenDeliveryModesSupported: []string{"poll"}, - BackchannelUserCodeParameterSupported: false, ResponseTypesSupported: ds.getSupportedResponseTypes(), GrantTypesSupported: ds.getSupportedGrantTypes(), TokenEndpointAuthMethodsSupported: ds.getSupportedTokenEndpointAuthMethods(), @@ -79,6 +75,17 @@ func (ds *discoveryService) GetOAuth2AuthorizationServerMetadata( DPoPSigningAlgValuesSupported: ds.getSupportedDPoPSigningAlgs(), } + if slices.Contains(metadata.GrantTypesSupported, string(providers.GrantTypeCIBA)) { + metadata.BackchannelAuthenticationEndpoint = ds.getBackchannelAuthenticationEndpoint() + metadata.BackchannelTokenDeliveryModesSupported = []string{"poll"} + metadata.BackchannelUserCodeParameterSupported = false + } + if ds.cfg.OAuth.TokenRevocation.Enabled { + metadata.RevocationEndpoint = ds.getRevocationEndpoint() + } + if ds.cfg.OAuth.DCR.IsEnabled() { + metadata.RegistrationEndpoint = ds.getRegistrationEndpoint() + } return metadata } @@ -90,7 +97,7 @@ func (ds *discoveryService) GetOIDCMetadata(ctx context.Context) (*OIDCProviderM if err != nil { return nil, err } - return &OIDCProviderMetadata{ + oidcProviderMetadata := &OIDCProviderMetadata{ OAuth2AuthorizationServerMetadata: *oauth2Meta, UserInfoEndpoint: ds.getUserInfoEndpoint(), ScopesSupported: ds.getSupportedOIDCScopes(), @@ -103,9 +110,14 @@ func (ds *discoveryService) GetOIDCMetadata(ctx context.Context) (*OIDCProviderM IDTokenEncryptionEncValuesSupported: inboundmodel.SupportedIDTokenEncryptionEncs, ClaimsSupported: ds.getSupportedClaims(), ClaimsParameterSupported: true, - EndSessionEndpoint: ds.getEndSessionEndpoint(), AcrValuesSupported: ds.getSupportedAcrValues(), - }, nil + } + + if ds.cfg.OAuth.Logout.Enabled { + oidcProviderMetadata.EndSessionEndpoint = ds.getEndSessionEndpoint() + } + + return oidcProviderMetadata, nil } func (ds *discoveryService) getEndSessionEndpoint() string { @@ -153,15 +165,15 @@ func (ds *discoveryService) getSupportedOIDCScopes() []string { } func (ds *discoveryService) getSupportedResponseTypes() []string { - return constants.GetSupportedResponseTypes() + return constants.GetSupportedResponseTypes(ds.cfg) } func (ds *discoveryService) getSupportedGrantTypes() []string { - return constants.GetSupportedGrantTypes() + return constants.GetSupportedGrantTypes(ds.cfg) } func (ds *discoveryService) getSupportedTokenEndpointAuthMethods() []string { - return constants.GetSupportedTokenEndpointAuthMethods() + return constants.GetSupportedTokenEndpointAuthMethods(ds.cfg) } func (ds *discoveryService) getSupportedCodeChallengeMethods() []string { diff --git a/backend/internal/oauth/oauth2/granthandlers/provider.go b/backend/internal/oauth/oauth2/granthandlers/provider.go index f6c317d28e..39165cce15 100644 --- a/backend/internal/oauth/oauth2/granthandlers/provider.go +++ b/backend/internal/oauth/oauth2/granthandlers/provider.go @@ -19,6 +19,8 @@ package granthandlers import ( + "slices" + "github.com/thunder-id/thunderid/internal/attributecache" oauthconfig "github.com/thunder-id/thunderid/internal/oauth/config" "github.com/thunder-id/thunderid/internal/oauth/oauth2/authz" @@ -62,38 +64,64 @@ func newGrantHandlerProvider( refreshTokenRevoker revocation.RefreshTokenRevokerInterface, cfg oauthconfig.Config, ) GrantHandlerProviderInterface { - return &GrantHandlerProvider{ - clientCredentialsGrantHandler: newClientCredentialsGrantHandler( - tokenBuilder, ouService, rbacAuthzService, actorProvider, resourceService, serverConfigService), - authorizationCodeGrantHandler: newAuthorizationCodeGrantHandler( - authzService, tokenBuilder, attrCacheService, resourceService, serverConfigService), - refreshTokenGrantHandler: newRefreshTokenGrantHandler( + allowedGrantTypes := cfg.OAuth.AllowedGrantTypes + grantProvider := &GrantHandlerProvider{} + if isGrantTypeAllowed(allowedGrantTypes, providers.GrantTypeClientCredentials) { + grantProvider.clientCredentialsGrantHandler = newClientCredentialsGrantHandler( + tokenBuilder, ouService, rbacAuthzService, actorProvider, resourceService, serverConfigService) + } + if isGrantTypeAllowed(allowedGrantTypes, providers.GrantTypeAuthorizationCode) { + grantProvider.authorizationCodeGrantHandler = newAuthorizationCodeGrantHandler( + authzService, tokenBuilder, attrCacheService, resourceService, serverConfigService) + } + if isGrantTypeAllowed(allowedGrantTypes, providers.GrantTypeRefreshToken) { + grantProvider.refreshTokenGrantHandler = newRefreshTokenGrantHandler( jwtService, tokenBuilder, tokenValidator, attrCacheService, resourceService, - serverConfigService, refreshTokenRevoker, cfg), - tokenExchangeGrantHandler: newTokenExchangeGrantHandler( - tokenBuilder, tokenValidator, rbacAuthzService, actorProvider, resourceService, serverConfigService), - cibaGrantHandler: newCIBAGrantHandler(cibaService, tokenBuilder, attrCacheService, resourceService), - jwtBearerGrantHandler: newJWTBearerGrantHandler( - tokenBuilder, tokenValidator, resourceService, serverConfigService), + serverConfigService, refreshTokenRevoker, cfg) + } + if isGrantTypeAllowed(allowedGrantTypes, providers.GrantTypeTokenExchange) { + grantProvider.tokenExchangeGrantHandler = newTokenExchangeGrantHandler( + tokenBuilder, tokenValidator, rbacAuthzService, actorProvider, resourceService, serverConfigService) + } + if isGrantTypeAllowed(allowedGrantTypes, providers.GrantTypeCIBA) { + grantProvider.cibaGrantHandler = newCIBAGrantHandler(cibaService, tokenBuilder, attrCacheService, + resourceService) } + if isGrantTypeAllowed(allowedGrantTypes, providers.GrantTypeJWTBearer) { + grantProvider.jwtBearerGrantHandler = newJWTBearerGrantHandler( + tokenBuilder, tokenValidator, resourceService, serverConfigService) + } + return grantProvider +} + +// isGrantTypeAllowed reports whether the given grant type may be registered. An empty +// allow list means no restriction is configured, so every grant type is allowed. +func isGrantTypeAllowed(allowedGrantTypes []string, grantType providers.GrantType) bool { + if len(allowedGrantTypes) == 0 { + return true + } + return slices.Contains(allowedGrantTypes, string(grantType)) } // GetGrantHandler returns the appropriate grant handler for the given grant type. func (p *GrantHandlerProvider) GetGrantHandler(grantType providers.GrantType) (GrantHandlerInterface, error) { + var handler GrantHandlerInterface switch grantType { case providers.GrantTypeClientCredentials: - return p.clientCredentialsGrantHandler, nil + handler = p.clientCredentialsGrantHandler case providers.GrantTypeAuthorizationCode: - return p.authorizationCodeGrantHandler, nil + handler = p.authorizationCodeGrantHandler case providers.GrantTypeRefreshToken: - return p.refreshTokenGrantHandler, nil + handler = p.refreshTokenGrantHandler case providers.GrantTypeTokenExchange: - return p.tokenExchangeGrantHandler, nil + handler = p.tokenExchangeGrantHandler case providers.GrantTypeCIBA: - return p.cibaGrantHandler, nil + handler = p.cibaGrantHandler case providers.GrantTypeJWTBearer: - return p.jwtBearerGrantHandler, nil - default: + handler = p.jwtBearerGrantHandler + } + if handler == nil { return nil, constants.UnSupportedGrantTypeError } + return handler, nil } diff --git a/backend/internal/oauth/oauth2/granthandlers/provider_test.go b/backend/internal/oauth/oauth2/granthandlers/provider_test.go index c0647f2d6d..f384645357 100644 --- a/backend/internal/oauth/oauth2/granthandlers/provider_test.go +++ b/backend/internal/oauth/oauth2/granthandlers/provider_test.go @@ -134,6 +134,14 @@ func (suite *GrantHandlerProviderTestSuite) TestGetGrantHandler_RefreshToken() { assert.Implements(suite.T(), (*RefreshTokenGrantHandlerInterface)(nil), handler) } +func (suite *GrantHandlerProviderTestSuite) TestGetGrantHandler_TokenExchange() { + handler, err := suite.provider.GetGrantHandler(providers.GrantTypeTokenExchange) + + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), handler) + assert.Implements(suite.T(), (*GrantHandlerInterface)(nil), handler) +} + func (suite *GrantHandlerProviderTestSuite) TestGetGrantHandler_CIBA() { handler, err := suite.provider.GetGrantHandler(providers.GrantTypeCIBA) @@ -175,6 +183,7 @@ func (suite *GrantHandlerProviderTestSuite) TestGetGrantHandler_AllSupportedType providers.GrantTypeClientCredentials, providers.GrantTypeAuthorizationCode, providers.GrantTypeRefreshToken, + providers.GrantTypeTokenExchange, providers.GrantTypeCIBA, providers.GrantTypeJWTBearer, } diff --git a/backend/internal/oauth/oauth2/granthandlers/refresh_token.go b/backend/internal/oauth/oauth2/granthandlers/refresh_token.go index 3d36f261ac..4adaeb9768 100644 --- a/backend/internal/oauth/oauth2/granthandlers/refresh_token.go +++ b/backend/internal/oauth/oauth2/granthandlers/refresh_token.go @@ -294,7 +294,7 @@ func (h *refreshTokenGrantHandler) HandleGrant(ctx context.Context, tokenRequest // Single-use: revoke the consumed refresh token so it cannot be replayed (RFC 9700 §4.14.2). // Fail closed — if the revocation cannot be recorded, the old token would remain usable, so the // rotation is rejected and the client retries with the still-valid old token. - if h.cfg.OAuth.RefreshToken.RevokePreviousOnRenew { + if h.refreshRevoker != nil && h.cfg.OAuth.RefreshToken.RevokePreviousOnRenew { expiryTime := time.Unix(refreshTokenClaims.Exp, 0).UTC() if err := h.refreshRevoker.RevokeRefreshToken( ctx, refreshTokenClaims.JTI, expiryTime); err != nil { diff --git a/backend/internal/oauth/oauth2/granthandlers/refresh_token_test.go b/backend/internal/oauth/oauth2/granthandlers/refresh_token_test.go index 91113c4ad7..19c6e9af19 100644 --- a/backend/internal/oauth/oauth2/granthandlers/refresh_token_test.go +++ b/backend/internal/oauth/oauth2/granthandlers/refresh_token_test.go @@ -598,6 +598,48 @@ func (suite *RefreshTokenGrantHandlerTestSuite) TestHandleGrant_RenewRevokeFailu assert.Equal(suite.T(), constants.ErrorServerError, err.Error) } +func (suite *RefreshTokenGrantHandlerTestSuite) TestHandleGrant_RevokePreviousOnRenew_NilRefreshRevoker() { + // When the token_revocation feature is disabled, refreshRevoker is nil even though + // renew_on_grant/revoke_previous_on_renew are independently configured. The handler must + // skip revocation rather than dereference the nil revoker. + suite.testCfg.OAuth.RefreshToken.RenewOnGrant = true + suite.testCfg.OAuth.RefreshToken.RevokePreviousOnRenew = true + suite.handler = newRefreshTokenGrantHandler( + suite.mockJWTService, + suite.mockTokenBuilder, + suite.mockTokenValidator, + suite.mockAttrCacheService, + suite.mockResourceService, + suite.mockServerConfigSvc, + nil, + suite.testCfg, + ).(*refreshTokenGrantHandler) + + suite.mockTokenValidator. + On("ValidateRefreshToken", mock.Anything, suite.validRefreshToken, testRefreshTokenClientID). + Return(&tokenservice.RefreshTokenClaims{ + Sub: testRefreshTokenUserID, + Audiences: []string{testRefreshTokenAudience}, + Scopes: []string{"read", "write"}, + GrantType: "authorization_code", + Iat: int64(suite.validClaims["iat"].(float64)), + JTI: "consumed-rt-jti", + Exp: int64(suite.validClaims["exp"].(float64)), + }, nil) + suite.mockTokenBuilder.On("BuildAccessToken", mock.Anything, mock.Anything).Return(&model.TokenDTO{ + Token: "new.access.token", IssuedAt: time.Now().Unix(), ExpiresIn: 3600, Scopes: []string{"read"}, + }, nil) + suite.mockTokenBuilder.On("BuildRefreshToken", mock.Anything, mock.Anything).Return(&model.TokenDTO{ + Token: "new.refresh.token", IssuedAt: time.Now().Unix(), ExpiresIn: 86400, Scopes: []string{"read", "write"}, + }, nil) + + response, err := suite.handler.HandleGrant(context.Background(), suite.testTokenReq, suite.oauthApp) + + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), response) + assert.Equal(suite.T(), "new.refresh.token", response.RefreshToken.Token) +} + func (suite *RefreshTokenGrantHandlerTestSuite) TestHandleGrant_Success_WithRenewOnGrantEnabled() { // Enable RenewOnGrant in config suite.testCfg.OAuth.RefreshToken.RenewOnGrant = true diff --git a/backend/internal/oauth/oauth2/tokenservice/validator.go b/backend/internal/oauth/oauth2/tokenservice/validator.go index 34a87fd673..493fb3a3c4 100644 --- a/backend/internal/oauth/oauth2/tokenservice/validator.go +++ b/backend/internal/oauth/oauth2/tokenservice/validator.go @@ -131,7 +131,7 @@ func (tv *tokenValidator) ValidateAccessToken(ctx context.Context, token string) scopes := extractScopesFromClaims(claims, false) jti, _ := extractStringClaim(claims, constants.ClaimJTI) - if err := tv.enforcementService.EnsureNotRevoked(ctx, jti); err != nil { + if err := tv.ensureNotRevoked(ctx, jti); err != nil { return nil, err } @@ -197,7 +197,7 @@ func (tv *tokenValidator) ValidateRefreshToken( dpopJkt = s } - if err := tv.enforcementService.EnsureNotRevoked(ctx, jti); err != nil { + if err := tv.ensureNotRevoked(ctx, jti); err != nil { return nil, err } @@ -253,7 +253,7 @@ func (tv *tokenValidator) ValidateSubjectToken( if err != nil { return nil, err } - if err := tv.enforcementService.EnsureNotRevoked(ctx, selfClaims.JTI); err != nil { + if err := tv.ensureNotRevoked(ctx, selfClaims.JTI); err != nil { return nil, err } return selfClaims, nil @@ -337,7 +337,7 @@ func (tv *tokenValidator) ValidateToken(ctx context.Context, token string) (map[ } jti, _ := extractStringClaim(claims, constants.ClaimJTI) - if err := tv.enforcementService.EnsureNotRevoked(ctx, jti); err != nil { + if err := tv.ensureNotRevoked(ctx, jti); err != nil { return nil, err } @@ -688,3 +688,10 @@ func (tv *tokenValidator) isAuthAssertion( return false } + +func (tv *tokenValidator) ensureNotRevoked(ctx context.Context, jti string) error { + if tv.enforcementService != nil { + return tv.enforcementService.EnsureNotRevoked(ctx, jti) + } + return nil +} diff --git a/backend/internal/oauth/oauth2/tokenservice/validator_test.go b/backend/internal/oauth/oauth2/tokenservice/validator_test.go index 74920637c0..1e27cb567e 100644 --- a/backend/internal/oauth/oauth2/tokenservice/validator_test.go +++ b/backend/internal/oauth/oauth2/tokenservice/validator_test.go @@ -2076,6 +2076,30 @@ func (suite *TokenValidatorTestSuite) TestValidateAccessToken_EnforcementUnavail assert.ErrorIs(suite.T(), err, revocation.ErrEnforcementUnavailable) } +// When token revocation is disabled, enforcementService is nil. Validation must still succeed +// rather than dereferencing the nil service. +func (suite *TokenValidatorTestSuite) TestValidateAccessToken_NilEnforcementService_Succeeds() { + claims := map[string]interface{}{ + "sub": "user123", + "iss": "https://example.com", + "aud": "test-app", + "client_id": "test-client", + "jti": "at-jti-no-enforcement", + } + token := suite.createTestAccessToken(claims) + suite.mockJWTService.On("VerifyJWT", mock.Anything, token, "", "https://example.com").Return(nil) + + validator := &tokenValidator{ + cfg: suite.validator.cfg, + jwtService: suite.mockJWTService, + } + + result, err := validator.ValidateAccessToken(context.Background(), token) + + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), result) +} + // Refresh token validation enforces the deny list as its final step: a revoked token surfaces // revocation.ErrTokenRevoked and an unavailable deny list fails closed with // revocation.ErrEnforcementUnavailable rather than returning claims. diff --git a/backend/pkg/thunderidengine/config/config.go b/backend/pkg/thunderidengine/config/config.go index 05ad430696..c8a9caff6b 100644 --- a/backend/pkg/thunderidengine/config/config.go +++ b/backend/pkg/thunderidengine/config/config.go @@ -179,7 +179,14 @@ type AuthorizationCodeConfig struct { // DCRConfig holds the Dynamic Client Registration configuration. type DCRConfig struct { - Insecure bool `yaml:"insecure" json:"insecure"` + Enabled *bool `yaml:"enabled" json:"enabled"` + Insecure bool `yaml:"insecure" json:"insecure"` +} + +// IsEnabled returns whether DCR is enabled, defaulting to false if unset +// (an explicit default lives in default.json). +func (c DCRConfig) IsEnabled() bool { + return c.Enabled != nil && *c.Enabled } // PARConfig holds the Pushed Authorization Request (RFC 9126) configuration. @@ -214,6 +221,27 @@ type OAuthConfig struct { // AllowWildcardRedirectURI enables wildcard pattern matching for redirect URIs. // When false (default), only exact redirect URI matching is performed. AllowWildcardRedirectURI bool `yaml:"allow_wildcard_redirect_uri" json:"allow_wildcard_redirect_uri"` + // AllowedGrantTypes enables registering of only the configured grant types + AllowedGrantTypes []string `yaml:"allowed_grant_types" json:"allowed_grant_types"` + // AllowedResponseTypes enables registering of only the configured response types + AllowedResponseTypes []string `yaml:"allowed_response_types" json:"allowed_response_types"` + // AllowedAuthMethods lists allowed client token endpoint auth methods + AllowedAuthMethods []string `yaml:"allowed_auth_methods" json:"allowed_auth_methods"` + + TokenRevocation OAuthTokenRevocationConfig `yaml:"token_revocation" json:"token_revocation"` + Logout LogoutConfig `yaml:"logout" json:"logout"` +} + +// OAuthTokenRevocationConfig holds the configuration details for the token revocation feature +type OAuthTokenRevocationConfig struct { + // Enabled controls whether the OAuth token revocation endpoint is active. + Enabled bool `yaml:"enabled" json:"enabled"` +} + +// LogoutConfig holds the configuration details for the logout endpoint +type LogoutConfig struct { + // Enabled controls whether the OAuth logout endpoint is active. + Enabled bool `yaml:"enabled" json:"enabled"` } // FlowConfig holds the configuration details for the flow service. diff --git a/backend/pkg/thunderidengine/engine.go b/backend/pkg/thunderidengine/engine.go index 7cef97d997..e8644b2c16 100644 --- a/backend/pkg/thunderidengine/engine.go +++ b/backend/pkg/thunderidengine/engine.go @@ -230,6 +230,33 @@ func validateEngineContext(ctx *engineContext) error { if ctx.authzProvider == nil { return errors.New("thunderidengine: authorization provider is not set") } + if ctx.actorProvider == nil { + return errors.New("thunderidengine: actor provider is not set") + } + if ctx.authnProvider == nil { + return errors.New("thunderidengine: authn provider is not set") + } + if ctx.resourceProvider == nil { + return errors.New("thunderidengine: resource server provider is not set") + } + if ctx.ouProvider == nil { + return errors.New("thunderidengine: organization unit provider is not set") + } + if ctx.designResolveProvider == nil { + return errors.New("thunderidengine: design provider is not set") + } + if ctx.flowProvider == nil { + return errors.New("thunderidengine: flow provider is not set") + } + if ctx.i18nProvider == nil { + return errors.New("thunderidengine: i18n provider is not set") + } + if ctx.idpProvider == nil { + return errors.New("thunderidengine: idp provider is not set") + } + if ctx.consentProvider == nil { + return errors.New("thunderidengine: consent provider is not set") + } return nil } diff --git a/backend/pkg/thunderidengine/engine_test.go b/backend/pkg/thunderidengine/engine_test.go index aeb98b80d9..ea72a71a56 100644 --- a/backend/pkg/thunderidengine/engine_test.go +++ b/backend/pkg/thunderidengine/engine_test.go @@ -39,11 +39,19 @@ import ( joseconfig "github.com/thunder-id/thunderid/internal/system/jose/config" engineconfig "github.com/thunder-id/thunderid/pkg/thunderidengine/config" "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" + "github.com/thunder-id/thunderid/tests/mocks/actorprovidermock" + "github.com/thunder-id/thunderid/tests/mocks/authnprovider/managermock" "github.com/thunder-id/thunderid/tests/mocks/authzmock" + "github.com/thunder-id/thunderid/tests/mocks/consentprovidermock" + "github.com/thunder-id/thunderid/tests/mocks/designprovidermock" "github.com/thunder-id/thunderid/tests/mocks/flow/coremock" "github.com/thunder-id/thunderid/tests/mocks/flow/executormock" + "github.com/thunder-id/thunderid/tests/mocks/flow/flowexecmock" + "github.com/thunder-id/thunderid/tests/mocks/i18nprovidermock" "github.com/thunder-id/thunderid/tests/mocks/idpprovidermock" "github.com/thunder-id/thunderid/tests/mocks/observabilityprovidermock" + "github.com/thunder-id/thunderid/tests/mocks/ouprovidermock" + "github.com/thunder-id/thunderid/tests/mocks/resourceserverprovidermock" "github.com/thunder-id/thunderid/tests/mocks/runtimestoreprovidermock" ) @@ -82,10 +90,19 @@ func newTestRuntimeStoreProvider(t *testing.T) providers.RuntimeStoreProvider { func validEngineContext(t *testing.T) *engineContext { return &engineContext{ - serverHome: "/tmp/server", - serverConfig: engineconfig.ServerConfig{Identifier: "test-server"}, - observabilitySvc: newTestObservabilityProvider(t), - authzProvider: newTestAuthzProvider(t), + serverHome: "/tmp/server", + serverConfig: engineconfig.ServerConfig{Identifier: "test-server"}, + observabilitySvc: newTestObservabilityProvider(t), + authzProvider: newTestAuthzProvider(t), + actorProvider: actorprovidermock.NewActorProviderMock(t), + authnProvider: managermock.NewAuthnProviderManagerMock(t), + resourceProvider: resourceserverprovidermock.NewResourceServerProviderMock(t), + ouProvider: ouprovidermock.NewOrganizationUnitProviderMock(t), + designResolveProvider: designprovidermock.NewDesignProviderMock(t), + flowProvider: flowexecmock.NewFlowProviderMock(t), + i18nProvider: i18nprovidermock.NewI18nProviderMock(t), + idpProvider: idpprovidermock.NewIDPProviderMock(t), + consentProvider: consentprovidermock.NewConsentProviderMock(t), } } @@ -117,6 +134,60 @@ func (suite *EngineTestSuite) TestValidateEngineContext() { ctx.authzProvider = nil assert.ErrorContains(t, validateEngineContext(ctx), "authorization provider") }) + + suite.T().Run("missing actor provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.actorProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "actor provider") + }) + + suite.T().Run("missing authn provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.authnProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "authn provider") + }) + + suite.T().Run("missing resource provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.resourceProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "resource server provider") + }) + + suite.T().Run("missing organization unit provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.ouProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "organization unit provider") + }) + + suite.T().Run("missing design provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.designResolveProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "design provider") + }) + + suite.T().Run("missing flow provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.flowProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "flow provider") + }) + + suite.T().Run("missing i18n provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.i18nProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "i18n provider") + }) + + suite.T().Run("missing idp provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.idpProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "idp provider") + }) + + suite.T().Run("missing consent provider", func(t *testing.T) { + ctx := validEngineContext(t) + ctx.consentProvider = nil + assert.ErrorContains(t, validateEngineContext(ctx), "consent provider") + }) } func (suite *EngineTestSuite) TestApplyCustomExecutors() { @@ -331,6 +402,14 @@ func (suite *EngineTestSuite) TestNew_HappyPath() { WithCacheConfig(engineconfig.CacheConfig{Disabled: true}), WithLogConfig(engineconfig.LogConfig{Level: "info", Format: "json"}), WithIDPProvider(newTestIDPProvider(t)), + WithActorProvider(actorprovidermock.NewActorProviderMock(t)), + WithAuthnProvider(managermock.NewAuthnProviderManagerMock(t)), + WithResourceProvider(resourceserverprovidermock.NewResourceServerProviderMock(t)), + WithOUProvider(ouprovidermock.NewOrganizationUnitProviderMock(t)), + WithDesignResolveProvider(designprovidermock.NewDesignProviderMock(t)), + WithFlowProvider(flowexecmock.NewFlowProviderMock(t)), + WithI18nProvider(i18nprovidermock.NewI18nProviderMock(t)), + WithConsentProvider(consentprovidermock.NewConsentProviderMock(t)), // Restrict to built-in executors that only depend on FlowFactory; the others assume // non-nil typed provider dependencies (e.g. the GitHub/Google/OIDC auth executors) that // this minimal test setup does not wire up. @@ -396,6 +475,14 @@ func (suite *EngineTestSuite) TestNew_InitializesRuntimeStoreWhenNotInjected() { WithGateClientConfig(engineconfig.GateClientConfig{Hostname: "localhost", Port: 8080, Scheme: "https"}), WithCacheConfig(engineconfig.CacheConfig{Disabled: true}), WithIDPProvider(newTestIDPProvider(t)), + WithActorProvider(actorprovidermock.NewActorProviderMock(t)), + WithAuthnProvider(managermock.NewAuthnProviderManagerMock(t)), + WithResourceProvider(resourceserverprovidermock.NewResourceServerProviderMock(t)), + WithOUProvider(ouprovidermock.NewOrganizationUnitProviderMock(t)), + WithDesignResolveProvider(designprovidermock.NewDesignProviderMock(t)), + WithFlowProvider(flowexecmock.NewFlowProviderMock(t)), + WithI18nProvider(i18nprovidermock.NewI18nProviderMock(t)), + WithConsentProvider(consentprovidermock.NewConsentProviderMock(t)), // Restrict to built-in executors that only depend on FlowFactory; the others assume // non-nil typed provider dependencies (e.g. the GitHub/Google/OIDC auth executors) that // this minimal test setup does not wire up. diff --git a/backend/tests/mocks/designprovidermock/DesignProvider_mock.go b/backend/tests/mocks/designprovidermock/DesignProvider_mock.go new file mode 100644 index 0000000000..1a3151836e --- /dev/null +++ b/backend/tests/mocks/designprovidermock/DesignProvider_mock.go @@ -0,0 +1,116 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package designprovidermock + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "github.com/thunder-id/thunderid/pkg/thunderidengine/common" + "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" +) + +// NewDesignProviderMock creates a new instance of DesignProviderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDesignProviderMock(t interface { + mock.TestingT + Cleanup(func()) +}) *DesignProviderMock { + mock := &DesignProviderMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// DesignProviderMock is an autogenerated mock type for the DesignProvider type +type DesignProviderMock struct { + mock.Mock +} + +type DesignProviderMock_Expecter struct { + mock *mock.Mock +} + +func (_m *DesignProviderMock) EXPECT() *DesignProviderMock_Expecter { + return &DesignProviderMock_Expecter{mock: &_m.Mock} +} + +// ResolveDesign provides a mock function for the type DesignProviderMock +func (_mock *DesignProviderMock) ResolveDesign(ctx context.Context, resolveType providers.DesignResolveType, id string) (*providers.DesignResponse, *common.ServiceError) { + ret := _mock.Called(ctx, resolveType, id) + + if len(ret) == 0 { + panic("no return value specified for ResolveDesign") + } + + var r0 *providers.DesignResponse + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, providers.DesignResolveType, string) (*providers.DesignResponse, *common.ServiceError)); ok { + return returnFunc(ctx, resolveType, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, providers.DesignResolveType, string) *providers.DesignResponse); ok { + r0 = returnFunc(ctx, resolveType, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providers.DesignResponse) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, providers.DesignResolveType, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, resolveType, id) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// DesignProviderMock_ResolveDesign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResolveDesign' +type DesignProviderMock_ResolveDesign_Call struct { + *mock.Call +} + +// ResolveDesign is a helper method to define mock.On call +// - ctx context.Context +// - resolveType providers.DesignResolveType +// - id string +func (_e *DesignProviderMock_Expecter) ResolveDesign(ctx interface{}, resolveType interface{}, id interface{}) *DesignProviderMock_ResolveDesign_Call { + return &DesignProviderMock_ResolveDesign_Call{Call: _e.mock.On("ResolveDesign", ctx, resolveType, id)} +} + +func (_c *DesignProviderMock_ResolveDesign_Call) Run(run func(ctx context.Context, resolveType providers.DesignResolveType, id string)) *DesignProviderMock_ResolveDesign_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 providers.DesignResolveType + if args[1] != nil { + arg1 = args[1].(providers.DesignResolveType) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *DesignProviderMock_ResolveDesign_Call) Return(designResponse *providers.DesignResponse, serviceError *common.ServiceError) *DesignProviderMock_ResolveDesign_Call { + _c.Call.Return(designResponse, serviceError) + return _c +} + +func (_c *DesignProviderMock_ResolveDesign_Call) RunAndReturn(run func(ctx context.Context, resolveType providers.DesignResolveType, id string) (*providers.DesignResponse, *common.ServiceError)) *DesignProviderMock_ResolveDesign_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/mocks/i18nprovidermock/I18nProvider_mock.go b/backend/tests/mocks/i18nprovidermock/I18nProvider_mock.go new file mode 100644 index 0000000000..9168970d8c --- /dev/null +++ b/backend/tests/mocks/i18nprovidermock/I18nProvider_mock.go @@ -0,0 +1,180 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package i18nprovidermock + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "github.com/thunder-id/thunderid/pkg/thunderidengine/common" + "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" +) + +// NewI18nProviderMock creates a new instance of I18nProviderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewI18nProviderMock(t interface { + mock.TestingT + Cleanup(func()) +}) *I18nProviderMock { + mock := &I18nProviderMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// I18nProviderMock is an autogenerated mock type for the I18nProvider type +type I18nProviderMock struct { + mock.Mock +} + +type I18nProviderMock_Expecter struct { + mock *mock.Mock +} + +func (_m *I18nProviderMock) EXPECT() *I18nProviderMock_Expecter { + return &I18nProviderMock_Expecter{mock: &_m.Mock} +} + +// ListLanguages provides a mock function for the type I18nProviderMock +func (_mock *I18nProviderMock) ListLanguages(ctx context.Context) ([]string, *common.ServiceError) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListLanguages") + } + + var r0 []string + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context) ([]string, *common.ServiceError)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context) *common.ServiceError); ok { + r1 = returnFunc(ctx) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// I18nProviderMock_ListLanguages_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListLanguages' +type I18nProviderMock_ListLanguages_Call struct { + *mock.Call +} + +// ListLanguages is a helper method to define mock.On call +// - ctx context.Context +func (_e *I18nProviderMock_Expecter) ListLanguages(ctx interface{}) *I18nProviderMock_ListLanguages_Call { + return &I18nProviderMock_ListLanguages_Call{Call: _e.mock.On("ListLanguages", ctx)} +} + +func (_c *I18nProviderMock_ListLanguages_Call) Run(run func(ctx context.Context)) *I18nProviderMock_ListLanguages_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *I18nProviderMock_ListLanguages_Call) Return(strings []string, serviceError *common.ServiceError) *I18nProviderMock_ListLanguages_Call { + _c.Call.Return(strings, serviceError) + return _c +} + +func (_c *I18nProviderMock_ListLanguages_Call) RunAndReturn(run func(ctx context.Context) ([]string, *common.ServiceError)) *I18nProviderMock_ListLanguages_Call { + _c.Call.Return(run) + return _c +} + +// ResolveTranslations provides a mock function for the type I18nProviderMock +func (_mock *I18nProviderMock) ResolveTranslations(ctx context.Context, language string, namespace string) (*providers.LanguageTranslationsResponse, *common.ServiceError) { + ret := _mock.Called(ctx, language, namespace) + + if len(ret) == 0 { + panic("no return value specified for ResolveTranslations") + } + + var r0 *providers.LanguageTranslationsResponse + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*providers.LanguageTranslationsResponse, *common.ServiceError)); ok { + return returnFunc(ctx, language, namespace) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *providers.LanguageTranslationsResponse); ok { + r0 = returnFunc(ctx, language, namespace) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providers.LanguageTranslationsResponse) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, language, namespace) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// I18nProviderMock_ResolveTranslations_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ResolveTranslations' +type I18nProviderMock_ResolveTranslations_Call struct { + *mock.Call +} + +// ResolveTranslations is a helper method to define mock.On call +// - ctx context.Context +// - language string +// - namespace string +func (_e *I18nProviderMock_Expecter) ResolveTranslations(ctx interface{}, language interface{}, namespace interface{}) *I18nProviderMock_ResolveTranslations_Call { + return &I18nProviderMock_ResolveTranslations_Call{Call: _e.mock.On("ResolveTranslations", ctx, language, namespace)} +} + +func (_c *I18nProviderMock_ResolveTranslations_Call) Run(run func(ctx context.Context, language string, namespace string)) *I18nProviderMock_ResolveTranslations_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *I18nProviderMock_ResolveTranslations_Call) Return(languageTranslationsResponse *providers.LanguageTranslationsResponse, serviceError *common.ServiceError) *I18nProviderMock_ResolveTranslations_Call { + _c.Call.Return(languageTranslationsResponse, serviceError) + return _c +} + +func (_c *I18nProviderMock_ResolveTranslations_Call) RunAndReturn(run func(ctx context.Context, language string, namespace string) (*providers.LanguageTranslationsResponse, *common.ServiceError)) *I18nProviderMock_ResolveTranslations_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/mocks/ouprovidermock/OrganizationUnitProvider_mock.go b/backend/tests/mocks/ouprovidermock/OrganizationUnitProvider_mock.go new file mode 100644 index 0000000000..fa17bed857 --- /dev/null +++ b/backend/tests/mocks/ouprovidermock/OrganizationUnitProvider_mock.go @@ -0,0 +1,488 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package ouprovidermock + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "github.com/thunder-id/thunderid/pkg/thunderidengine/common" + "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" +) + +// NewOrganizationUnitProviderMock creates a new instance of OrganizationUnitProviderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOrganizationUnitProviderMock(t interface { + mock.TestingT + Cleanup(func()) +}) *OrganizationUnitProviderMock { + mock := &OrganizationUnitProviderMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// OrganizationUnitProviderMock is an autogenerated mock type for the OrganizationUnitProvider type +type OrganizationUnitProviderMock struct { + mock.Mock +} + +type OrganizationUnitProviderMock_Expecter struct { + mock *mock.Mock +} + +func (_m *OrganizationUnitProviderMock) EXPECT() *OrganizationUnitProviderMock_Expecter { + return &OrganizationUnitProviderMock_Expecter{mock: &_m.Mock} +} + +// CreateOrganizationUnit provides a mock function for the type OrganizationUnitProviderMock +func (_mock *OrganizationUnitProviderMock) CreateOrganizationUnit(ctx context.Context, request providers.OrganizationUnitRequestWithID) (providers.OrganizationUnit, *common.ServiceError) { + ret := _mock.Called(ctx, request) + + if len(ret) == 0 { + panic("no return value specified for CreateOrganizationUnit") + } + + var r0 providers.OrganizationUnit + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, providers.OrganizationUnitRequestWithID) (providers.OrganizationUnit, *common.ServiceError)); ok { + return returnFunc(ctx, request) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, providers.OrganizationUnitRequestWithID) providers.OrganizationUnit); ok { + r0 = returnFunc(ctx, request) + } else { + r0 = ret.Get(0).(providers.OrganizationUnit) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, providers.OrganizationUnitRequestWithID) *common.ServiceError); ok { + r1 = returnFunc(ctx, request) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// OrganizationUnitProviderMock_CreateOrganizationUnit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateOrganizationUnit' +type OrganizationUnitProviderMock_CreateOrganizationUnit_Call struct { + *mock.Call +} + +// CreateOrganizationUnit is a helper method to define mock.On call +// - ctx context.Context +// - request providers.OrganizationUnitRequestWithID +func (_e *OrganizationUnitProviderMock_Expecter) CreateOrganizationUnit(ctx interface{}, request interface{}) *OrganizationUnitProviderMock_CreateOrganizationUnit_Call { + return &OrganizationUnitProviderMock_CreateOrganizationUnit_Call{Call: _e.mock.On("CreateOrganizationUnit", ctx, request)} +} + +func (_c *OrganizationUnitProviderMock_CreateOrganizationUnit_Call) Run(run func(ctx context.Context, request providers.OrganizationUnitRequestWithID)) *OrganizationUnitProviderMock_CreateOrganizationUnit_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 providers.OrganizationUnitRequestWithID + if args[1] != nil { + arg1 = args[1].(providers.OrganizationUnitRequestWithID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *OrganizationUnitProviderMock_CreateOrganizationUnit_Call) Return(organizationUnit providers.OrganizationUnit, serviceError *common.ServiceError) *OrganizationUnitProviderMock_CreateOrganizationUnit_Call { + _c.Call.Return(organizationUnit, serviceError) + return _c +} + +func (_c *OrganizationUnitProviderMock_CreateOrganizationUnit_Call) RunAndReturn(run func(ctx context.Context, request providers.OrganizationUnitRequestWithID) (providers.OrganizationUnit, *common.ServiceError)) *OrganizationUnitProviderMock_CreateOrganizationUnit_Call { + _c.Call.Return(run) + return _c +} + +// GetOrganizationUnit provides a mock function for the type OrganizationUnitProviderMock +func (_mock *OrganizationUnitProviderMock) GetOrganizationUnit(ctx context.Context, id string) (providers.OrganizationUnit, *common.ServiceError) { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetOrganizationUnit") + } + + var r0 providers.OrganizationUnit + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (providers.OrganizationUnit, *common.ServiceError)); ok { + return returnFunc(ctx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) providers.OrganizationUnit); ok { + r0 = returnFunc(ctx, id) + } else { + r0 = ret.Get(0).(providers.OrganizationUnit) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, id) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// OrganizationUnitProviderMock_GetOrganizationUnit_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrganizationUnit' +type OrganizationUnitProviderMock_GetOrganizationUnit_Call struct { + *mock.Call +} + +// GetOrganizationUnit is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *OrganizationUnitProviderMock_Expecter) GetOrganizationUnit(ctx interface{}, id interface{}) *OrganizationUnitProviderMock_GetOrganizationUnit_Call { + return &OrganizationUnitProviderMock_GetOrganizationUnit_Call{Call: _e.mock.On("GetOrganizationUnit", ctx, id)} +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnit_Call) Run(run func(ctx context.Context, id string)) *OrganizationUnitProviderMock_GetOrganizationUnit_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnit_Call) Return(organizationUnit providers.OrganizationUnit, serviceError *common.ServiceError) *OrganizationUnitProviderMock_GetOrganizationUnit_Call { + _c.Call.Return(organizationUnit, serviceError) + return _c +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnit_Call) RunAndReturn(run func(ctx context.Context, id string) (providers.OrganizationUnit, *common.ServiceError)) *OrganizationUnitProviderMock_GetOrganizationUnit_Call { + _c.Call.Return(run) + return _c +} + +// GetOrganizationUnitChildren provides a mock function for the type OrganizationUnitProviderMock +func (_mock *OrganizationUnitProviderMock) GetOrganizationUnitChildren(ctx context.Context, id string, limit int, offset int, f *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError) { + ret := _mock.Called(ctx, id, limit, offset, f) + + if len(ret) == 0 { + panic("no return value specified for GetOrganizationUnitChildren") + } + + var r0 *providers.OrganizationUnitListResponse + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string, int, int, *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError)); ok { + return returnFunc(ctx, id, limit, offset, f) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, int, int, *common.FilterGroup) *providers.OrganizationUnitListResponse); ok { + r0 = returnFunc(ctx, id, limit, offset, f) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providers.OrganizationUnitListResponse) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, int, int, *common.FilterGroup) *common.ServiceError); ok { + r1 = returnFunc(ctx, id, limit, offset, f) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrganizationUnitChildren' +type OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call struct { + *mock.Call +} + +// GetOrganizationUnitChildren is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - limit int +// - offset int +// - f *common.FilterGroup +func (_e *OrganizationUnitProviderMock_Expecter) GetOrganizationUnitChildren(ctx interface{}, id interface{}, limit interface{}, offset interface{}, f interface{}) *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call { + return &OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call{Call: _e.mock.On("GetOrganizationUnitChildren", ctx, id, limit, offset, f)} +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call) Run(run func(ctx context.Context, id string, limit int, offset int, f *common.FilterGroup)) *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 int + if args[2] != nil { + arg2 = args[2].(int) + } + var arg3 int + if args[3] != nil { + arg3 = args[3].(int) + } + var arg4 *common.FilterGroup + if args[4] != nil { + arg4 = args[4].(*common.FilterGroup) + } + run( + arg0, + arg1, + arg2, + arg3, + arg4, + ) + }) + return _c +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call) Return(organizationUnitListResponse *providers.OrganizationUnitListResponse, serviceError *common.ServiceError) *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call { + _c.Call.Return(organizationUnitListResponse, serviceError) + return _c +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call) RunAndReturn(run func(ctx context.Context, id string, limit int, offset int, f *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError)) *OrganizationUnitProviderMock_GetOrganizationUnitChildren_Call { + _c.Call.Return(run) + return _c +} + +// GetOrganizationUnitList provides a mock function for the type OrganizationUnitProviderMock +func (_mock *OrganizationUnitProviderMock) GetOrganizationUnitList(ctx context.Context, limit int, offset int, f *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError) { + ret := _mock.Called(ctx, limit, offset, f) + + if len(ret) == 0 { + panic("no return value specified for GetOrganizationUnitList") + } + + var r0 *providers.OrganizationUnitListResponse + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, int, int, *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError)); ok { + return returnFunc(ctx, limit, offset, f) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, int, int, *common.FilterGroup) *providers.OrganizationUnitListResponse); ok { + r0 = returnFunc(ctx, limit, offset, f) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providers.OrganizationUnitListResponse) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, int, int, *common.FilterGroup) *common.ServiceError); ok { + r1 = returnFunc(ctx, limit, offset, f) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// OrganizationUnitProviderMock_GetOrganizationUnitList_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetOrganizationUnitList' +type OrganizationUnitProviderMock_GetOrganizationUnitList_Call struct { + *mock.Call +} + +// GetOrganizationUnitList is a helper method to define mock.On call +// - ctx context.Context +// - limit int +// - offset int +// - f *common.FilterGroup +func (_e *OrganizationUnitProviderMock_Expecter) GetOrganizationUnitList(ctx interface{}, limit interface{}, offset interface{}, f interface{}) *OrganizationUnitProviderMock_GetOrganizationUnitList_Call { + return &OrganizationUnitProviderMock_GetOrganizationUnitList_Call{Call: _e.mock.On("GetOrganizationUnitList", ctx, limit, offset, f)} +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnitList_Call) Run(run func(ctx context.Context, limit int, offset int, f *common.FilterGroup)) *OrganizationUnitProviderMock_GetOrganizationUnitList_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 int + if args[1] != nil { + arg1 = args[1].(int) + } + var arg2 int + if args[2] != nil { + arg2 = args[2].(int) + } + var arg3 *common.FilterGroup + if args[3] != nil { + arg3 = args[3].(*common.FilterGroup) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnitList_Call) Return(organizationUnitListResponse *providers.OrganizationUnitListResponse, serviceError *common.ServiceError) *OrganizationUnitProviderMock_GetOrganizationUnitList_Call { + _c.Call.Return(organizationUnitListResponse, serviceError) + return _c +} + +func (_c *OrganizationUnitProviderMock_GetOrganizationUnitList_Call) RunAndReturn(run func(ctx context.Context, limit int, offset int, f *common.FilterGroup) (*providers.OrganizationUnitListResponse, *common.ServiceError)) *OrganizationUnitProviderMock_GetOrganizationUnitList_Call { + _c.Call.Return(run) + return _c +} + +// IsOrganizationUnitExists provides a mock function for the type OrganizationUnitProviderMock +func (_mock *OrganizationUnitProviderMock) IsOrganizationUnitExists(ctx context.Context, id string) (bool, *common.ServiceError) { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for IsOrganizationUnitExists") + } + + var r0 bool + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (bool, *common.ServiceError)); ok { + return returnFunc(ctx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) bool); ok { + r0 = returnFunc(ctx, id) + } else { + r0 = ret.Get(0).(bool) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, id) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// OrganizationUnitProviderMock_IsOrganizationUnitExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsOrganizationUnitExists' +type OrganizationUnitProviderMock_IsOrganizationUnitExists_Call struct { + *mock.Call +} + +// IsOrganizationUnitExists is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *OrganizationUnitProviderMock_Expecter) IsOrganizationUnitExists(ctx interface{}, id interface{}) *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call { + return &OrganizationUnitProviderMock_IsOrganizationUnitExists_Call{Call: _e.mock.On("IsOrganizationUnitExists", ctx, id)} +} + +func (_c *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call) Run(run func(ctx context.Context, id string)) *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call) Return(b bool, serviceError *common.ServiceError) *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call { + _c.Call.Return(b, serviceError) + return _c +} + +func (_c *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call) RunAndReturn(run func(ctx context.Context, id string) (bool, *common.ServiceError)) *OrganizationUnitProviderMock_IsOrganizationUnitExists_Call { + _c.Call.Return(run) + return _c +} + +// IsParent provides a mock function for the type OrganizationUnitProviderMock +func (_mock *OrganizationUnitProviderMock) IsParent(ctx context.Context, parentID string, childID string) (bool, *common.ServiceError) { + ret := _mock.Called(ctx, parentID, childID) + + if len(ret) == 0 { + panic("no return value specified for IsParent") + } + + var r0 bool + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (bool, *common.ServiceError)); ok { + return returnFunc(ctx, parentID, childID) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { + r0 = returnFunc(ctx, parentID, childID) + } else { + r0 = ret.Get(0).(bool) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, parentID, childID) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// OrganizationUnitProviderMock_IsParent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsParent' +type OrganizationUnitProviderMock_IsParent_Call struct { + *mock.Call +} + +// IsParent is a helper method to define mock.On call +// - ctx context.Context +// - parentID string +// - childID string +func (_e *OrganizationUnitProviderMock_Expecter) IsParent(ctx interface{}, parentID interface{}, childID interface{}) *OrganizationUnitProviderMock_IsParent_Call { + return &OrganizationUnitProviderMock_IsParent_Call{Call: _e.mock.On("IsParent", ctx, parentID, childID)} +} + +func (_c *OrganizationUnitProviderMock_IsParent_Call) Run(run func(ctx context.Context, parentID string, childID string)) *OrganizationUnitProviderMock_IsParent_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *OrganizationUnitProviderMock_IsParent_Call) Return(b bool, serviceError *common.ServiceError) *OrganizationUnitProviderMock_IsParent_Call { + _c.Call.Return(b, serviceError) + return _c +} + +func (_c *OrganizationUnitProviderMock_IsParent_Call) RunAndReturn(run func(ctx context.Context, parentID string, childID string) (bool, *common.ServiceError)) *OrganizationUnitProviderMock_IsParent_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/mocks/resourceserverprovidermock/ResourceServerProvider_mock.go b/backend/tests/mocks/resourceserverprovidermock/ResourceServerProvider_mock.go new file mode 100644 index 0000000000..431b194cfb --- /dev/null +++ b/backend/tests/mocks/resourceserverprovidermock/ResourceServerProvider_mock.go @@ -0,0 +1,256 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package resourceserverprovidermock + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "github.com/thunder-id/thunderid/pkg/thunderidengine/common" + "github.com/thunder-id/thunderid/pkg/thunderidengine/providers" +) + +// NewResourceServerProviderMock creates a new instance of ResourceServerProviderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewResourceServerProviderMock(t interface { + mock.TestingT + Cleanup(func()) +}) *ResourceServerProviderMock { + mock := &ResourceServerProviderMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// ResourceServerProviderMock is an autogenerated mock type for the ResourceServerProvider type +type ResourceServerProviderMock struct { + mock.Mock +} + +type ResourceServerProviderMock_Expecter struct { + mock *mock.Mock +} + +func (_m *ResourceServerProviderMock) EXPECT() *ResourceServerProviderMock_Expecter { + return &ResourceServerProviderMock_Expecter{mock: &_m.Mock} +} + +// GetResourceServer provides a mock function for the type ResourceServerProviderMock +func (_mock *ResourceServerProviderMock) GetResourceServer(ctx context.Context, id string) (*providers.ResourceServer, *common.ServiceError) { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetResourceServer") + } + + var r0 *providers.ResourceServer + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*providers.ResourceServer, *common.ServiceError)); ok { + return returnFunc(ctx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *providers.ResourceServer); ok { + r0 = returnFunc(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providers.ResourceServer) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, id) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// ResourceServerProviderMock_GetResourceServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetResourceServer' +type ResourceServerProviderMock_GetResourceServer_Call struct { + *mock.Call +} + +// GetResourceServer is a helper method to define mock.On call +// - ctx context.Context +// - id string +func (_e *ResourceServerProviderMock_Expecter) GetResourceServer(ctx interface{}, id interface{}) *ResourceServerProviderMock_GetResourceServer_Call { + return &ResourceServerProviderMock_GetResourceServer_Call{Call: _e.mock.On("GetResourceServer", ctx, id)} +} + +func (_c *ResourceServerProviderMock_GetResourceServer_Call) Run(run func(ctx context.Context, id string)) *ResourceServerProviderMock_GetResourceServer_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *ResourceServerProviderMock_GetResourceServer_Call) Return(resourceServer *providers.ResourceServer, serviceError *common.ServiceError) *ResourceServerProviderMock_GetResourceServer_Call { + _c.Call.Return(resourceServer, serviceError) + return _c +} + +func (_c *ResourceServerProviderMock_GetResourceServer_Call) RunAndReturn(run func(ctx context.Context, id string) (*providers.ResourceServer, *common.ServiceError)) *ResourceServerProviderMock_GetResourceServer_Call { + _c.Call.Return(run) + return _c +} + +// GetResourceServerByIdentifier provides a mock function for the type ResourceServerProviderMock +func (_mock *ResourceServerProviderMock) GetResourceServerByIdentifier(ctx context.Context, identifier string) (*providers.ResourceServer, *common.ServiceError) { + ret := _mock.Called(ctx, identifier) + + if len(ret) == 0 { + panic("no return value specified for GetResourceServerByIdentifier") + } + + var r0 *providers.ResourceServer + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (*providers.ResourceServer, *common.ServiceError)); ok { + return returnFunc(ctx, identifier) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) *providers.ResourceServer); ok { + r0 = returnFunc(ctx, identifier) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*providers.ResourceServer) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) *common.ServiceError); ok { + r1 = returnFunc(ctx, identifier) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// ResourceServerProviderMock_GetResourceServerByIdentifier_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetResourceServerByIdentifier' +type ResourceServerProviderMock_GetResourceServerByIdentifier_Call struct { + *mock.Call +} + +// GetResourceServerByIdentifier is a helper method to define mock.On call +// - ctx context.Context +// - identifier string +func (_e *ResourceServerProviderMock_Expecter) GetResourceServerByIdentifier(ctx interface{}, identifier interface{}) *ResourceServerProviderMock_GetResourceServerByIdentifier_Call { + return &ResourceServerProviderMock_GetResourceServerByIdentifier_Call{Call: _e.mock.On("GetResourceServerByIdentifier", ctx, identifier)} +} + +func (_c *ResourceServerProviderMock_GetResourceServerByIdentifier_Call) Run(run func(ctx context.Context, identifier string)) *ResourceServerProviderMock_GetResourceServerByIdentifier_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *ResourceServerProviderMock_GetResourceServerByIdentifier_Call) Return(resourceServer *providers.ResourceServer, serviceError *common.ServiceError) *ResourceServerProviderMock_GetResourceServerByIdentifier_Call { + _c.Call.Return(resourceServer, serviceError) + return _c +} + +func (_c *ResourceServerProviderMock_GetResourceServerByIdentifier_Call) RunAndReturn(run func(ctx context.Context, identifier string) (*providers.ResourceServer, *common.ServiceError)) *ResourceServerProviderMock_GetResourceServerByIdentifier_Call { + _c.Call.Return(run) + return _c +} + +// ValidatePermissions provides a mock function for the type ResourceServerProviderMock +func (_mock *ResourceServerProviderMock) ValidatePermissions(ctx context.Context, resourceServerID string, permissions []string) ([]string, *common.ServiceError) { + ret := _mock.Called(ctx, resourceServerID, permissions) + + if len(ret) == 0 { + panic("no return value specified for ValidatePermissions") + } + + var r0 []string + var r1 *common.ServiceError + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string) ([]string, *common.ServiceError)); ok { + return returnFunc(ctx, resourceServerID, permissions) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string) []string); ok { + r0 = returnFunc(ctx, resourceServerID, permissions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string) *common.ServiceError); ok { + r1 = returnFunc(ctx, resourceServerID, permissions) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*common.ServiceError) + } + } + return r0, r1 +} + +// ResourceServerProviderMock_ValidatePermissions_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidatePermissions' +type ResourceServerProviderMock_ValidatePermissions_Call struct { + *mock.Call +} + +// ValidatePermissions is a helper method to define mock.On call +// - ctx context.Context +// - resourceServerID string +// - permissions []string +func (_e *ResourceServerProviderMock_Expecter) ValidatePermissions(ctx interface{}, resourceServerID interface{}, permissions interface{}) *ResourceServerProviderMock_ValidatePermissions_Call { + return &ResourceServerProviderMock_ValidatePermissions_Call{Call: _e.mock.On("ValidatePermissions", ctx, resourceServerID, permissions)} +} + +func (_c *ResourceServerProviderMock_ValidatePermissions_Call) Run(run func(ctx context.Context, resourceServerID string, permissions []string)) *ResourceServerProviderMock_ValidatePermissions_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []string + if args[2] != nil { + arg2 = args[2].([]string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *ResourceServerProviderMock_ValidatePermissions_Call) Return(strings []string, serviceError *common.ServiceError) *ResourceServerProviderMock_ValidatePermissions_Call { + _c.Call.Return(strings, serviceError) + return _c +} + +func (_c *ResourceServerProviderMock_ValidatePermissions_Call) RunAndReturn(run func(ctx context.Context, resourceServerID string, permissions []string) ([]string, *common.ServiceError)) *ResourceServerProviderMock_ValidatePermissions_Call { + _c.Call.Return(run) + return _c +} diff --git a/backend/tests/resources/deployment.yaml b/backend/tests/resources/deployment.yaml index c9baa78ea5..292f972cad 100644 --- a/backend/tests/resources/deployment.yaml +++ b/backend/tests/resources/deployment.yaml @@ -27,6 +27,12 @@ oauth: jwt: issuer: thunderid validity_period: 3600 + dcr: + enabled: true + logout: + enabled: true + token_revocation: + enabled: true flow: max_version_history: 3 diff --git a/docs/content/deployment/configuration.mdx b/docs/content/deployment/configuration.mdx index 033fe40180..5e03130c20 100644 --- a/docs/content/deployment/configuration.mdx +++ b/docs/content/deployment/configuration.mdx @@ -452,7 +452,11 @@ OAuth 2.0 and OpenID Connect settings. | `oauth.refresh_token.revoke_previous_on_renew` | `true` | If `true`, revokes the consumed refresh token after a successful renewal. Applies when `renew_on_grant` is enabled | | `oauth.refresh_token.validity_period` | `86400` | Refresh token validity period in seconds (24 hours) | | `oauth.authorization_code.validity_period` | `600` | Authorization code validity period in seconds (10 minutes) | +| `oauth.dcr.enabled` | `true` | If `true`, enables the Dynamic Client Registration endpoint | | `oauth.dcr.insecure` | `false` | If `true`, allows insecure dynamic client registration (development only) | +| `oauth.allowed_auth_methods` | `["client_secret_basic", "client_secret_post", "private_key_jwt", "none"]` | Client token endpoint authentication methods allowed during client registration | +| `oauth.allowed_response_types` | `["code"]` | OAuth response types allowed during client registration | +| `oauth.allowed_grant_types` | `["client_credentials", "authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange", "urn:openid:params:grant-type:ciba", "urn:ietf:params:oauth:grant-type:jwt-bearer"]` | OAuth grant types allowed during client registration | | `oauth.allow_wildcard_redirect_uri` | `false` | If `true`, allows wildcard patterns in registered redirect URIs: `*` and `**` in the path component, and `*` in the host component (label-internal, alphanumeric only). When `false`, only exact redirect URI matching is performed and registering a wildcard URI returns a `400 Bad Request` error. | :::note diff --git a/install/helm/conf/deployment.yaml b/install/helm/conf/deployment.yaml index 918c0b099b..01328263a3 100644 --- a/install/helm/conf/deployment.yaml +++ b/install/helm/conf/deployment.yaml @@ -245,7 +245,20 @@ oauth: authorization_code: validity_period: {{ .Values.configuration.oauth.authorizationCode.validityPeriod }} dcr: + enabled: {{ .Values.configuration.oauth.dcr.enabled }} insecure: {{ .Values.configuration.oauth.dcr.insecure }} + allowed_auth_methods: + {{- range .Values.configuration.oauth.allowedAuthMethods }} + - {{ . | quote }} + {{- end }} + allowed_response_types: + {{- range .Values.configuration.oauth.allowedResponseTypes }} + - {{ . | quote }} + {{- end }} + allowed_grant_types: + {{- range .Values.configuration.oauth.allowedGrantTypes }} + - {{ . | quote }} + {{- end }} flow: default_auth_flow_handle: {{ .Values.configuration.flow.defaultAuthFlowHandle | quote }} diff --git a/install/helm/values.yaml b/install/helm/values.yaml index c7171d0b70..6ffc65a940 100644 --- a/install/helm/values.yaml +++ b/install/helm/values.yaml @@ -416,7 +416,25 @@ configuration: authorizationCode: validityPeriod: 600 dcr: + enabled: true insecure: false + # Client token endpoint auth methods allowed during registration. + allowedAuthMethods: + - "client_secret_basic" + - "client_secret_post" + - "private_key_jwt" + - "none" + # OAuth response types allowed during client registration. + allowedResponseTypes: + - "code" + # OAuth grant types allowed during client registration. + allowedGrantTypes: + - "client_credentials" + - "authorization_code" + - "refresh_token" + - "urn:ietf:params:oauth:grant-type:token-exchange" + - "urn:openid:params:grant-type:ciba" + - "urn:ietf:params:oauth:grant-type:jwt-bearer" # Flow configuration flow: