diff --git a/.env.example b/.env.example index 95fad330..b5e5f845 100644 --- a/.env.example +++ b/.env.example @@ -196,6 +196,8 @@ TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRET= TINYAUTH_OIDC_CLIENTS_name_CLIENTSECRETFILE= # List of trusted redirect URIs. TINYAUTH_OIDC_CLIENTS_name_TRUSTEDREDIRECTURIS= +# Skip the consent screen for this trusted OIDC client. +TINYAUTH_OIDC_CLIENTS_name_TRUSTED=false # Client name in UI. TINYAUTH_OIDC_CLIENTS_name_NAME= diff --git a/internal/controller/oidc_controller.go b/internal/controller/oidc_controller.go index a4b2d76d..580b5874 100644 --- a/internal/controller/oidc_controller.go +++ b/internal/controller/oidc_controller.go @@ -327,6 +327,14 @@ func (controller *OIDCController) skipConsent(c *gin.Context) { return } + client, ok := controller.oidc.GetClient(authorizeReq.ClientID) + if ok && client.Trusted { + c.JSON(200, SkipConsentResponse{ + SkipConsent: true, + }) + return + } + consent, err := controller.oidc.GetOIDCConsent(c, userContext.GetUsername(), authorizeReq.ClientID) if err != nil || consent == nil { diff --git a/internal/controller/oidc_controller_test.go b/internal/controller/oidc_controller_test.go index e3da603b..b3f6a295 100644 --- a/internal/controller/oidc_controller_test.go +++ b/internal/controller/oidc_controller_test.go @@ -351,6 +351,33 @@ func TestOIDCController(t *testing.T) { assert.False(t, res.SkipConsent) }, }, + { + description: "Skip consent returns true for a trusted client without prior consent", + middlewares: []gin.HandlerFunc{authedUser}, + run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { + require.NoError(t, store.DeleteOIDCConsentByClientID(ctx, "trusted-client-id")) + + ticket := oidcService.CreateAuthorizeRequestTicket(service.AuthorizeRequest{ + Scope: "openid profile", + ResponseType: "code", + ClientID: "trusted-client-id", + RedirectURI: "https://trusted.example.com/callback", + }) + + req := httptest.NewRequest("GET", "/api/oidc/skip-consent?oidc_ticket="+url.QueryEscape(ticket), nil) + router.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var res SkipConsentResponse + require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &res)) + assert.True(t, res.SkipConsent) + + // skip-consent is only a check; it must not consume the ticket. + _, ok := oidcService.GetAuthorizeRequestByTicket(ticket) + assert.True(t, ok) + }, + }, { description: "Skip consent returns false when a new scope is requested", middlewares: []gin.HandlerFunc{authedUser}, diff --git a/internal/model/config.go b/internal/model/config.go index 619e061f..e02cc863 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -288,6 +288,7 @@ type OIDCClientConfig struct { ClientSecret string `description:"OIDC client secret." yaml:"clientSecret,omitempty"` ClientSecretFile string `description:"Path to the file containing the OIDC client secret." yaml:"clientSecretFile,omitempty"` TrustedRedirectURIs []string `description:"List of trusted redirect URIs." yaml:"trustedRedirectUris,omitempty"` + Trusted bool `description:"Skip the consent screen for this trusted OIDC client." yaml:"trusted,omitempty"` Name string `description:"Client name in UI." yaml:"name,omitempty"` } diff --git a/internal/test/test.go b/internal/test/test.go index 45daf2a1..6ff41a88 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -32,6 +32,13 @@ func CreateTestConfigs(t *testing.T) (model.Config, model.RuntimeConfig) { TrustedRedirectURIs: []string{"https://test.example.com/callback"}, Name: "Test Client", }, + "trusted-test": { + ClientID: "trusted-client-id", + ClientSecret: "trusted-client-secret", + TrustedRedirectURIs: []string{"https://trusted.example.com/callback"}, + Trusted: true, + Name: "Trusted Test Client", + }, }, PrivateKeyPath: filepath.Join(tempDir, "key.pem"), PublicKeyPath: filepath.Join(tempDir, "key.pub"),