Skip to content
Open
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand Down
8 changes: 8 additions & 0 deletions internal/controller/oidc_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions internal/controller/oidc_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
1 change: 1 addition & 0 deletions internal/model/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
7 changes: 7 additions & 0 deletions internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading