diff --git a/.env.example b/.env.example index bfdc357d..3136a89b 100644 --- a/.env.example +++ b/.env.example @@ -194,6 +194,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= @@ -222,8 +224,10 @@ TINYAUTH_LDAP_BINDPASSWORDFILE= TINYAUTH_LDAP_BASEDN= # Allow insecure LDAP connections. TINYAUTH_LDAP_INSECURE=false -# LDAP search filter. +# LDAP user search filter. Use %s as the username placeholder. TINYAUTH_LDAP_SEARCHFILTER="(uid=%s)" +# LDAP group search filter. Use %s as the user DN placeholder. +TINYAUTH_LDAP_GROUPSEARCHFILTER="(&(objectclass=groupOfUniqueNames)(uniquemember=%s))" # Certificate for mTLS authentication. TINYAUTH_LDAP_AUTHCERT= # Certificate key for mTLS authentication. 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..f95113b0 100644 --- a/internal/controller/oidc_controller_test.go +++ b/internal/controller/oidc_controller_test.go @@ -351,6 +351,29 @@ 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) + }, + }, { 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 24ba6302..bf544444 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -57,9 +57,10 @@ func NewDefaultConfiguration(runtimeEnv RuntimeEnv) *Config { WarningsEnabled: true, }, LDAP: LDAPConfig{ - Insecure: false, - SearchFilter: "(uid=%s)", - GroupCacheTTL: 900, // 15 minutes + Insecure: false, + SearchFilter: "(uid=%s)", + GroupSearchFilter: "(&(objectclass=groupOfUniqueNames)(uniquemember=%s))", + GroupCacheTTL: 900, // 15 minutes }, Log: LogConfig{ Level: "info", @@ -209,16 +210,17 @@ type UIConfig struct { } type LDAPConfig struct { - Address string `description:"LDAP server address." yaml:"address,omitempty"` - BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn,omitempty"` - BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword,omitempty"` - BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile,omitempty"` - BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn,omitempty"` - Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure,omitempty"` - SearchFilter string `description:"LDAP search filter." yaml:"searchFilter,omitempty"` - AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert,omitempty"` - AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey,omitempty"` - GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL,omitempty"` + Address string `description:"LDAP server address." yaml:"address,omitempty"` + BindDN string `description:"Bind DN for LDAP authentication." yaml:"bindDn,omitempty"` + BindPassword string `description:"Bind password for LDAP authentication." yaml:"bindPassword,omitempty"` + BindPasswordFile string `description:"Path to the Bind password." yaml:"bindPasswordFile,omitempty"` + BaseDN string `description:"Base DN for LDAP searches." yaml:"baseDn,omitempty"` + Insecure bool `description:"Allow insecure LDAP connections." yaml:"insecure,omitempty"` + SearchFilter string `description:"LDAP user search filter. Use %s as the username placeholder." yaml:"searchFilter,omitempty"` + GroupSearchFilter string `description:"LDAP group search filter. Use %s as the user DN placeholder." yaml:"groupSearchFilter,omitempty"` + AuthCert string `description:"Certificate for mTLS authentication." yaml:"authCert,omitempty"` + AuthKey string `description:"Certificate key for mTLS authentication." yaml:"authKey,omitempty"` + GroupCacheTTL int `description:"Cache duration for LDAP group membership in seconds." yaml:"groupCacheTTL,omitempty"` } type LogConfig struct { @@ -282,6 +284,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/service/ldap_service.go b/internal/service/ldap_service.go index 2a5a5adb..6ded3aab 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -200,7 +200,7 @@ func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) { searchRequest := ldapgo.NewSearchRequest( ldap.config.LDAP.BaseDN, ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, - fmt.Sprintf("(&(objectclass=groupOfUniqueNames)(uniquemember=%s))", escapedUserDN), + fmt.Sprintf(ldap.config.LDAP.GroupSearchFilter, escapedUserDN), []string{"dn"}, nil, ) 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"),