diff --git a/go.mod b/go.mod index 58979803f..13c405c33 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ go 1.27.0 toolchain go1.27.1 require ( - authelia.com/provider/oauth2 v0.3.2 + authelia.com/provider/oauth2 v0.3.3 cel.dev/cel-go v0.32.0 github.com/asaskevich/govalidator/v12 v12.0.0 github.com/authelia/jsonschema v0.1.7 diff --git a/go.sum b/go.sum index a55458087..ce1616857 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -authelia.com/provider/oauth2 v0.3.2 h1:wjzScV0/VcVEQyFFScdB0Q/3QN6KRVSchXcVS/sRzBU= -authelia.com/provider/oauth2 v0.3.2/go.mod h1:p9dDQbHomYStjCxSV39O7NAMgRv2WWTMpU1b2Y/1dPA= +authelia.com/provider/oauth2 v0.3.3 h1:XKe7bm/2bxMxGoe8W1OpOBmplj+FThZuglj6yqQWcQk= +authelia.com/provider/oauth2 v0.3.3/go.mod h1:p9dDQbHomYStjCxSV39O7NAMgRv2WWTMpU1b2Y/1dPA= cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= diff --git a/internal/oidc/config.go b/internal/oidc/config.go index d3f618d9e..85a4e8c0a 100644 --- a/internal/oidc/config.go +++ b/internal/oidc/config.go @@ -95,6 +95,7 @@ type Config struct { SendDebugMessagesToClients bool DisableRefreshTokenValidation bool + DisableRefreshTokenRotation bool OmitRedirectScopeParameter bool DPoPEnabled bool @@ -109,11 +110,15 @@ type Config struct { EnforceClientAssertionIssuerAudience bool + RequireRequestObjectAudienceAndLifetime bool + RequestObjectMaximumLifetime time.Duration + BackChannelLogoutLifespan time.Duration BackChannelLogoutConcurrency int JWTScopeField jwt.JWTScopeFieldEnum JWTMaxDuration time.Duration + JWTClockSkew time.Duration JWTSecuredAuthorizationLifespan time.Duration @@ -202,9 +207,10 @@ type JWTAccessTokenConfig struct { // PARConfig holds specific oauthelia2.Configurator information for Pushed Authorization Requests. type PARConfig struct { - Require bool - URIPrefix string - ContextLifespan time.Duration + Require bool + RequireRedirectURI bool + URIPrefix string + ContextLifespan time.Duration } // IssuersConfig holds specific oauthelia2.Configurator information for the issuer. @@ -546,6 +552,19 @@ func (c *Config) GetJWTMaxDuration(ctx context.Context) (duration time.Duration) return c.JWTMaxDuration } +// GetJWTClockSkew returns how far into the future an 'iat' or 'nbf' claim may be in a JWT received from a client. +// Defaults to 10 seconds when zero, is disabled when negative, and is capped at 60 seconds. +func (c *Config) GetJWTClockSkew(ctx context.Context) (skew time.Duration) { + switch { + case c.JWTClockSkew == 0: + return jwtClockSkewDefault + case c.JWTClockSkew < 0: + return 0 + default: + return min(c.JWTClockSkew, jwtClockSkewMaximum) + } +} + // GetRedirectSecureChecker returns the redirect URL security validator. func (c *Config) GetRedirectSecureChecker(ctx context.Context) func(context.Context, *url.URL) (secure bool) { return oauthelia2.IsRedirectURISecure @@ -613,6 +632,11 @@ func (c *Config) GetDisableRefreshTokenValidation(ctx context.Context) (disable return c.DisableRefreshTokenValidation } +// GetDisableRefreshTokenRotation returns the disable refresh token rotation flag. +func (c *Config) GetDisableRefreshTokenRotation(ctx context.Context) (disable bool) { + return c.DisableRefreshTokenRotation +} + // GetJWTSecuredAuthorizeResponseModeLifespan returns the configured JWT Secured Authorization lifespan. func (c *Config) GetJWTSecuredAuthorizeResponseModeLifespan(ctx context.Context) (lifespan time.Duration) { if c.JWTSecuredAuthorizationLifespan.Seconds() <= 0 { @@ -885,6 +909,12 @@ func (c *Config) GetRequirePushedAuthorizationRequests(ctx context.Context) (enf return c.PAR.Require } +// GetRequireRedirectURIPushedAuthorizationRequests returns true if the 'redirect_uri' parameter is required in Pushed +// Authorization Requests even when the client has a single registered redirect URI. +func (c *Config) GetRequireRedirectURIPushedAuthorizationRequests(ctx context.Context) (require bool) { + return c.PAR.RequireRedirectURI +} + // GetResponseModeHandlers returns the response mode handlers. func (c *Config) GetResponseModeHandlers(ctx context.Context) oauthelia2.ResponseModeHandlers { return c.Handlers.ResponseMode @@ -1114,6 +1144,25 @@ func (c *Config) GetRequireSignedRequestObjectSkipPushedAuthorizationRequests(ct return false } +// GetRequireRequestObjectAudienceAndLifetime returns true if a Request Object must contain the 'aud', 'nbf' and 'exp' +// claims with a lifetime bounded by GetRequestObjectMaximumLifetime. +func (c *Config) GetRequireRequestObjectAudienceAndLifetime(ctx context.Context) (require bool) { + return c.RequireRequestObjectAudienceAndLifetime +} + +// GetRequestObjectMaximumLifetime returns the bound applied to a Request Object's 'nbf' and 'exp' claims. Defaults to +// 60 minutes when zero, and is disabled when negative. +func (c *Config) GetRequestObjectMaximumLifetime(ctx context.Context) (lifetime time.Duration) { + switch { + case c.RequestObjectMaximumLifetime == 0: + return lifespanRequestObjectMaximumDefault + case c.RequestObjectMaximumLifetime < 0: + return 0 + default: + return c.RequestObjectMaximumLifetime + } +} + // GetIDTokenValidationStrategy returns the ID Token validation strategy used by RP-Initiated Logout. It has no // default and may be nil. func (c *Config) GetIDTokenValidationStrategy(ctx context.Context) (strategy oauthelia2.TokenValidationStrategy) { diff --git a/internal/oidc/config_test.go b/internal/oidc/config_test.go index 73c383b83..7703028c0 100644 --- a/internal/oidc/config_test.go +++ b/internal/oidc/config_test.go @@ -114,6 +114,25 @@ func TestConfig_Durations(t *testing.T) { assert.Equal(t, time.Duration(0), config.Lifespans.AuthorizeCode) assert.Equal(t, time.Minute*15, config.GetAuthorizeCodeLifespan(ctx)) assert.Equal(t, time.Minute*15, config.Lifespans.AuthorizeCode) + + assert.Equal(t, time.Second*10, config.GetJWTClockSkew(ctx)) + + config.JWTClockSkew = time.Second * 30 + assert.Equal(t, time.Second*30, config.GetJWTClockSkew(ctx)) + + config.JWTClockSkew = time.Hour + assert.Equal(t, time.Minute, config.GetJWTClockSkew(ctx)) + + config.JWTClockSkew = -1 + assert.Equal(t, time.Duration(0), config.GetJWTClockSkew(ctx)) + + assert.Equal(t, time.Hour, config.GetRequestObjectMaximumLifetime(ctx)) + + config.RequestObjectMaximumLifetime = time.Minute * 30 + assert.Equal(t, time.Minute*30, config.GetRequestObjectMaximumLifetime(ctx)) + + config.RequestObjectMaximumLifetime = -1 + assert.Equal(t, time.Duration(0), config.GetRequestObjectMaximumLifetime(ctx)) } func TestConfig_GetTokenEntropy(t *testing.T) { @@ -134,6 +153,16 @@ func TestConfig_Misc(t *testing.T) { assert.False(t, config.DisableRefreshTokenValidation) assert.False(t, config.GetDisableRefreshTokenValidation(ctx)) + assert.False(t, config.GetDisableRefreshTokenRotation(ctx)) + + config.DisableRefreshTokenRotation = true + assert.True(t, config.GetDisableRefreshTokenRotation(ctx)) + + assert.False(t, config.GetRequireRequestObjectAudienceAndLifetime(ctx)) + + config.RequireRequestObjectAudienceAndLifetime = true + assert.True(t, config.GetRequireRequestObjectAudienceAndLifetime(ctx)) + assert.Equal(t, "", config.Issuers.AccessToken) assert.Equal(t, "", config.GetAccessTokenIssuer(ctx)) @@ -239,6 +268,12 @@ func TestConfig_PAR(t *testing.T) { assert.True(t, config.GetRequirePushedAuthorizationRequests(ctx)) + assert.False(t, config.GetRequireRedirectURIPushedAuthorizationRequests(ctx)) + + config.PAR.RequireRedirectURI = true + + assert.True(t, config.GetRequireRedirectURIPushedAuthorizationRequests(ctx)) + assert.Equal(t, time.Duration(0), config.PAR.ContextLifespan) assert.Equal(t, time.Minute*5, config.GetPushedAuthorizeContextLifespan(ctx)) assert.Equal(t, time.Minute*5, config.PAR.ContextLifespan) diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 04b303cca..8ff36f0f4 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -96,6 +96,9 @@ const ( lifespanRFC8628CodeDefault = time.Minute * 10 lifespanRFC8628PollingIntervalDefault = time.Second * 10 lifespanVerifiableCredentialsNonceDefault = time.Hour + lifespanRequestObjectMaximumDefault = time.Hour + jwtClockSkewDefault = time.Second * 10 + jwtClockSkewMaximum = time.Minute lifespanDPoPProofDefault = time.Second * 10 lifespanBackChannelLogoutDefault = time.Minute * 5 )