Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions backend/.mockery.public.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion backend/cmd/server/config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"validity_period": 600
},
"dcr": {
"enabled" : true,
"insecure": false
},
"par": {
Expand All @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions backend/cmd/server/servicemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
37 changes: 28 additions & 9 deletions backend/internal/application/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 6 additions & 3 deletions backend/internal/application/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)}),
}
}
Expand Down
27 changes: 27 additions & 0 deletions backend/internal/application/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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}
Expand Down
65 changes: 53 additions & 12 deletions backend/internal/inboundclient/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)) &&
Expand Down Expand Up @@ -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 != ""
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Comment on lines +1070 to +1111

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add configured allow-list enforcement tests.

The supplied tests only initialize OAuthConfig{} with empty lists, so they do not verify that each non-empty allow-list accepts allowed values and rejects disabled grants, response types, and auth methods. Add focused table-driven coverage for both create and update validation paths. As per coding guidelines, “Write tests for new features and bug fixes, targeting at least 80% coverage.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/internal/inboundclient/service.go` around lines 1070 - 1111, Add
focused table-driven tests covering non-empty OAuth allow-lists for
validateWithAllowedGrantTypes, validateWithAllowedResponseTypes, and
validateAllowedTokenEndpointAuthMethod. Exercise both create and update
validation paths, asserting configured values are accepted and disabled grants,
response types, and auth methods are rejected, while preserving existing
empty-list coverage.

Source: Coding guidelines


// validatePublicClient validates constraints required for public clients.
func validatePublicClient(p *providers.OAuthProfile) error {
if providers.TokenEndpointAuthMethod(p.TokenEndpointAuthMethod) != providers.TokenEndpointAuthMethodNone {
Expand Down
Loading
Loading