@@ -11,6 +11,7 @@ import (
1111
1212 popslices "github.com/gobuffalo/pop/v6/slices"
1313 jwt "github.com/golang-jwt/jwt/v5"
14+ "github.com/gofrs/uuid"
1415 "github.com/stretchr/testify/assert"
1516 "github.com/stretchr/testify/require"
1617 "github.com/stretchr/testify/suite"
@@ -107,40 +108,68 @@ func (ts *CustomOAuthAdminTestSuite) TestCreateOAuth2Provider() {
107108 assert .Empty (ts .T (), provider .ClientSecret )
108109}
109110
110- func (ts * CustomOAuthAdminTestSuite ) TestCreateOIDCProvider () {
111- payload := map [ string ] interface {} {
112- "provider_type" : "oidc" ,
113- "identifier" : "custom:self-keycloak" ,
114- "name " : "Keycloak " ,
115- "client_id " : "test-client-id " ,
116- "client_secret " : "test-client-secret " ,
117- "issuer " : "https://example.com/realms/myrealm " ,
118- "scopes " : [] string { "profile" , "email" } ,
119- "pkce_enabled " : true ,
120- "enabled " : true ,
121- }
111+ func (ts * CustomOAuthAdminTestSuite ) TestCreateOIDCProviderValidatesDiscovery () {
112+ ts . Run ( "Unreachable issuer rejected" , func () {
113+ // An OIDC provider with an unresolvable issuer is caught by URL validation
114+ payload := map [ string ] interface {}{
115+ "provider_type " : "oidc " ,
116+ "identifier " : "custom:bad-issuer " ,
117+ "name " : "Bad Issuer " ,
118+ "client_id " : "test-client-id " ,
119+ "client_secret " : "test-client-secret" ,
120+ "issuer " : "https://unreachable.example.com/realms/myrealm" ,
121+ "scopes " : [] string { "profile" , "email" } ,
122+ }
122123
123- var body bytes.Buffer
124- require .NoError (ts .T (), json .NewEncoder (& body ).Encode (payload ))
124+ var body bytes.Buffer
125+ require .NoError (ts .T (), json .NewEncoder (& body ).Encode (payload ))
125126
126- req := httptest .NewRequest (http .MethodPost , "/admin/custom-providers" , & body )
127- req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , ts .token ))
127+ req := httptest .NewRequest (http .MethodPost , "/admin/custom-providers" , & body )
128+ req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , ts .token ))
128129
129- w := httptest .NewRecorder ()
130- ts .API .handler .ServeHTTP (w , req )
130+ w := httptest .NewRecorder ()
131+ ts .API .handler .ServeHTTP (w , req )
131132
132- require .Equal (ts .T (), http .StatusCreated , w .Code )
133+ require .Equal (ts .T (), http .StatusBadRequest , w .Code )
133134
134- var provider models.CustomOAuthProvider
135- require .NoError (ts .T (), json .NewDecoder (w .Body ).Decode (& provider ))
135+ var apiErr apierrors.HTTPError
136+ require .NoError (ts .T (), json .NewDecoder (w .Body ).Decode (& apiErr ))
137+ assert .Equal (ts .T (), apierrors .ErrorCodeValidationFailed , apiErr .ErrorCode )
138+ })
136139
137- assert .Equal (ts .T (), models .ProviderTypeOIDC , provider .ProviderType )
138- assert .Equal (ts .T (), "custom:self-keycloak" , provider .Identifier )
139- assert .Contains (ts .T (), provider .Scopes , "openid" ) // Auto-added for OIDC
140- assert .Contains (ts .T (), provider .Scopes , "profile" )
140+ ts .Run ("Invalid discovery document rejected" , func () {
141+ // Use a real resolvable domain whose discovery endpoint returns non-JSON.
142+ // This passes URL validation but fails at discovery fetch/parse.
143+ payload := map [string ]interface {}{
144+ "provider_type" : "oidc" ,
145+ "identifier" : "custom:bad-discovery" ,
146+ "name" : "Bad Discovery" ,
147+ "client_id" : "test-client-id" ,
148+ "client_secret" : "test-client-secret" ,
149+ "issuer" : "https://example.com" ,
150+ "scopes" : []string {"profile" , "email" },
151+ }
141152
142- // Ensure client secret is not exposed in JSON
143- assert .Empty (ts .T (), provider .ClientSecret )
153+ var body bytes.Buffer
154+ require .NoError (ts .T (), json .NewEncoder (& body ).Encode (payload ))
155+
156+ req := httptest .NewRequest (http .MethodPost , "/admin/custom-providers" , & body )
157+ req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , ts .token ))
158+
159+ w := httptest .NewRecorder ()
160+ ts .API .handler .ServeHTTP (w , req )
161+
162+ require .Equal (ts .T (), http .StatusBadRequest , w .Code )
163+
164+ var apiErr apierrors.HTTPError
165+ require .NoError (ts .T (), json .NewDecoder (w .Body ).Decode (& apiErr ))
166+ assert .Equal (ts .T (), apierrors .ErrorCodeValidationFailed , apiErr .ErrorCode )
167+ // Should fail at discovery fetch or parse stage
168+ assert .True (ts .T (),
169+ strings .Contains (apiErr .Message , "OIDC discovery" ) ||
170+ strings .Contains (apiErr .Message , "Failed to fetch" ),
171+ "Expected discovery-related error, got: %s" , apiErr .Message )
172+ })
144173}
145174
146175func (ts * CustomOAuthAdminTestSuite ) TestCreateProviderValidation () {
@@ -429,7 +458,7 @@ func (ts *CustomOAuthAdminTestSuite) TestListProviders() {
429458 // Create some providers
430459 ts .createProvider (ts .createTestOAuth2Payload ("oauth2-1" ), http .StatusCreated )
431460 ts .createProvider (ts .createTestOAuth2Payload ("oauth2-2" ), http .StatusCreated )
432- ts .createProvider ( ts . createTestOIDCPayload ( "oidc-1" , "https://oidc1.example.com" ), http . StatusCreated )
461+ ts .createOIDCProviderInDB ( "oidc-1" , "https://oidc1.example.com" )
433462
434463 req := httptest .NewRequest (http .MethodGet , "/admin/custom-providers" , nil )
435464 req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , ts .token ))
@@ -463,8 +492,8 @@ func (ts *CustomOAuthAdminTestSuite) TestListProvidersEmptyReturnsArray() {
463492func (ts * CustomOAuthAdminTestSuite ) TestListProvidersWithTypeFilter () {
464493 // Create mixed providers
465494 ts .createProvider (ts .createTestOAuth2Payload ("oauth2-1" ), http .StatusCreated )
466- ts .createProvider ( ts . createTestOIDCPayload ( "oidc-1" , "https://oidc1.example.com" ), http . StatusCreated )
467- ts .createProvider ( ts . createTestOIDCPayload ( "oidc-2" , "https://oidc2.example.com" ), http . StatusCreated )
495+ ts .createOIDCProviderInDB ( "oidc-1" , "https://oidc1.example.com" )
496+ ts .createOIDCProviderInDB ( "oidc-2" , "https://oidc2.example.com" )
468497
469498 // Filter by OAuth2
470499 req := httptest .NewRequest (http .MethodGet , "/admin/custom-providers?type=oauth2" , nil )
@@ -617,25 +646,31 @@ func (ts *CustomOAuthAdminTestSuite) createTestOAuth2Payload(identifier string)
617646 }
618647}
619648
620- func (ts * CustomOAuthAdminTestSuite ) createTestOIDCPayload (identifier , issuer string ) map [string ]interface {} {
649+ // createOIDCProviderInDB inserts an OIDC provider directly into the database,
650+ // bypassing the admin handler (and its discovery validation). Use this for tests
651+ // that need an OIDC provider to exist but aren't testing the create flow.
652+ func (ts * CustomOAuthAdminTestSuite ) createOIDCProviderInDB (identifier , issuer string ) * models.CustomOAuthProvider {
621653 if ! strings .HasPrefix (identifier , "custom:" ) {
622654 identifier = "custom:" + identifier
623655 }
624- // If issuer is not provided or uses non-resolvable domain, use example.com
625- if issuer == "" || strings . Contains ( issuer , "oidc1.example.com" ) || strings . Contains ( issuer , "oidc2.example.com" ) {
626- issuer = "https://example.com/realms/" + identifier
627- }
628- return map [ string ] interface {}{
629- "provider_type" : "oidc" ,
630- "identifier" : identifier ,
631- "name" : "Test OIDC Provider" ,
632- "client_id" : "test-client-id" ,
633- "client_secret" : "test-client-secret" ,
634- "issuer" : issuer ,
635- "scopes" : []string {"profile" , "email" },
636- "pkce_enabled" : true ,
637- "enabled" : true ,
656+ id , err := uuid . NewV4 ()
657+ require . NoError ( ts . T (), err )
658+
659+ provider := & models. CustomOAuthProvider {
660+ ID : id ,
661+ ProviderType : models . ProviderTypeOIDC ,
662+ Identifier : identifier ,
663+ Name : "Test OIDC Provider" ,
664+ ClientID : "test-client-id" ,
665+ ClientSecret : "test-client-secret" ,
666+ Issuer : & issuer ,
667+ Scopes : []string {"openid" , "profile" , "email" },
668+ PKCEEnabled : true ,
669+ Enabled : true ,
638670 }
671+
672+ require .NoError (ts .T (), models .CreateCustomOAuthProvider (ts .API .db , provider ))
673+ return provider
639674}
640675
641676func (ts * CustomOAuthAdminTestSuite ) createProvider (payload map [string ]interface {}, expectedStatus int ) * httptest.ResponseRecorder {
0 commit comments