summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJames Elliott <james-d-elliott@users.noreply.github.com>2025-02-22 18:25:42 +1100
committerGitHub <noreply@github.com>2025-02-22 07:25:42 +0000
commit9c718b39888bbaafdbc623acd0efd2138b6b8068 (patch)
treee189c54c06912763952eeb0ab081466531bd1cb8 /internal
parentf67097c6cb7fe14ccac071b37d6323e17b377506 (diff)
feat(oidc): prompt parameter support (#8080)
This adds formal support for the prompt parameter. Closes #2596 Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/handlers/handler_firstfactor.go125
-rw-r--r--internal/handlers/handler_firstfactor_test.go1236
-rw-r--r--internal/handlers/handler_oidc_authorization.go34
-rw-r--r--internal/handlers/handler_oidc_authorization_consent.go74
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_explicit.go6
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_implicit.go38
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_pre_configured.go20
-rw-r--r--internal/handlers/handler_oidc_authorization_consent_test.go1404
-rw-r--r--internal/handlers/handler_oidc_consent.go2
-rw-r--r--internal/handlers/handler_sign_duo.go2
-rw-r--r--internal/handlers/handler_sign_totp.go2
-rw-r--r--internal/handlers/handler_sign_webauthn.go2
-rw-r--r--internal/handlers/response.go105
-rw-r--r--internal/handlers/types.go11
-rw-r--r--internal/mocks/authelia_ctx.go51
-rw-r--r--internal/mocks/storage.go6
-rw-r--r--internal/oidc/const.go6
-rw-r--r--internal/oidc/session.go4
-rw-r--r--internal/oidc/types.go3
-rw-r--r--internal/oidc/util.go40
-rw-r--r--internal/oidc/util_blackbox_test.go109
-rw-r--r--internal/server/handlers.go1
-rw-r--r--internal/session/user_session.go29
-rw-r--r--internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.down.sql5
-rw-r--r--internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.up.sql2
-rw-r--r--internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.down.sql6
-rw-r--r--internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.up.sql3
-rw-r--r--internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.down.sql382
-rw-r--r--internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.up.sql381
-rw-r--r--internal/storage/migrations_test.go2
-rw-r--r--internal/storage/provider.go8
-rw-r--r--internal/storage/sql_provider.go12
32 files changed, 3945 insertions, 166 deletions
diff --git a/internal/handlers/handler_firstfactor.go b/internal/handlers/handler_firstfactor.go
index 12269cad4..bdf51bf34 100644
--- a/internal/handlers/handler_firstfactor.go
+++ b/internal/handlers/handler_firstfactor.go
@@ -153,7 +153,130 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
successful = true
if bodyJSON.Workflow == workflowOpenIDConnect {
- handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.TargetURL, bodyJSON.WorkflowID)
+ handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.WorkflowID)
+ } else {
+ Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
+ }
+ }
+}
+
+// FirstFactorReauthenticatePOST is a specialized handler which checks the currently logged in users current password
+// and updates their last authenticated time.
+func FirstFactorReauthenticatePOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.RequestHandler {
+ return func(ctx *middlewares.AutheliaCtx) {
+ var successful bool
+
+ requestTime := time.Now()
+
+ if delayFunc != nil {
+ defer delayFunc(ctx, requestTime, &successful)
+ }
+
+ bodyJSON := bodyFirstFactorReauthenticateRequest{}
+
+ if err := ctx.ParseBody(&bodyJSON); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrParseRequestBody, regulation.AuthType1FA)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ provider, err := ctx.GetSessionProvider()
+ if err != nil {
+ ctx.Logger.WithError(err).Error("Failed to get session provider during 1FA attempt")
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ userSession, err := provider.GetSession(ctx.RequestCtx)
+ if err != nil {
+ ctx.Logger.WithError(err).Errorf("Error occurred attempting to load session.")
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if bannedUntil, err := ctx.Providers.Regulator.Regulate(ctx, userSession.Username); err != nil {
+ if errors.Is(err, regulation.ErrUserIsBanned) {
+ _ = markAuthenticationAttempt(ctx, false, &bannedUntil, userSession.Username, regulation.AuthType1FA, nil)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ ctx.Logger.WithError(err).Errorf(logFmtErrRegulationFail, regulation.AuthType1FA, userSession.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(userSession.Username, bodyJSON.Password)
+ if err != nil {
+ _ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthType1FA, err)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if !userPasswordOk {
+ _ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthType1FA, nil)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthType1FA, nil); err != nil {
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ if err = ctx.RegenerateSession(); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrSessionRegenerate, regulation.AuthType1FA, userSession.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ // Get the details of the given user from the user provider.
+ userDetails, err := ctx.Providers.UserProvider.GetDetails(userSession.Username)
+ if err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrObtainProfileDetails, regulation.AuthType1FA, userSession.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ ctx.Logger.Tracef(logFmtTraceProfileDetails, userSession.Username, userDetails.Groups, userDetails.Emails)
+
+ userSession.SetOneFactorReauthenticate(ctx.Clock.Now(), userDetails)
+
+ if ctx.Configuration.AuthenticationBackend.RefreshInterval.Update() {
+ userSession.RefreshTTL = ctx.Clock.Now().Add(ctx.Configuration.AuthenticationBackend.RefreshInterval.Value())
+ }
+
+ if err = ctx.SaveSession(userSession); err != nil {
+ ctx.Logger.WithError(err).Errorf(logFmtErrSessionSave, "updated profile", regulation.AuthType1FA, logFmtActionAuthentication, userSession.Username)
+
+ respondUnauthorized(ctx, messageAuthenticationFailed)
+
+ return
+ }
+
+ successful = true
+
+ if bodyJSON.Workflow == workflowOpenIDConnect {
+ handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.WorkflowID)
} else {
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
}
diff --git a/internal/handlers/handler_firstfactor_test.go b/internal/handlers/handler_firstfactor_test.go
index 1ab273318..492b27274 100644
--- a/internal/handlers/handler_firstfactor_test.go
+++ b/internal/handlers/handler_firstfactor_test.go
@@ -1,10 +1,13 @@
package handlers
import (
+ "database/sql"
"fmt"
"net/url"
"testing"
+ "time"
+ "github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
@@ -15,6 +18,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model"
+ "github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/regulation"
)
@@ -36,7 +40,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsNil() {
FirstFactorPOST(nil)(s.mock.Ctx)
// No body.
- AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Failed to parse 1FA request body", "unable to parse body: unexpected end of JSON input")
+ s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to parse body: unexpected end of JSON input")
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
}
@@ -47,7 +51,7 @@ func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() {
}`)
FirstFactorPOST(nil)(s.mock.Ctx)
- AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Failed to parse 1FA request body", "unable to validate body: password: non zero value required")
+ s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to validate body: password: non zero value required")
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
}
@@ -59,13 +63,13 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(false, fmt.Errorf("failed"))
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
- Username: "test",
+ Username: testValue,
Successful: false,
Banned: false,
Time: s.mock.Clock.Now(),
@@ -80,7 +84,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
}`)
FirstFactorPOST(nil)(s.mock.Ctx)
- AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Unsuccessful 1FA authentication attempt by user 'test'", "failed")
+ s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test'", "failed")
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
}
@@ -93,13 +97,13 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderC
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(false, fmt.Errorf("invalid credentials"))
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
- Username: "test",
+ Username: testValue,
Successful: false,
Banned: false,
Time: s.mock.Clock.Now(),
@@ -116,6 +120,76 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderC
FirstFactorPOST(nil)(s.mock.Ctx)
}
+func (s *FirstFactorSuite) TestShouldCheckUserNotBanned() {
+ s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2}, s.mock.StorageMock, &s.mock.Clock)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "keepMeLoggedIn": true
+ }`)
+
+ gomock.InOrder(
+ s.mock.UserProviderMock.EXPECT().GetDetails(testValue).Return(&authentication.UserDetails{Username: testValue}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(nil, nil),
+
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(false, fmt.Errorf("invalid credentials")),
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: false,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ })))
+ FirstFactorPOST(nil)(s.mock.Ctx)
+}
+
+func (s *FirstFactorSuite) TestShouldCheckBannedUser() {
+ s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2, FindTime: time.Hour, BanTime: time.Hour}, s.mock.StorageMock, &s.mock.Clock)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "keepMeLoggedIn": true
+ }`)
+
+ gomock.InOrder(
+ s.mock.UserProviderMock.EXPECT().GetDetails(testValue).Return(&authentication.UserDetails{Username: testValue}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()).
+ Return([]model.AuthenticationAttempt{
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ }, nil),
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: true,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ })))
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test' and they are banned until 2013-02-03 00:59:59 +0000 UTC", "")
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() {
s.mock.UserProviderMock.
EXPECT().
@@ -124,13 +198,13 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCrede
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(false, nil)
s.mock.StorageMock.
EXPECT().
AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
- Username: "test",
+ Username: testValue,
Successful: false,
Banned: false,
Time: s.mock.Clock.Now(),
@@ -150,7 +224,7 @@ func (s *FirstFactorSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCrede
func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
s.mock.UserProviderMock.
EXPECT().
- GetDetails(gomock.Eq("test")).
+ GetDetails(gomock.Eq(testValue)).
Return(nil, fmt.Errorf("failed"))
s.mock.Ctx.Request.SetBodyString(`{
@@ -158,6 +232,7 @@ func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
"password": "hello",
"keepMeLoggedIn": true
}`)
+
FirstFactorPOST(nil)(s.mock.Ctx)
AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Error occurred getting details for user with username input 'test' which usually indicates they do not exist", "failed")
@@ -172,7 +247,7 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() {
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(true, nil)
s.mock.StorageMock.
@@ -187,21 +262,21 @@ func (s *FirstFactorSuite) TestShouldFailIfAuthenticationMarkFail() {
}`)
FirstFactorPOST(nil)(s.mock.Ctx)
- AssertLogEntryMessageAndError(s.T(), s.mock.Hook.LastEntry(), "Unable to mark 1FA authentication attempt by user 'test'", "failed")
+ s.mock.AssertLastLogMessage(s.T(), "Unable to mark 1FA authentication attempt by user 'test'", "failed")
s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
}
func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(true, nil)
s.mock.UserProviderMock.
EXPECT().
- GetDetails(gomock.Eq("test")).
+ GetDetails(gomock.Eq(testValue)).
Return(&authentication.UserDetails{
- Username: "test",
+ Username: testValue,
Emails: []string{"test@example.com"},
Groups: []string{"dev", "admins"},
}, nil)
@@ -225,7 +300,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
- assert.Equal(s.T(), "test", userSession.Username)
+ assert.Equal(s.T(), testValue, userSession.Username)
assert.Equal(s.T(), true, userSession.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails)
@@ -235,14 +310,14 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeChecked() {
func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(true, nil)
s.mock.UserProviderMock.
EXPECT().
- GetDetails(gomock.Eq("test")).
+ GetDetails(gomock.Eq(testValue)).
Return(&authentication.UserDetails{
- Username: "test",
+ Username: testValue,
Emails: []string{"test@example.com"},
Groups: []string{"dev", "admins"},
}, nil)
@@ -267,7 +342,7 @@ func (s *FirstFactorSuite) TestShouldAuthenticateUserWithRememberMeUnchecked() {
userSession, err := s.mock.Ctx.GetSession()
s.Assert().NoError(err)
- assert.Equal(s.T(), "test", userSession.Username)
+ assert.Equal(s.T(), testValue, userSession.Username)
assert.Equal(s.T(), false, userSession.KeepMeLoggedIn)
assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel)
assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails)
@@ -319,7 +394,7 @@ func (s *FirstFactorSuite) TestShouldSaveUsernameFromAuthenticationBackendInSess
s.mock.UserProviderMock.
EXPECT().
- GetDetails(gomock.Eq("test")).
+ GetDetails(gomock.Eq(testValue)).
Return(&authentication.UserDetails{
// This is the name in authentication backend, in some setups the binding is
// case insensitive but the user ID in session must match the user in LDAP
@@ -376,14 +451,14 @@ func (s *FirstFactorRedirectionSuite) SetupTest() {
s.mock.UserProviderMock.
EXPECT().
- CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
Return(true, nil)
s.mock.UserProviderMock.
EXPECT().
- GetDetails(gomock.Eq("test")).
+ GetDetails(gomock.Eq(testValue)).
Return(&authentication.UserDetails{
- Username: "test",
+ Username: testValue,
Emails: []string{"test@example.com"},
Groups: []string{"dev", "admins"},
}, nil)
@@ -505,7 +580,1120 @@ func (s *FirstFactorRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvi
s.mock.Assert200OK(s.T(), nil)
}
+func (s *FirstFactorRedirectionSuite) TestShouldReplyWhenBadTargetURL() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "targetURL": "#https://23kjnm412jk3"
+ }`)
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyTwoFactorOK() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "targetURL": "https://two-factor.example.com"
+ }`)
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), nil)
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyTwoTwoFactorUnsafe() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "targetURL": "https://unsafe-domain.com"
+ }`)
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), nil)
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyTwoTwoFactorSafe() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "targetURL": "https://test.example.com"
+ }`)
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://test.example.com"})
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectCantParseUUID() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "aaaaaaaaaaaaaaaaaaaaaaaaaaa-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to parse consent session challenge id 'aaaaaaaaaaaaaaaaaaaaaaaaaaa-9107-4067-8d31-407ca59eb69c': invalid UUID length: 55", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectCantGetConsentSession() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(nil, fmt.Errorf("failed to obtain")),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to load consent session by challenge id 'd1ba0ad8-9107-4067-8d31-407ca59eb69c': failed to obtain", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectConsentSessionAlreadyResponded() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{RespondedAt: sql.NullTime{Valid: true, Time: time.Now().Add(-time.Minute)}}, nil),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "consent has already been responded to 'd1ba0ad8-9107-4067-8d31-407ca59eb69c'", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectCantGetClient() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(&schema.Configuration{IdentityProviders: schema.IdentityProviders{OIDC: &schema.IdentityProvidersOpenIDConnect{}}}, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to get client for client with id 'd1ba0ad8-9107-4067-8d31-407ca59eb69c' with consent challenge id '00000000-0000-0000-0000-000000000000': invalid_client", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNoOpaqueID() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, fmt.Errorf("bad identifier")),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to determine consent subject for client with id 'abc' with consent challenge id '00000000-0000-0000-0000-000000000000': bad identifier", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNoOpaqueIDCreateError() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, nil),
+ s.mock.StorageMock.EXPECT().
+ SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(fmt.Errorf("oops")),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to determine consent subject for client with id 'abc' with consent challenge id '00000000-0000-0000-0000-000000000000': oops", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNoOpaqueIDCreateSaveConsentError() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc"}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, nil),
+ s.mock.StorageMock.EXPECT().
+ SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ s.mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(fmt.Errorf("bad id")),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to update consent subject for client with id 'abc' with consent challenge id '00000000-0000-0000-0000-000000000000': bad id", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectFormRequiresLogin() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ form := url.Values{
+ "max_age": []string{"0"},
+ }
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: form.Encode()}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, nil),
+ s.mock.StorageMock.EXPECT().
+ SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ s.mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "http://example.com/consent/openid/login?workflow=openid_connect&workflow_id=d1ba0ad8-9107-4067-8d31-407ca59eb69c"})
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectFormRequiresLoginBadForm() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: "1238y12978y189gb128g1287g12807g128702g38172%1"}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, nil),
+ s.mock.StorageMock.EXPECT().
+ SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ s.mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
+ s.mock.AssertLastLogMessage(s.T(), "unable to get authorization form values from consent session with challenge id '00000000-0000-0000-0000-000000000000': invalid URL escape \"%1\"", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNeeds2FA() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ AuthorizationPolicy: "two_factor",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: ""}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, nil),
+ s.mock.StorageMock.EXPECT().
+ SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ s.mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), nil)
+ s.mock.AssertLastLogMessage(s.T(), "OpenID Connect client 'abc' requires 2FA, cannot be redirected yet", "")
+}
+
+func (s *FirstFactorRedirectionSuite) TestShouldReplyOpenIDConnectNeeds1FA() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"two-factor.example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ AuthorizationPolicy: "one_factor",
+ },
+ },
+ },
+ },
+ }
+
+ s.mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, s.mock.StorageMock, s.mock.Ctx.Providers.Templates)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "requestMethod": "GET",
+ "keepMeLoggedIn": false,
+ "workflow": "openid_connect",
+ "workflowID": "d1ba0ad8-9107-4067-8d31-407ca59eb69c"
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(s.mock.Ctx), gomock.Eq(uuid.Must(uuid.Parse("d1ba0ad8-9107-4067-8d31-407ca59eb69c")))).
+ Return(&model.OAuth2ConsentSession{ClientID: "abc", Form: "grant_type=authorization_code"}, nil),
+ s.mock.StorageMock.EXPECT().
+ LoadUserOpaqueIdentifierBySignature(gomock.Eq(s.mock.Ctx), gomock.Eq("openid"), gomock.Eq(""), gomock.Eq("test")).
+ Return(nil, nil),
+ s.mock.StorageMock.EXPECT().
+ SaveUserOpaqueIdentifier(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ s.mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionSubject(gomock.Eq(s.mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+
+ FirstFactorPOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "http://example.com/api/oidc/authorization?consent_id=d1ba0ad8-9107-4067-8d31-407ca59eb69c&grant_type=authorization_code"})
+}
+
+type FirstFactorReauthenticateSuite struct {
+ suite.Suite
+
+ mock *mocks.MockAutheliaCtx
+}
+
+func (s *FirstFactorReauthenticateSuite) SetupTest() {
+ s.mock = mocks.NewMockAutheliaCtx(s.T())
+
+ session, err := s.mock.Ctx.GetSession()
+
+ s.Require().NoError(err)
+
+ session.Username = testValue
+ session.AuthenticationLevel = authentication.OneFactor
+
+ s.Require().NoError(s.mock.Ctx.SaveSession(session))
+}
+
+func (s *FirstFactorReauthenticateSuite) TearDownTest() {
+ s.mock.Close()
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldFailIfBodyIsNil() {
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ // No body.
+ s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to parse body: unexpected end of JSON input")
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldFailIfBodyIsInBadFormat() {
+ // Missing password.
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test"
+ }`)
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ s.mock.AssertLastLogMessage(s.T(), "Failed to parse 1FA request body", "unable to validate body: password: non zero value required")
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(false, fmt.Errorf("failed"))
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: false,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ }))
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test'", "failed")
+
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldCheckAuthenticationIsNotMarkedWhenProviderCheckPasswordError() {
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(false, fmt.Errorf("invalid credentials"))
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: false,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ }))
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldCheckUserNotBanned() {
+ s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2}, s.mock.StorageMock, &s.mock.Clock)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "keepMeLoggedIn": true
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(nil, nil),
+
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(false, fmt.Errorf("invalid credentials")),
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: false,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ })))
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldCheckBannedUser() {
+ s.mock.Ctx.Providers.Regulator = regulation.NewRegulator(schema.Regulation{MaxRetries: 2, FindTime: time.Hour, BanTime: time.Hour}, s.mock.StorageMock, &s.mock.Clock)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "username": "test",
+ "password": "hello",
+ "keepMeLoggedIn": true
+ }`)
+
+ gomock.InOrder(
+ s.mock.StorageMock.EXPECT().
+ LoadAuthenticationLogs(gomock.Eq(s.mock.Ctx), testValue, gomock.Any(), gomock.Any(), gomock.Any()).
+ Return([]model.AuthenticationAttempt{
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ {Successful: false, Time: s.mock.Clock.Now().Add(-time.Second)},
+ }, nil),
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(gomock.Eq(s.mock.Ctx), gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: true,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ })))
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ s.mock.AssertLastLogMessage(s.T(), "Unsuccessful 1FA authentication attempt by user 'test' and they are banned until 2013-02-03 00:59:59 +0000 UTC", "")
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldCheckAuthenticationIsMarkedWhenInvalidCredentials() {
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(false, nil)
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Eq(model.AuthenticationAttempt{
+ Username: testValue,
+ Successful: false,
+ Banned: false,
+ Time: s.mock.Clock.Now(),
+ Type: regulation.AuthType1FA,
+ RemoteIP: model.NewNullIPFromString("0.0.0.0"),
+ }))
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldFailIfUserProviderGetDetailsFail() {
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(true, nil)
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
+ Return(nil)
+
+ s.mock.UserProviderMock.
+ EXPECT().
+ GetDetails(gomock.Eq(testValue)).
+ Return(nil, fmt.Errorf("failed"))
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ s.mock.AssertLastLogMessage(s.T(), "Could not obtain profile details during 1FA authentication for user 'test'", "failed")
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldFailIfAuthenticationMarkFail() {
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(true, nil)
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
+ Return(fmt.Errorf("failed"))
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ s.mock.AssertLastLogMessage(s.T(), "Unable to mark 1FA authentication attempt by user 'test'", "failed")
+ s.mock.Assert401KO(s.T(), "Authentication failed. Check your credentials.")
+}
+
+func (s *FirstFactorReauthenticateSuite) TestShouldSaveUsernameFromAuthenticationBackendInSession() {
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(true, nil)
+
+ s.mock.UserProviderMock.
+ EXPECT().
+ GetDetails(gomock.Eq(testValue)).
+ Return(&authentication.UserDetails{
+ // This is the name in authentication backend, in some setups the binding is
+ // case insensitive but the user ID in session must match the user in LDAP
+ // for the other modules of Authelia to be coherent.
+ Username: "Test",
+ Emails: []string{"test@example.com"},
+ Groups: []string{"dev", "admins"},
+ }, nil)
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
+ Return(nil)
+
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ assert.Equal(s.T(), fasthttp.StatusOK, s.mock.Ctx.Response.StatusCode())
+ assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
+
+ userSession, err := s.mock.Ctx.GetSession()
+ s.Assert().NoError(err)
+
+ assert.Equal(s.T(), "Test", userSession.Username)
+ assert.Equal(s.T(), false, userSession.KeepMeLoggedIn)
+ assert.Equal(s.T(), authentication.OneFactor, userSession.AuthenticationLevel)
+ assert.Equal(s.T(), []string{"test@example.com"}, userSession.Emails)
+ assert.Equal(s.T(), []string{"dev", "admins"}, userSession.Groups)
+}
+
+type FirstFactorReauthenticateRedirectionSuite struct {
+ suite.Suite
+
+ mock *mocks.MockAutheliaCtx
+}
+
+func (s *FirstFactorReauthenticateRedirectionSuite) SetupTest() {
+ s.mock = mocks.NewMockAutheliaCtx(s.T())
+
+ session, err := s.mock.Ctx.GetSession()
+
+ s.Require().NoError(err)
+
+ session.Username = testValue
+ session.AuthenticationLevel = authentication.OneFactor
+
+ s.Require().NoError(s.mock.Ctx.SaveSession(session))
+
+ s.mock.Ctx.Configuration.Session.Cookies[0].DefaultRedirectionURL = &url.URL{Scheme: "https", Host: "default.local"}
+ s.mock.Ctx.Configuration.AccessControl.DefaultPolicy = testBypass
+ s.mock.Ctx.Configuration.AccessControl.Rules = []schema.AccessControlRule{
+ {
+ Domains: []string{"default.local"},
+ Policy: "one_factor",
+ },
+ }
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&s.mock.Ctx.Configuration)
+
+ s.mock.UserProviderMock.
+ EXPECT().
+ CheckUserPassword(gomock.Eq(testValue), gomock.Eq("hello")).
+ Return(true, nil)
+
+ s.mock.UserProviderMock.
+ EXPECT().
+ GetDetails(gomock.Eq(testValue)).
+ Return(&authentication.UserDetails{
+ Username: testValue,
+ Emails: []string{"test@example.com"},
+ Groups: []string{"dev", "admins"},
+ }, nil)
+
+ s.mock.StorageMock.
+ EXPECT().
+ AppendAuthenticationLog(s.mock.Ctx, gomock.Any()).
+ Return(nil)
+}
+
+func (s *FirstFactorReauthenticateRedirectionSuite) TearDownTest() {
+ s.mock.Close()
+}
+
+// When:
+//
+// 1/ the target url is unknown
+// 2/ two_factor is disabled (no policy is set to two_factor)
+// 3/ default_redirect_url is provided
+//
+// Then:
+//
+// the user should be redirected to the default url.
+func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldRedirectToDefaultURLWhenNoTargetURLProvidedAndTwoFactorDisabled() {
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})
+}
+
+// When:
+//
+// 1/ the target url is unsafe
+// 2/ two_factor is disabled (no policy is set to two_factor)
+// 3/ default_redirect_url is provided
+//
+// Then:
+//
+// the user should be redirected to the default url.
+func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldRedirectToDefaultURLWhenURLIsUnsafeAndTwoFactorDisabled() {
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello",
+ "targetURL": "http://notsafe.local"
+ }`)
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), &redirectResponse{Redirect: "https://www.example.com"})
+}
+
+// When:
+//
+// 1/ two_factor is enabled (default policy)
+//
+// Then:
+//
+// the user should receive 200 without redirection URL.
+func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldReply200WhenNoTargetURLProvidedAndTwoFactorEnabled() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "two_factor",
+ },
+ })
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), nil)
+}
+
+// When:
+//
+// 1/ two_factor is enabled (some rule)
+//
+// Then:
+//
+// the user should receive 200 without redirection URL.
+func (s *FirstFactorReauthenticateRedirectionSuite) TestShouldReply200WhenUnsafeTargetURLProvidedAndTwoFactorEnabled() {
+ s.mock.Ctx.Providers.Authorizer = authorization.NewAuthorizer(&schema.Configuration{
+ AccessControl: schema.AccessControl{
+ DefaultPolicy: "one_factor",
+ Rules: []schema.AccessControlRule{
+ {
+ Domains: []string{"test.example.com"},
+ Policy: "one_factor",
+ },
+ {
+ Domains: []string{"example.com"},
+ Policy: "two_factor",
+ },
+ },
+ }})
+ s.mock.Ctx.Request.SetBodyString(`{
+ "password": "hello"
+ }`)
+
+ FirstFactorReauthenticatePOST(nil)(s.mock.Ctx)
+
+ // Respond with 200.
+ s.mock.Assert200OK(s.T(), nil)
+}
+
func TestFirstFactorSuite(t *testing.T) {
suite.Run(t, new(FirstFactorSuite))
suite.Run(t, new(FirstFactorRedirectionSuite))
}
+
+func TestFirstFactorReauthenticateSuite(t *testing.T) {
+ suite.Run(t, new(FirstFactorReauthenticateSuite))
+ suite.Run(t, new(FirstFactorReauthenticateRedirectionSuite))
+}
+
+const (
+ testValue = "test"
+)
diff --git a/internal/handlers/handler_oidc_authorization.go b/internal/handlers/handler_oidc_authorization.go
index d661559e0..2099b9af1 100644
--- a/internal/handlers/handler_oidc_authorization.go
+++ b/internal/handlers/handler_oidc_authorization.go
@@ -4,12 +4,10 @@ import (
"errors"
"net/http"
"net/url"
- "time"
oauthelia2 "authelia.com/provider/oauth2"
"github.com/authelia/authelia/v4/internal/authentication"
- "github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
@@ -27,18 +25,13 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
responder oauthelia2.AuthorizeResponder
client oidc.Client
policy oidc.ClientAuthorizationPolicy
- authTime time.Time
- issuer *url.URL
err error
)
requester, err = ctx.Providers.OpenIDConnect.NewAuthorizeRequest(ctx, r)
+ if requester == nil {
+ err = oauthelia2.ErrServerError.WithDebug("The requester was nil.")
- if requester != nil && requester.GetResponseMode() == oidc.ResponseModeFormPost {
- ctx.SetUserValue(middlewares.UserValueKeyOpenIDConnectResponseModeFormPost, true)
- }
-
- if err != nil {
ctx.Logger.Errorf("Authorization Request failed with error: %s", oauthelia2.ErrorToDebugRFC6749Error(err))
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
@@ -50,6 +43,14 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
ctx.SetUserValue(middlewares.UserValueKeyOpenIDConnectResponseModeFormPost, true)
}
+ if err != nil {
+ ctx.Logger.Errorf("Authorization Request failed with error: %s", oauthelia2.ErrorToDebugRFC6749Error(err))
+
+ ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
+
+ return
+ }
+
clientID := requester.GetClient().GetID()
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being processed", requester.GetID(), clientID)
@@ -79,6 +80,7 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
}
var (
+ issuer *url.URL
details *authentication.UserDetails
userSession session.UserSession
consent *model.OAuth2ConsentSession
@@ -127,17 +129,9 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
extraClaims := oidcGrantRequests(requester, consent, details)
- if authTime, err = userSession.AuthenticatedTime(client.GetAuthorizationPolicyRequiredLevel(authorization.Subject{Username: details.Username, Groups: details.Groups, IP: ctx.RemoteIP()})); err != nil {
- ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' using policy '%s' could not be processed: error occurred checking authentication time: %+v", requester.GetID(), client.GetID(), policy.Name, err)
+ ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID)
- ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oauthelia2.ErrServerError.WithHint("Could not obtain the authentication time."))
-
- return
- }
-
- ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' using policy '%s' was successfully processed for user '%s' with groups: %+v, proceeding to build Authorization Response", requester.GetID(), clientID, policy.Name, userSession.Username, userSession.Groups)
-
- session := oidc.NewSessionWithAuthorizeRequest(ctx, issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyID(ctx, client.GetIDTokenSignedResponseKeyID(), client.GetIDTokenSignedResponseAlg()), details.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester)
+ session := oidc.NewSessionWithAuthorizeRequest(ctx, issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKeyID(ctx, client.GetIDTokenSignedResponseKeyID(), client.GetIDTokenSignedResponseAlg()), details.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, userSession.LastAuthenticatedTime(), consent, requester)
ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' using policy '%s' creating session for Authorization Response for subject '%s' with username '%s' with groups: %+v and claims: %+v",
requester.GetID(), session.ClientID, policy.Name, session.Subject, session.Username, userSession.Groups, session.Claims)
@@ -160,8 +154,6 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
return
}
- responder.GetParameters().Set(oidc.FormParameterIssuer, issuer.String())
-
ctx.Providers.OpenIDConnect.WriteAuthorizeResponse(ctx, rw, requester, responder)
}
diff --git a/internal/handlers/handler_oidc_authorization_consent.go b/internal/handlers/handler_oidc_authorization_consent.go
index 9bea500b8..44d13cfba 100644
--- a/internal/handlers/handler_oidc_authorization_consent.go
+++ b/internal/handlers/handler_oidc_authorization_consent.go
@@ -78,10 +78,28 @@ func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.UR
return handler(ctx, issuer, client, userSession, subject, rw, r, requester)
}
-func handleOIDCAuthorizationConsentNotAuthenticated(ctx *middlewares.AutheliaCtx, issuer *url.URL, _ oidc.Client,
+func handleOIDCAuthorizationConsentNotAuthenticated(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,
_ session.UserSession, _ uuid.UUID,
rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
- redirectionURL := handleOIDCAuthorizationConsentGetRedirectionURL(ctx, issuer, nil, requester, r.Form)
+ var err error
+
+ if consent, err = handleOpenIDConnectNewConsentSession(uuid.UUID{}, requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); err != nil {
+ ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "generating", err)
+
+ ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotGenerate)
+
+ return nil, true
+ }
+
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {
+ ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "saving", err)
+
+ ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
+
+ return nil, true
+ }
+
+ redirectionURL := handleOIDCAuthorizationConsentGetRedirectionURL(ctx, issuer, consent)
handleOIDCPushedAuthorizeConsent(ctx, requester, r.Form)
@@ -115,7 +133,7 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer
return nil, true
}
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {
ctx.Logger.Errorf(logFmtErrConsentGenerateError, requester.GetID(), client.GetID(), client.GetConsentPolicy(), "saving", err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
@@ -123,6 +141,14 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer
return nil, true
}
+ if oidc.RequesterRequiresLogin(requester, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ handleOIDCAuthorizationConsentPromptLoginRedirect(ctx, issuer, client, userSession, rw, r, requester, consent)
+
+ return nil, true
+ } else {
+ ctx.Logger.WithFields(map[string]any{"requested_at": consent.RequestedAt, "authenticated_at": userSession.LastAuthenticatedTime(), "prompt": requester.GetRequestForm().Get("prompt")}).Debugf("Authorization Request with id '%s' on client with id '%s' is not being redirected for reauthentication", requester.GetID(), client.GetID())
+ }
+
handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
return consent, true
@@ -134,7 +160,7 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel, authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}) {
location, _ = url.ParseRequestURI(issuer.String())
- location.Path = path.Join(location.Path, oidc.EndpointPathConsent)
+ location.Path = path.Join(location.Path, oidc.EndpointPathConsentDecision)
query := location.Query()
query.Set(queryArgID, consent.ChallengeID.String())
@@ -143,7 +169,7 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer
ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.AuthenticationLevel.String(), "sufficient", client.GetAuthorizationPolicyRequiredLevel(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}))
} else {
- location = handleOIDCAuthorizationConsentGetRedirectionURL(ctx, issuer, consent, requester, r.Form)
+ location = handleOIDCAuthorizationConsentGetRedirectionURL(ctx, issuer, consent)
ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.AuthenticationLevel.String(), "insufficient", client.GetAuthorizationPolicyRequiredLevel(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}))
}
@@ -176,7 +202,23 @@ func handleOIDCPushedAuthorizeConsent(ctx *middlewares.AutheliaCtx, requester oa
}
}
-func handleOIDCAuthorizationConsentGetRedirectionURL(_ *middlewares.AutheliaCtx, issuer *url.URL, consent *model.OAuth2ConsentSession, requester oauthelia2.AuthorizeRequester, form url.Values) (redirectURL *url.URL) {
+func handleOIDCAuthorizationConsentPromptLoginRedirect(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client, userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester, consent *model.OAuth2ConsentSession) {
+ ctx.Logger.WithFields(map[string]any{"requested_at": consent.RequestedAt, "authenticated_at": userSession.LastAuthenticatedTime()}).Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected for reauthentication: prompt type login was requested", requester.GetID(), client.GetID())
+
+ handleOIDCPushedAuthorizeConsent(ctx, requester, r.Form)
+
+ redirectionURL := issuer.JoinPath(oidc.EndpointPathConsentLogin)
+
+ query := redirectionURL.Query()
+ query.Set(queryArgWorkflow, workflowOpenIDConnect)
+ query.Set(queryArgWorkflowID, consent.ChallengeID.String())
+
+ redirectionURL.RawQuery = query.Encode()
+
+ http.Redirect(rw, r, redirectionURL.String(), http.StatusFound)
+}
+
+func handleOIDCAuthorizationConsentGetRedirectionURL(_ *middlewares.AutheliaCtx, issuer *url.URL, consent *model.OAuth2ConsentSession) (redirectURL *url.URL) {
iss := issuer.String()
if !strings.HasSuffix(iss, "/") {
@@ -187,23 +229,7 @@ func handleOIDCAuthorizationConsentGetRedirectionURL(_ *middlewares.AutheliaCtx,
query := redirectURL.Query()
query.Set(queryArgWorkflow, workflowOpenIDConnect)
-
- switch {
- case consent != nil:
- query.Set(queryArgWorkflowID, consent.ChallengeID.String())
- case form != nil:
- rd, _ := url.ParseRequestURI(iss)
- rd.Path = path.Join(rd.Path, oidc.EndpointPathAuthorization)
- rd.RawQuery = form.Encode()
-
- query.Set(queryArgRD, rd.String())
- case requester != nil:
- rd, _ := url.ParseRequestURI(iss)
- rd.Path = path.Join(rd.Path, oidc.EndpointPathAuthorization)
- rd.RawQuery = requester.GetRequestForm().Encode()
-
- query.Set(queryArgRD, rd.String())
- }
+ query.Set(queryArgWorkflowID, consent.ChallengeID.String())
redirectURL.RawQuery = query.Encode()
@@ -246,7 +272,7 @@ func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client oid
consent.Subject = model.NullUUID(subject)
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, *consent); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, consent); err != nil {
return fmt.Errorf("failed to update the consent subject: %w", err)
}
}
diff --git a/internal/handlers/handler_oidc_authorization_consent_explicit.go b/internal/handlers/handler_oidc_authorization_consent_explicit.go
index d211ac5d0..24a5dd79c 100644
--- a/internal/handlers/handler_oidc_authorization_consent_explicit.go
+++ b/internal/handlers/handler_oidc_authorization_consent_explicit.go
@@ -70,6 +70,12 @@ func handleOIDCAuthorizationConsentModeExplicitWithID(ctx *middlewares.AutheliaC
return nil, true
}
+ if oidc.RequesterRequiresLogin(requester, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ handleOIDCAuthorizationConsentPromptLoginRedirect(ctx, issuer, client, userSession, rw, r, requester, consent)
+
+ return nil, true
+ }
+
if !consent.CanGrant() {
ctx.Logger.Errorf(logFmtErrConsentCantGrant, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, "explicit")
diff --git a/internal/handlers/handler_oidc_authorization_consent_implicit.go b/internal/handlers/handler_oidc_authorization_consent_implicit.go
index cd1516005..308c4fbb5 100644
--- a/internal/handlers/handler_oidc_authorization_consent_implicit.go
+++ b/internal/handlers/handler_oidc_authorization_consent_implicit.go
@@ -37,9 +37,9 @@ func handleOIDCAuthorizationConsentModeImplicit(ctx *middlewares.AutheliaCtx, is
}
}
-func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaCtx, _ *url.URL, client oidc.Client,
+func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,
userSession session.UserSession, subject uuid.UUID, consentID uuid.UUID,
- rw http.ResponseWriter, _ *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
+ rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var (
err error
)
@@ -68,6 +68,14 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC
return nil, true
}
+ if oidc.RequesterRequiresLogin(requester, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ handleOIDCAuthorizationConsentPromptLoginRedirect(ctx, issuer, client, userSession, rw, r, requester, consent)
+
+ return nil, true
+ } else {
+ ctx.Logger.WithFields(map[string]any{"requested_at": consent.RequestedAt, "authenticated_at": userSession.LastAuthenticatedTime(), "prompt": requester.GetRequestForm().Get("prompt")}).Debugf("Authorization Request with id '%s' on client with id '%s' is not being redirected for reauthentication", requester.GetID(), client.GetID())
+ }
+
if !consent.CanGrant() {
ctx.Logger.Errorf(logFmtErrConsentCantGrant, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, "implicit")
@@ -78,7 +86,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC
consent.Grant()
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {
ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
@@ -89,9 +97,9 @@ func handleOIDCAuthorizationConsentModeImplicitWithID(ctx *middlewares.AutheliaC
return consent, false
}
-func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.AutheliaCtx, _ *url.URL, client oidc.Client,
- _ session.UserSession, subject uuid.UUID,
- rw http.ResponseWriter, _ *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
+func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client oidc.Client,
+ userSession session.UserSession, subject uuid.UUID,
+ rw http.ResponseWriter, r *http.Request, requester oauthelia2.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var (
err error
)
@@ -104,7 +112,7 @@ func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.Authel
return nil, true
}
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {
ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
@@ -112,17 +120,27 @@ func handleOIDCAuthorizationConsentModeImplicitWithoutID(ctx *middlewares.Authel
return nil, true
}
- if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consent.ChallengeID); err != nil {
- ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
+ challenge := consent.ChallengeID
+
+ if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, challenge); err != nil {
+ ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), challenge, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
return nil, true
}
+ if oidc.RequesterRequiresLogin(requester, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ handleOIDCAuthorizationConsentPromptLoginRedirect(ctx, issuer, client, userSession, rw, r, requester, consent)
+
+ return nil, true
+ } else {
+ ctx.Logger.WithFields(map[string]any{"requested_at": consent.RequestedAt, "authenticated_at": userSession.LastAuthenticatedTime(), "prompt": requester.GetRequestForm().Get("prompt")}).Debugf("Authorization Request with id '%s' on client with id '%s' is not being redirected for reauthentication", requester.GetID(), client.GetID())
+ }
+
consent.Grant()
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {
ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
diff --git a/internal/handlers/handler_oidc_authorization_consent_pre_configured.go b/internal/handlers/handler_oidc_authorization_consent_pre_configured.go
index 873098938..91d67d65a 100644
--- a/internal/handlers/handler_oidc_authorization_consent_pre_configured.go
+++ b/internal/handlers/handler_oidc_authorization_consent_pre_configured.go
@@ -75,6 +75,12 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth
return nil, true
}
+ if oidc.RequesterRequiresLogin(requester, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ handleOIDCAuthorizationConsentPromptLoginRedirect(ctx, issuer, client, userSession, rw, r, requester, consent)
+
+ return nil, true
+ }
+
if !consent.CanGrant() {
ctx.Logger.Errorf(logFmtErrConsentCantGrantPreConf, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID)
@@ -96,7 +102,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithID(ctx *middlewares.Auth
consent.PreConfiguration = sql.NullInt64{Int64: config.ID, Valid: true}
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {
ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
@@ -168,7 +174,7 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A
return nil, true
}
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, consent); err != nil {
ctx.Logger.Errorf(logFmtErrConsentSaveSession, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
@@ -184,11 +190,19 @@ func handleOIDCAuthorizationConsentModePreConfiguredWithoutID(ctx *middlewares.A
return nil, true
}
+ if oidc.RequesterRequiresLogin(requester, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ handleOIDCAuthorizationConsentPromptLoginRedirect(ctx, issuer, client, userSession, rw, r, requester, consent)
+
+ return nil, true
+ } else {
+ ctx.Logger.WithFields(map[string]any{"requested_at": consent.RequestedAt, "authenticated_at": userSession.LastAuthenticatedTime(), "prompt": requester.GetRequestForm().Get("prompt")}).Debugf("Authorization Request with id '%s' on client with id '%s' is not being redirected for reauthentication", requester.GetID(), client.GetID())
+ }
+
consent.Grant()
consent.PreConfiguration = sql.NullInt64{Int64: config.ID, Valid: true}
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, false); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, false); err != nil {
ctx.Logger.Errorf(logFmtErrConsentSaveSessionResponse, requester.GetID(), client.GetID(), client.GetConsentPolicy(), consent.ChallengeID, err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrConsentCouldNotSave)
diff --git a/internal/handlers/handler_oidc_authorization_consent_test.go b/internal/handlers/handler_oidc_authorization_consent_test.go
new file mode 100644
index 000000000..ec7f6e872
--- /dev/null
+++ b/internal/handlers/handler_oidc_authorization_consent_test.go
@@ -0,0 +1,1404 @@
+package handlers
+
+import (
+ "database/sql"
+ "fmt"
+ "net/http/httptest"
+ "net/url"
+ "regexp"
+ "testing"
+ "time"
+
+ oauthelia2 "authelia.com/provider/oauth2"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
+ "github.com/authelia/authelia/v4/internal/mocks"
+ "github.com/authelia/authelia/v4/internal/model"
+ "github.com/authelia/authelia/v4/internal/oidc"
+ "github.com/authelia/authelia/v4/internal/session"
+)
+
+func TestHandleOIDCAuthorizationConsentGenerate(t *testing.T) {
+ mustParseURI := func(t *testing.T, in string) *url.URL {
+ result, err := url.Parse(in)
+ require.NoError(t, err)
+
+ return result
+ }
+
+ clientTest := &oidc.RegisteredClient{
+ ID: testValue,
+ }
+
+ sub := uuid.MustParse("e79b6494-8852-4439-860c-159f2cba83dc")
+
+ testCases := []struct {
+ name string
+ issuer *url.URL
+ client oidc.Client
+ userSession session.UserSession
+ subject uuid.UUID
+ requester oauthelia2.AuthorizeRequester
+ expected *model.OAuth2ConsentSession
+ handled bool
+ setup func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ expect func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ }{
+ {
+ name: "ShouldHandleDBError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ },
+ },
+ expected: (*model.OAuth2ConsentSession)(nil),
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(fmt.Errorf("invalid")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred saving consent: invalid$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleQueryArgsError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ },
+ },
+ expected: (*model.OAuth2ConsentSession)(nil),
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.Ctx.QueryArgs().Add(queryArgConsentID, "abc")
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred generating consent: consent id value was present when it should be absent$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandlePromptLoginNotRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "prompt=login",
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleMaxAgeNotRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "max_age=10",
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandlePromptLoginRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleMaxAgeRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 0, SecondFactorAuthnTimestamp: 0},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+ mock.Clock.Set(time.Unix(1000000, 0))
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, mock.StorageMock, mock.Ctx.Providers.Templates)
+
+ rw := httptest.NewRecorder()
+
+ if tc.setup != nil {
+ tc.setup(t, mock)
+ }
+
+ consent, handled := handleOIDCAuthorizationConsentGenerate(mock.Ctx, tc.issuer, tc.client, tc.userSession, tc.subject, rw, httptest.NewRequest("GET", "https://example.com", nil), tc.requester)
+
+ assert.Equal(t, tc.handled, handled)
+
+ if tc.expected == nil {
+ assert.Nil(t, consent)
+ } else {
+ require.NotNil(t, consent)
+ assert.Equal(t, tc.expected.ClientID, consent.ClientID)
+ assert.Equal(t, tc.expected.Subject, consent.Subject)
+ assert.Equal(t, tc.expected.Granted, consent.Granted)
+ assert.Equal(t, tc.expected.Authorized, consent.Authorized)
+ assert.Equal(t, tc.expected.RequestedAt, consent.RequestedAt)
+ assert.Equal(t, tc.expected.RespondedAt, consent.RespondedAt)
+ assert.Equal(t, tc.expected.Form, consent.Form)
+ assert.Equal(t, tc.expected.RequestedScopes, consent.RequestedScopes)
+ assert.Equal(t, tc.expected.RequestedAudience, consent.RequestedAudience)
+ assert.Equal(t, tc.expected.GrantedScopes, consent.GrantedScopes)
+ assert.Equal(t, tc.expected.GrantedAudience, consent.GrantedAudience)
+ assert.Equal(t, tc.expected.PreConfiguration, consent.PreConfiguration)
+ }
+
+ if tc.expect != nil {
+ tc.expect(t, mock)
+ }
+ })
+ }
+}
+
+func TestHandleOIDCAuthorizationConsentNotAuthenticated(t *testing.T) {
+ mustParseURI := func(t *testing.T, in string) *url.URL {
+ result, err := url.Parse(in)
+ require.NoError(t, err)
+
+ return result
+ }
+
+ clientTest := &oidc.RegisteredClient{
+ ID: testValue,
+ }
+
+ sub := uuid.MustParse("e79b6494-8852-4439-860c-159f2cba83dc")
+
+ testCases := []struct {
+ name string
+ issuer *url.URL
+ client oidc.Client
+ userSession session.UserSession
+ subject uuid.UUID
+ requester oauthelia2.AuthorizeRequester
+ expected *model.OAuth2ConsentSession
+ handled bool
+ setup func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ expect func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ }{
+ {
+ name: "ShouldHandleDBError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ },
+ },
+ expected: (*model.OAuth2ConsentSession)(nil),
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(fmt.Errorf("invalid")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred saving consent: invalid$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandlePromptLoginRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleMaxAgeRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 0, SecondFactorAuthnTimestamp: 0},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+ mock.Clock.Set(time.Unix(1000000, 0))
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, mock.StorageMock, mock.Ctx.Providers.Templates)
+
+ rw := httptest.NewRecorder()
+
+ if tc.setup != nil {
+ tc.setup(t, mock)
+ }
+
+ consent, handled := handleOIDCAuthorizationConsentNotAuthenticated(mock.Ctx, tc.issuer, tc.client, tc.userSession, tc.subject, rw, httptest.NewRequest("GET", "https://example.com", nil), tc.requester)
+
+ assert.Equal(t, tc.handled, handled)
+
+ if tc.expected == nil {
+ assert.Nil(t, consent)
+ } else {
+ require.NotNil(t, consent)
+ assert.Equal(t, tc.expected.ClientID, consent.ClientID)
+ assert.Equal(t, tc.expected.Subject, consent.Subject)
+ assert.Equal(t, tc.expected.Granted, consent.Granted)
+ assert.Equal(t, tc.expected.Authorized, consent.Authorized)
+ assert.Equal(t, tc.expected.RequestedAt, consent.RequestedAt)
+ assert.Equal(t, tc.expected.RespondedAt, consent.RespondedAt)
+ assert.Equal(t, tc.expected.Form, consent.Form)
+ assert.Equal(t, tc.expected.RequestedScopes, consent.RequestedScopes)
+ assert.Equal(t, tc.expected.RequestedAudience, consent.RequestedAudience)
+ assert.Equal(t, tc.expected.GrantedScopes, consent.GrantedScopes)
+ assert.Equal(t, tc.expected.GrantedAudience, consent.GrantedAudience)
+ assert.Equal(t, tc.expected.PreConfiguration, consent.PreConfiguration)
+ }
+
+ if tc.expect != nil {
+ tc.expect(t, mock)
+ }
+ })
+ }
+}
+
+func TestHandleOIDCAuthorizationConsentModeImplicitWithoutID(t *testing.T) {
+ mustParseURI := func(t *testing.T, in string) *url.URL {
+ result, err := url.Parse(in)
+ require.NoError(t, err)
+
+ return result
+ }
+
+ clientTest := &oidc.RegisteredClient{
+ ID: testValue,
+ }
+
+ sub := uuid.MustParse("e79b6494-8852-4439-860c-159f2cba83dc")
+
+ testCases := []struct {
+ name string
+ issuer *url.URL
+ client oidc.Client
+ userSession session.UserSession
+ subject uuid.UUID
+ requester oauthelia2.AuthorizeRequester
+ expected *model.OAuth2ConsentSession
+ handled bool
+ setup func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ expect func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ }{
+ {
+ name: "ShouldHandleDBError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(fmt.Errorf("invalid")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}': error occurred saving consent session: invalid$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleLoadSessionFromDBError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil, fmt.Errorf("bad")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}': error occurred saving consent session: bad$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequiredErrorSave",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(&model.OAuth2ConsentSession{
+ ID: 1,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "prompt=login",
+ RequestedAt: time.Unix(1000000, 0),
+ }, nil),
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionResponse(gomock.Eq(mock.Ctx), gomock.Any(), gomock.Eq(false)).
+ Return(fmt.Errorf("bad conn")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}': error occurred saving consent session response: bad conn$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "prompt=login",
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ handled: false,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(&model.OAuth2ConsentSession{
+ ID: 1,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "prompt=login",
+ RequestedAt: time.Unix(1000000, 0),
+ }, nil),
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionResponse(gomock.Eq(mock.Ctx), gomock.Any(), gomock.Eq(false)).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormMaxAgeNotRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "max_age=10",
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ handled: false,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(&model.OAuth2ConsentSession{
+ ID: 1,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "max_age=10",
+ RequestedAt: time.Unix(1000000, 0),
+ }, nil),
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionResponse(gomock.Eq(mock.Ctx), gomock.Any(), gomock.Eq(false)).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandlePromptLoginRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(&model.OAuth2ConsentSession{
+ ID: 1,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "prompt=login",
+ RequestedAt: time.Unix(1000000, 0),
+ }, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleMaxAgeRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 0, SecondFactorAuthnTimestamp: 0},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSession(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(nil),
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Any()).
+ Return(&model.OAuth2ConsentSession{
+ ID: 1,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "max_age=10",
+ RequestedAt: time.Unix(1000000, 0),
+ }, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+ mock.Clock.Set(time.Unix(1000000, 0))
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, mock.StorageMock, mock.Ctx.Providers.Templates)
+
+ rw := httptest.NewRecorder()
+
+ if tc.setup != nil {
+ tc.setup(t, mock)
+ }
+
+ consent, handled := handleOIDCAuthorizationConsentModeImplicitWithoutID(mock.Ctx, tc.issuer, tc.client, tc.userSession, tc.subject, rw, httptest.NewRequest("GET", "https://example.com", nil), tc.requester)
+
+ assert.Equal(t, tc.handled, handled)
+
+ if tc.expected == nil {
+ assert.Nil(t, consent)
+ } else {
+ require.NotNil(t, consent)
+
+ if tc.expected.ID != 0 {
+ assert.Equal(t, tc.expected.ID, consent.ID)
+ }
+
+ assert.Equal(t, tc.expected.ClientID, consent.ClientID)
+ assert.Equal(t, tc.expected.Subject, consent.Subject)
+ assert.Equal(t, tc.expected.Granted, consent.Granted)
+ assert.Equal(t, tc.expected.Authorized, consent.Authorized)
+ assert.Equal(t, tc.expected.RequestedAt, consent.RequestedAt)
+ assert.Equal(t, tc.expected.RespondedAt, consent.RespondedAt)
+ assert.Equal(t, tc.expected.Form, consent.Form)
+ assert.Equal(t, tc.expected.RequestedScopes, consent.RequestedScopes)
+ assert.Equal(t, tc.expected.RequestedAudience, consent.RequestedAudience)
+ assert.Equal(t, tc.expected.GrantedScopes, consent.GrantedScopes)
+ assert.Equal(t, tc.expected.GrantedAudience, consent.GrantedAudience)
+ assert.Equal(t, tc.expected.PreConfiguration, consent.PreConfiguration)
+ }
+
+ if tc.expect != nil {
+ tc.expect(t, mock)
+ }
+ })
+ }
+}
+
+func TestHandleOIDCAuthorizationConsentModeImplicitWithID(t *testing.T) {
+ mustParseURI := func(t *testing.T, in string) *url.URL {
+ result, err := url.Parse(in)
+ require.NoError(t, err)
+
+ return result
+ }
+
+ clientTest := &oidc.RegisteredClient{
+ ID: testValue,
+ }
+
+ challenge := uuid.MustParse("11303e1f-f8af-436a-9a72-c7361bfc9f37")
+ sub := uuid.MustParse("e79b6494-8852-4439-860c-159f2cba83dc")
+
+ testCases := []struct {
+ name string
+ issuer *url.URL
+ client oidc.Client
+ userSession session.UserSession
+ subject uuid.UUID
+ requester oauthelia2.AuthorizeRequester
+ expected *model.OAuth2ConsentSession
+ handled bool
+ setup func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ expect func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ }{
+ {
+ name: "ShouldHandleDBError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(nil, fmt.Errorf("error in db")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '11303e1f-f8af-436a-9a72-c7361bfc9f37': error occurred while loading session: error in db$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleLoadSessionWrongID",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ChallengeID: challenge, Subject: uuid.NullUUID{UUID: challenge, Valid: true}}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '11303e1f-f8af-436a-9a72-c7361bfc9f37': user 'test' with subject 'e79b6494-8852-4439-860c-159f2cba83dc' is not authorized to consent for subject '11303e1f-f8af-436a-9a72-c7361bfc9f37'$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequiredErrorSave",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ChallengeID: challenge, Subject: uuid.NullUUID{UUID: sub, Valid: true}}, nil),
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionResponse(gomock.Eq(mock.Ctx), gomock.Any(), gomock.Eq(false)).
+ Return(fmt.Errorf("bad conn")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}': error occurred saving consent session response: bad conn$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{
+ ID: 40,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "prompt=login",
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ handled: false,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 40, ChallengeID: challenge, ClientID: "test", Subject: uuid.NullUUID{UUID: sub, Valid: true}, Form: "prompt=login", RequestedAt: time.Unix(1000000, 0)}, nil),
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionResponse(gomock.Eq(mock.Ctx), gomock.Any(), gomock.Eq(false)).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormPromptLoginRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 100000, SecondFactorAuthnTimestamp: 100000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 40, ChallengeID: challenge, ClientID: "test", Subject: uuid.NullUUID{UUID: sub, Valid: true}, Form: "prompt=login", RequestedAt: time.Unix(1000000, 0)}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormMaxAgeRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 100000, SecondFactorAuthnTimestamp: 100000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 40, ChallengeID: challenge, ClientID: "test", Subject: uuid.NullUUID{UUID: sub, Valid: true}, Form: "max_age=10", RequestedAt: time.Unix(1000000, 0)}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormMaxAgeNotRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{
+ ID: 40,
+ ClientID: "test",
+ Subject: uuid.NullUUID{UUID: sub, Valid: true},
+ Form: "max_age=10",
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ handled: false,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 40, ChallengeID: challenge, ClientID: "test", Subject: uuid.NullUUID{UUID: sub, Valid: true}, Form: "max_age=10", RequestedAt: time.Unix(1000000, 0)}, nil),
+ mock.StorageMock.EXPECT().
+ SaveOAuth2ConsentSessionResponse(gomock.Eq(mock.Ctx), gomock.Any(), gomock.Eq(false)).
+ Return(nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+ mock.Clock.Set(time.Unix(1000000, 0))
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, mock.StorageMock, mock.Ctx.Providers.Templates)
+
+ rw := httptest.NewRecorder()
+
+ if tc.setup != nil {
+ tc.setup(t, mock)
+ }
+
+ consent, handled := handleOIDCAuthorizationConsentModeImplicitWithID(mock.Ctx, tc.issuer, tc.client, tc.userSession, tc.subject, challenge, rw, httptest.NewRequest("GET", "https://example.com", nil), tc.requester)
+
+ assert.Equal(t, tc.handled, handled)
+
+ if tc.expected == nil {
+ assert.Nil(t, consent)
+ } else {
+ require.NotNil(t, consent)
+
+ if tc.expected.ID != 0 {
+ assert.Equal(t, tc.expected.ID, consent.ID)
+ }
+
+ assert.Equal(t, tc.expected.ClientID, consent.ClientID)
+ assert.Equal(t, tc.expected.Subject, consent.Subject)
+ assert.Equal(t, tc.expected.Granted, consent.Granted)
+ assert.Equal(t, tc.expected.Authorized, consent.Authorized)
+ assert.Equal(t, tc.expected.RequestedAt, consent.RequestedAt)
+ assert.Equal(t, tc.expected.RespondedAt, consent.RespondedAt)
+ assert.Equal(t, tc.expected.Form, consent.Form)
+ assert.Equal(t, tc.expected.RequestedScopes, consent.RequestedScopes)
+ assert.Equal(t, tc.expected.RequestedAudience, consent.RequestedAudience)
+ assert.Equal(t, tc.expected.GrantedScopes, consent.GrantedScopes)
+ assert.Equal(t, tc.expected.GrantedAudience, consent.GrantedAudience)
+ assert.Equal(t, tc.expected.PreConfiguration, consent.PreConfiguration)
+ }
+
+ if tc.expect != nil {
+ tc.expect(t, mock)
+ }
+ })
+ }
+}
+
+func TestHandleOIDCAuthorizationConsentModeExplicitWithID(t *testing.T) {
+ mustParseURI := func(t *testing.T, in string) *url.URL {
+ result, err := url.Parse(in)
+ require.NoError(t, err)
+
+ return result
+ }
+
+ clientTest := &oidc.RegisteredClient{
+ ID: testValue,
+ }
+
+ challenge := uuid.MustParse("11303e1f-f8af-436a-9a72-c7361bfc9f37")
+ sub := uuid.MustParse("e79b6494-8852-4439-860c-159f2cba83dc")
+
+ testCases := []struct {
+ name string
+ issuer *url.URL
+ client oidc.Client
+ userSession session.UserSession
+ subject uuid.UUID
+ challenge uuid.UUID
+ requester oauthelia2.AuthorizeRequester
+ expected *model.OAuth2ConsentSession
+ handled bool
+ setup func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ expect func(t *testing.T, mock *mocks.MockAutheliaCtx)
+ }{
+ {
+ name: "ShouldHandleDBError",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(nil, fmt.Errorf("error in db")),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '11303e1f-f8af-436a-9a72-c7361bfc9f37': error occurred while loading session: error in db$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleLoadSessionWrongID",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ChallengeID: challenge, Subject: uuid.NullUUID{UUID: challenge, Valid: true}}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '11303e1f-f8af-436a-9a72-c7361bfc9f37': user 'test' with subject 'e79b6494-8852-4439-860c-159f2cba83dc' is not authorized to consent for subject '11303e1f-f8af-436a-9a72-c7361bfc9f37'$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequiredAuthorized",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: &model.OAuth2ConsentSession{ID: 44, ChallengeID: challenge, Subject: uuid.NullUUID{UUID: sub, Valid: true}, Authorized: true, RespondedAt: sql.NullTime{Time: time.Unix(1000000, 0), Valid: true}},
+ handled: false,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 44, ChallengeID: challenge, Subject: uuid.NullUUID{UUID: sub, Valid: true}, Authorized: true, RespondedAt: sql.NullTime{Time: time.Unix(1000000, 0), Valid: true}}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequiredNotResponded",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 44, ChallengeID: challenge, Subject: uuid.NullUUID{UUID: sub, Valid: true}}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequiredNotRejected",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 44, ChallengeID: challenge, Subject: uuid.NullUUID{UUID: sub, Valid: true}, RespondedAt: sql.NullTime{Time: time.Unix(1000000, 0), Valid: true}}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '11303e1f-f8af-436a-9a72-c7361bfc9f37': the user explicitly rejected this consent session$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNotRequiredCantGrant",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 44, Granted: true, ChallengeID: challenge, Subject: uuid.NullUUID{UUID: sub, Valid: true}, RespondedAt: sql.NullTime{Time: time.Unix(1000000, 0), Valid: true}}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: error occurred performing consent for consent session with id '11303e1f-f8af-436a-9a72-c7361bfc9f37': the session does not appear to be valid for explicit consent: either the subject is null, the consent has already been granted, or the consent session is a pre-configured session$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginNilChallenge",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 1000000, SecondFactorAuthnTimestamp: 1000000},
+ subject: sub,
+ challenge: uuid.Nil,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ mock.AssertLastLogMessageRegexp(t, regexp.MustCompile(`^Authorization Request with id '[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}' on client with id 'test' using consent mode 'explicit' could not be processed: the consent id had a zero value$`), nil)
+ },
+ },
+ {
+ name: "ShouldHandleFormPromptLoginRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 100000, SecondFactorAuthnTimestamp: 100000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterPrompt: []string{oidc.PromptLogin},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 40, ChallengeID: challenge, ClientID: "test", Subject: uuid.NullUUID{UUID: sub, Valid: true}, Form: "prompt=login", RequestedAt: time.Unix(1000000, 0)}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ {
+ name: "ShouldHandleFormMaxAgeRequired",
+ issuer: mustParseURI(t, "https://auth.example.com"),
+ client: clientTest,
+ userSession: session.UserSession{Username: testValue, FirstFactorAuthnTimestamp: 100000, SecondFactorAuthnTimestamp: 100000},
+ subject: sub,
+ challenge: challenge,
+ requester: &oauthelia2.AuthorizeRequest{
+ Request: oauthelia2.Request{
+ Client: clientTest,
+ Form: url.Values{
+ oidc.FormParameterMaximumAge: []string{"10"},
+ },
+ RequestedAt: time.Unix(1000000, 0),
+ },
+ },
+ expected: nil,
+ handled: true,
+ setup: func(t *testing.T, mock *mocks.MockAutheliaCtx) {
+ gomock.InOrder(
+ mock.StorageMock.EXPECT().
+ LoadOAuth2ConsentSessionByChallengeID(gomock.Eq(mock.Ctx), gomock.Eq(challenge)).
+ Return(&model.OAuth2ConsentSession{ID: 40, ChallengeID: challenge, ClientID: "test", Subject: uuid.NullUUID{UUID: sub, Valid: true}, Form: "max_age=10", RequestedAt: time.Unix(1000000, 0)}, nil),
+ )
+ },
+ expect: func(t *testing.T, mock *mocks.MockAutheliaCtx) {},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ defer mock.Close()
+
+ mock.Ctx.Clock = &mock.Clock
+ mock.Clock.Set(time.Unix(1000000, 0))
+
+ config := &schema.Configuration{
+ IdentityProviders: schema.IdentityProviders{
+ OIDC: &schema.IdentityProvidersOpenIDConnect{
+ Clients: []schema.IdentityProvidersOpenIDConnectClient{
+ {
+ ID: "abc",
+ },
+ },
+ },
+ },
+ }
+
+ mock.Ctx.Providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(config, mock.StorageMock, mock.Ctx.Providers.Templates)
+
+ rw := httptest.NewRecorder()
+
+ if tc.setup != nil {
+ tc.setup(t, mock)
+ }
+
+ consent, handled := handleOIDCAuthorizationConsentModeExplicitWithID(mock.Ctx, tc.issuer, tc.client, tc.userSession, tc.subject, tc.challenge, rw, httptest.NewRequest("GET", "https://example.com", nil), tc.requester)
+
+ assert.Equal(t, tc.handled, handled)
+
+ if tc.expected == nil {
+ assert.Nil(t, consent)
+ } else {
+ require.NotNil(t, consent)
+
+ if tc.expected.ID != 0 {
+ assert.Equal(t, tc.expected.ID, consent.ID)
+ }
+
+ assert.Equal(t, tc.expected.ClientID, consent.ClientID)
+ assert.Equal(t, tc.expected.Subject, consent.Subject)
+ assert.Equal(t, tc.expected.Granted, consent.Granted)
+ assert.Equal(t, tc.expected.Authorized, consent.Authorized)
+ assert.Equal(t, tc.expected.RequestedAt, consent.RequestedAt)
+ assert.Equal(t, tc.expected.RespondedAt, consent.RespondedAt)
+ assert.Equal(t, tc.expected.Form, consent.Form)
+ assert.Equal(t, tc.expected.RequestedScopes, consent.RequestedScopes)
+ assert.Equal(t, tc.expected.RequestedAudience, consent.RequestedAudience)
+ assert.Equal(t, tc.expected.GrantedScopes, consent.GrantedScopes)
+ assert.Equal(t, tc.expected.GrantedAudience, consent.GrantedAudience)
+ assert.Equal(t, tc.expected.PreConfiguration, consent.PreConfiguration)
+ }
+
+ if tc.expect != nil {
+ tc.expect(t, mock)
+ }
+ })
+ }
+}
diff --git a/internal/handlers/handler_oidc_consent.go b/internal/handlers/handler_oidc_consent.go
index 6609829bf..32931e914 100644
--- a/internal/handlers/handler_oidc_consent.go
+++ b/internal/handlers/handler_oidc_consent.go
@@ -127,7 +127,7 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
}
}
- if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, bodyJSON.Consent); err != nil {
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, consent, bodyJSON.Consent); err != nil {
ctx.Logger.Errorf("Failed to save the consent session response to the database: %+v", err)
ctx.SetJSONError(messageOperationFailed)
diff --git a/internal/handlers/handler_sign_duo.go b/internal/handlers/handler_sign_duo.go
index 5e0c81a9d..95961f6bd 100644
--- a/internal/handlers/handler_sign_duo.go
+++ b/internal/handlers/handler_sign_duo.go
@@ -274,7 +274,7 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, userSession *session.UserSession,
}
if bodyJSON.Workflow == workflowOpenIDConnect {
- handleOIDCWorkflowResponse(ctx, userSession, bodyJSON.TargetURL, bodyJSON.WorkflowID)
+ handleOIDCWorkflowResponse(ctx, userSession, bodyJSON.WorkflowID)
} else {
Handle2FAResponse(ctx, bodyJSON.TargetURL)
}
diff --git a/internal/handlers/handler_sign_totp.go b/internal/handlers/handler_sign_totp.go
index ef38d2623..badc0d754 100644
--- a/internal/handlers/handler_sign_totp.go
+++ b/internal/handlers/handler_sign_totp.go
@@ -211,7 +211,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
}
if bodyJSON.Workflow == workflowOpenIDConnect {
- handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.TargetURL, bodyJSON.WorkflowID)
+ handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.WorkflowID)
} else {
Handle2FAResponse(ctx, bodyJSON.TargetURL)
}
diff --git a/internal/handlers/handler_sign_webauthn.go b/internal/handlers/handler_sign_webauthn.go
index c624a3bcd..bb2c61d57 100644
--- a/internal/handlers/handler_sign_webauthn.go
+++ b/internal/handlers/handler_sign_webauthn.go
@@ -267,7 +267,7 @@ func WebAuthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
assertionResponse.Response.AuthenticatorData.Flags.HasUserVerified())
if bodyJSON.Workflow == workflowOpenIDConnect {
- handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.TargetURL, bodyJSON.WorkflowID)
+ handleOIDCWorkflowResponse(ctx, &userSession, bodyJSON.WorkflowID)
} else {
Handle2FAResponse(ctx, bodyJSON.TargetURL)
}
diff --git a/internal/handlers/response.go b/internal/handlers/response.go
index f4112fc6f..107aaf5b3 100644
--- a/internal/handlers/response.go
+++ b/internal/handlers/response.go
@@ -3,7 +3,6 @@ package handlers
import (
"fmt"
"net/url"
- "path"
"time"
"github.com/google/uuid"
@@ -129,51 +128,7 @@ func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
ctx.ReplyOK()
}
-// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
-func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, targetURI, workflowID string) {
- switch {
- case len(workflowID) != 0:
- handleOIDCWorkflowResponseWithID(ctx, userSession, workflowID)
- case len(targetURI) != 0:
- handleOIDCWorkflowResponseWithTargetURL(ctx, userSession, targetURI)
- default:
- ctx.Error(fmt.Errorf("invalid post data: must contain either a target url or a workflow id"), messageAuthenticationFailed)
- }
-}
-
-func handleOIDCWorkflowResponseWithTargetURL(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, targetURI string) {
- var (
- issuerURL *url.URL
- targetURL *url.URL
- err error
- )
-
- if targetURL, err = url.ParseRequestURI(targetURI); err != nil {
- ctx.Error(fmt.Errorf("unable to parse target URL '%s': %w", targetURI, err), messageAuthenticationFailed)
-
- return
- }
-
- issuerURL = ctx.RootURL()
-
- if targetURL.Host != issuerURL.Host {
- ctx.Error(fmt.Errorf("unable to redirect to '%s': target host '%s' does not match expected issuer host '%s'", targetURL, targetURL.Host, issuerURL.Host), messageAuthenticationFailed)
-
- return
- }
-
- if userSession.IsAnonymous() {
- ctx.Error(fmt.Errorf("unable to redirect to '%s': user is anonymous", targetURL), messageAuthenticationFailed)
-
- return
- }
-
- if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
- ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
- }
-}
-
-func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, id string) {
+func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, id string) {
var (
workflowID uuid.UUID
client oidc.Client
@@ -194,7 +149,7 @@ func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, userSession
}
if consent.Responded() {
- ctx.Error(fmt.Errorf("consent has already been responded to '%s': %w", id, err), messageAuthenticationFailed)
+ ctx.Error(fmt.Errorf("consent has already been responded to '%s'", id), messageAuthenticationFailed)
return
}
@@ -211,26 +166,58 @@ func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, userSession
return
}
- level := client.GetAuthorizationPolicyRequiredLevel(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()})
+ if !consent.Subject.Valid {
+ if consent.Subject.UUID, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifierURI(), userSession.Username); err != nil {
+ ctx.Error(fmt.Errorf("unable to determine consent subject for client with id '%s' with consent challenge id '%s': %w", client.GetID(), consent.ChallengeID, err), messageAuthenticationFailed)
- switch {
- case authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, level), level == authorization.Denied:
- var (
- targetURL *url.URL
- form url.Values
- )
+ return
+ }
- targetURL = ctx.RootURL()
+ consent.Subject.Valid = true
- if form, err = consent.GetForm(); err != nil {
- ctx.Error(fmt.Errorf("unable to get authorization form values from consent session with challenge id '%s': %w", consent.ChallengeID, err), messageAuthenticationFailed)
+ if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, consent); err != nil {
+ ctx.Error(fmt.Errorf("unable to update consent subject for client with id '%s' with consent challenge id '%s': %w", client.GetID(), consent.ChallengeID, err), messageAuthenticationFailed)
return
}
+ }
- form.Set(queryArgConsentID, workflowID.String())
+ var (
+ issuer *url.URL
+ form url.Values
+ )
+
+ issuer = ctx.RootURL()
+
+ if form, err = consent.GetForm(); err != nil {
+ ctx.Error(fmt.Errorf("unable to get authorization form values from consent session with challenge id '%s': %w", consent.ChallengeID, err), messageAuthenticationFailed)
+
+ return
+ }
+
+ if oidc.RequestFormRequiresLogin(form, consent.RequestedAt, userSession.LastAuthenticatedTime()) {
+ targetURL := issuer.JoinPath(oidc.EndpointPathConsentLogin)
+
+ query := targetURL.Query()
+ query.Set(queryArgWorkflow, workflowOpenIDConnect)
+ query.Set(queryArgWorkflowID, workflowID.String())
- targetURL.Path = path.Join(targetURL.Path, oidc.EndpointPathAuthorization)
+ targetURL.RawQuery = query.Encode()
+
+ if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
+ ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
+ }
+
+ return
+ }
+
+ level := client.GetAuthorizationPolicyRequiredLevel(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()})
+
+ switch {
+ case authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, level), level == authorization.Denied:
+ targetURL := issuer.JoinPath(oidc.EndpointPathAuthorization)
+
+ form.Set(queryArgConsentID, workflowID.String())
targetURL.RawQuery = form.Encode()
if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
diff --git a/internal/handlers/types.go b/internal/handlers/types.go
index 399d4066a..377b158b9 100644
--- a/internal/handlers/types.go
+++ b/internal/handlers/types.go
@@ -94,15 +94,24 @@ type bodyPreferred2FAMethod struct {
type bodyFirstFactorRequest struct {
Username string `json:"username" valid:"required"`
Password string `json:"password" valid:"required"`
- TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
WorkflowID string `json:"workflowID"`
+ TargetURL string `json:"targetURL"`
RequestMethod string `json:"requestMethod"`
KeepMeLoggedIn *bool `json:"keepMeLoggedIn"`
// KeepMeLoggedIn: Cannot require this field because of https://github.com/asaskevich/govalidator/pull/329
// TODO(c.michaud): add required validation once the above PR is merged.
}
+// bodyFirstFactorRequest represents the JSON body received by the endpoint.
+type bodyFirstFactorReauthenticateRequest struct {
+ Password string `json:"password" valid:"required"`
+ Workflow string `json:"workflow"`
+ WorkflowID string `json:"workflowID"`
+ TargetURL string `json:"targetURL"`
+ RequestMethod string `json:"requestMethod"`
+}
+
// checkURIWithinDomainRequestBody represents the JSON body received by the endpoint checking if an URI is within
// the configured domain.
type checkURIWithinDomainRequestBody struct {
diff --git a/internal/mocks/authelia_ctx.go b/internal/mocks/authelia_ctx.go
index fb7f4d9ec..c9f53fd79 100644
--- a/internal/mocks/authelia_ctx.go
+++ b/internal/mocks/authelia_ctx.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/url"
+ "regexp"
"testing"
"time"
@@ -303,6 +304,56 @@ func (m *MockAutheliaCtx) Assert200OK(t *testing.T, data interface{}) {
assert.Equal(t, string(b), string(m.Ctx.Response.Body()))
}
+func (m *MockAutheliaCtx) AssertLastLogMessageRegexp(t *testing.T, message, err *regexp.Regexp) {
+ entry := m.Hook.LastEntry()
+
+ require.NotNil(t, entry)
+
+ if message != nil {
+ assert.Regexp(t, message, entry.Message)
+ }
+
+ v, ok := entry.Data["error"]
+
+ if err == nil {
+ assert.False(t, ok)
+ assert.Nil(t, v)
+ } else {
+ assert.True(t, ok)
+ require.NotNil(t, v)
+
+ theErr, ok := v.(error)
+ assert.True(t, ok)
+ require.NotNil(t, theErr)
+
+ assert.Regexp(t, err, theErr.Error())
+ }
+}
+
+func (m *MockAutheliaCtx) AssertLastLogMessage(t *testing.T, message, err string) {
+ entry := m.Hook.LastEntry()
+
+ require.NotNil(t, entry)
+
+ assert.Equal(t, message, entry.Message)
+
+ v, ok := entry.Data["error"]
+
+ if err == "" {
+ assert.False(t, ok)
+ assert.Nil(t, v)
+ } else {
+ assert.True(t, ok)
+ require.NotNil(t, v)
+
+ theErr, ok := v.(error)
+ assert.True(t, ok)
+ require.NotNil(t, theErr)
+
+ assert.EqualError(t, theErr, err)
+ }
+}
+
// GetResponseData retrieves a response from the service.
func (m *MockAutheliaCtx) GetResponseData(t *testing.T, data interface{}) {
okResponse := middlewares.OKResponse{}
diff --git a/internal/mocks/storage.go b/internal/mocks/storage.go
index f3ac04a59..fe70f4339 100644
--- a/internal/mocks/storage.go
+++ b/internal/mocks/storage.go
@@ -717,7 +717,7 @@ func (mr *MockStorageMockRecorder) SaveOAuth2ConsentPreConfiguration(ctx, config
}
// SaveOAuth2ConsentSession mocks base method.
-func (m *MockStorage) SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) error {
+func (m *MockStorage) SaveOAuth2ConsentSession(ctx context.Context, consent *model.OAuth2ConsentSession) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveOAuth2ConsentSession", ctx, consent)
ret0, _ := ret[0].(error)
@@ -745,7 +745,7 @@ func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSessionGranted(ctx, id any)
}
// SaveOAuth2ConsentSessionResponse mocks base method.
-func (m *MockStorage) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, rejection bool) error {
+func (m *MockStorage) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent *model.OAuth2ConsentSession, rejection bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveOAuth2ConsentSessionResponse", ctx, consent, rejection)
ret0, _ := ret[0].(error)
@@ -759,7 +759,7 @@ func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSessionResponse(ctx, consent
}
// SaveOAuth2ConsentSessionSubject mocks base method.
-func (m *MockStorage) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) error {
+func (m *MockStorage) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent *model.OAuth2ConsentSession) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveOAuth2ConsentSessionSubject", ctx, consent)
ret0, _ := ret[0].(error)
diff --git a/internal/oidc/const.go b/internal/oidc/const.go
index 9b9e66ec6..d0e483295 100644
--- a/internal/oidc/const.go
+++ b/internal/oidc/const.go
@@ -173,6 +173,7 @@ const (
FormParameterScope = valueScope
FormParameterIssuer = valueIss
FormParameterPrompt = "prompt"
+ FormParameterMaximumAge = "max_age"
)
const (
@@ -212,7 +213,10 @@ const (
// Paths.
const (
- EndpointPathConsent = "/consent"
+ EndpointPathConsent = "/consent/openid"
+ EndpointPathConsentDecision = EndpointPathConsent + "/decision"
+ EndpointPathConsentLogin = EndpointPathConsent + "/login"
+
EndpointPathWellKnownOpenIDConfiguration = "/.well-known/openid-configuration"
EndpointPathWellKnownOAuthAuthorizationServer = "/.well-known/oauth-authorization-server"
EndpointPathJWKs = "/jwks.json"
diff --git a/internal/oidc/session.go b/internal/oidc/session.go
index de0b7c53f..b747c5437 100644
--- a/internal/oidc/session.go
+++ b/internal/oidc/session.go
@@ -84,7 +84,7 @@ type Session struct {
}
// GetChallengeID returns the challenge id.
-func (s *Session) GetChallengeID() uuid.NullUUID {
+func (s *Session) GetChallengeID() (challenge uuid.NullUUID) {
return s.ChallengeID
}
@@ -157,7 +157,7 @@ func (s *Session) GetJWTClaims() jwt.JWTClaimsContainer {
}
// GetIDTokenClaims returns the *jwt.IDTokenClaims for this session.
-func (s *Session) GetIDTokenClaims() *jwt.IDTokenClaims {
+func (s *Session) GetIDTokenClaims() (claims *jwt.IDTokenClaims) {
if s.DefaultSession == nil {
return nil
}
diff --git a/internal/oidc/types.go b/internal/oidc/types.go
index 7db01b671..387405d23 100644
--- a/internal/oidc/types.go
+++ b/internal/oidc/types.go
@@ -141,6 +141,9 @@ type Client interface {
GetRequirePushedAuthorizationRequests() (enforce bool)
+ GetJSONWebKeys() (jwks *jose.JSONWebKeySet)
+ GetJSONWebKeysURI() (uri string)
+
GetEnforcePKCE() (enforce bool)
GetEnforcePKCEChallengeMethod() (enforce bool)
GetPKCEChallengeMethod() (method string)
diff --git a/internal/oidc/util.go b/internal/oidc/util.go
index 26382dce0..3b3b22d12 100644
--- a/internal/oidc/util.go
+++ b/internal/oidc/util.go
@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"sort"
+ "strconv"
"strings"
"time"
@@ -368,6 +369,45 @@ func IsMaybeSignedJWT(value string) (is bool) {
return strings.Count(value, ".") == 2
}
+// RequesterRequiresLogin returns true if the oauthelia2.Requester requires the user to authenticate again.
+func RequesterRequiresLogin(requester oauthelia2.Requester, requested, authenticated time.Time) (required bool) {
+ if requester == nil {
+ return false
+ }
+
+ if _, ok := requester.(oauthelia2.DeviceAuthorizeRequester); ok {
+ return false
+ }
+
+ return RequestFormRequiresLogin(requester.GetRequestForm(), requested, authenticated)
+}
+
+// RequestFormRequiresLogin returns true if the form requires the user to authenticate again.
+func RequestFormRequiresLogin(form url.Values, requested, authenticated time.Time) (required bool) {
+ if form.Has(FormParameterPrompt) {
+ if oauthelia2.Arguments(oauthelia2.RemoveEmpty(strings.Split(form.Get(FormParameterPrompt), " "))).Has(PromptLogin) && authenticated.Before(requested) {
+ return true
+ }
+ }
+
+ if form.Has(FormParameterMaximumAge) {
+ value := form.Get(FormParameterMaximumAge)
+
+ var (
+ age int64
+ err error
+ )
+
+ if age, err = strconv.ParseInt(value, 10, 64); err != nil {
+ age = 0
+ }
+
+ return age == 0 || authenticated.IsZero() || requested.IsZero() || authenticated.Add(time.Duration(age)*time.Second).Before(requested)
+ }
+
+ return false
+}
+
func ValidateSectorIdentifierURI(ctx ClientContext, cache map[string][]string, sectorURI *url.URL, redirectURIs []string) (err error) {
var (
sectorRedirectURIs []string
diff --git a/internal/oidc/util_blackbox_test.go b/internal/oidc/util_blackbox_test.go
index ca110897c..619cbf87a 100644
--- a/internal/oidc/util_blackbox_test.go
+++ b/internal/oidc/util_blackbox_test.go
@@ -163,6 +163,115 @@ func TestGetLangFromRequester(t *testing.T) {
}
}
+func TestRequesterRequiresLogin(t *testing.T) {
+ testCases := []struct {
+ name string
+ have oauthelia2.Requester
+ requested, authenticated int64
+ expected bool
+ }{
+ {
+ name: "ShouldNotRequireWithoutPrompt",
+ have: &oauthelia2.Request{
+ Form: url.Values{},
+ },
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldHandleNilForm",
+ have: &oauthelia2.Request{},
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldHandleNil",
+ have: nil,
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldNotRequireWithPromptNone",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterPrompt: []string{oidc.PromptNone}},
+ },
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldNotRequireWithPromptNonePastAuthenticated",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterPrompt: []string{oidc.PromptNone}},
+ },
+ requested: 100000,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldNotRequireWithPromptLogin",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterPrompt: []string{oidc.PromptLogin}},
+ },
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldRequireWithPromptLoginPastAuthenticated",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterPrompt: []string{oidc.PromptLogin}},
+ },
+ requested: 100000,
+ authenticated: 0,
+ expected: true,
+ },
+ {
+ name: "ShouldNotRequireWithMaxAge",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterMaximumAge: []string{"100"}},
+ },
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ {
+ name: "ShouldRequireWithMaxAgePastAuthenticated",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterMaximumAge: []string{"100"}},
+ },
+ requested: 100000,
+ authenticated: 0,
+ expected: true,
+ },
+ {
+ name: "ShouldRequireWithMaxAgePastAuthenticatedInvalid",
+ have: &oauthelia2.Request{
+ Form: url.Values{oidc.FormParameterMaximumAge: []string{"not100"}},
+ },
+ requested: 1,
+ authenticated: 0,
+ expected: true,
+ },
+ {
+ name: "ShouldHandleDeviceCode",
+ have: &oauthelia2.DeviceAuthorizeRequest{},
+ requested: 0,
+ authenticated: 0,
+ expected: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, oidc.RequesterRequiresLogin(tc.have, time.Unix(tc.requested, 0), time.Unix(tc.authenticated, 0)))
+ })
+ }
+}
+
type TestGetLangRequester struct {
}
diff --git a/internal/server/handlers.go b/internal/server/handlers.go
index 8de19c571..8e16872d6 100644
--- a/internal/server/handlers.go
+++ b/internal/server/handlers.go
@@ -252,6 +252,7 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
delayFunc := middlewares.TimingAttackDelay(10, 250, 85, time.Second, true)
r.POST("/api/firstfactor", middlewareAPI(handlers.FirstFactorPOST(delayFunc)))
+ r.POST("/api/firstfactor/reauthenticate", middleware1FA(handlers.FirstFactorReauthenticatePOST(delayFunc)))
r.POST("/api/logout", middlewareAPI(handlers.LogoutPOST))
// Only register endpoints if forgot password is not disabled.
diff --git a/internal/session/user_session.go b/internal/session/user_session.go
index 958f25957..4c0f7ae90 100644
--- a/internal/session/user_session.go
+++ b/internal/session/user_session.go
@@ -24,12 +24,17 @@ func (s *UserSession) IsAnonymous() bool {
// SetOneFactor sets the 1FA AMR's and expected property values for one factor authentication.
func (s *UserSession) SetOneFactor(now time.Time, details *authentication.UserDetails, keepMeLoggedIn bool) {
- s.FirstFactorAuthnTimestamp = now.Unix()
- s.LastActivity = now.Unix()
s.AuthenticationLevel = authentication.OneFactor
s.KeepMeLoggedIn = keepMeLoggedIn
+ s.SetOneFactorReauthenticate(now, details)
+}
+
+func (s *UserSession) SetOneFactorReauthenticate(now time.Time, details *authentication.UserDetails) {
+ s.FirstFactorAuthnTimestamp = now.Unix()
+ s.LastActivity = now.Unix()
+
s.Username = details.Username
s.DisplayName = details.DisplayName
s.Groups = details.Groups
@@ -72,18 +77,34 @@ func (s *UserSession) SetTwoFactorWebAuthn(now time.Time, hardware, userPresence
s.WebAuthn = nil
}
+func (s *UserSession) GetFirstFactorAuthn() time.Time {
+ return time.Unix(s.FirstFactorAuthnTimestamp, 0).UTC()
+}
+
+func (s *UserSession) GetSecondFactorAuthn() time.Time {
+ return time.Unix(s.SecondFactorAuthnTimestamp, 0).UTC()
+}
+
// AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level.
func (s *UserSession) AuthenticatedTime(level authorization.Level) (authenticatedTime time.Time, err error) {
switch level {
case authorization.OneFactor:
- return time.Unix(s.FirstFactorAuthnTimestamp, 0).UTC(), nil
+ return s.GetFirstFactorAuthn(), nil
case authorization.TwoFactor:
- return time.Unix(s.SecondFactorAuthnTimestamp, 0).UTC(), nil
+ return s.GetSecondFactorAuthn(), nil
default:
return time.Unix(0, 0).UTC(), errors.New("invalid authorization level")
}
}
+func (s *UserSession) LastAuthenticatedTime() (authenticated time.Time) {
+ if s.FirstFactorAuthnTimestamp > s.SecondFactorAuthnTimestamp {
+ return s.GetFirstFactorAuthn()
+ }
+
+ return s.GetSecondFactorAuthn()
+}
+
// Identity value of the user session.
func (s *UserSession) Identity() Identity {
identity := Identity{
diff --git a/internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.down.sql b/internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.down.sql
new file mode 100644
index 000000000..e2f6ee5e0
--- /dev/null
+++ b/internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.down.sql
@@ -0,0 +1,5 @@
+DELETE FROM oauth2_consent_session
+WHERE subject IS NULL;
+
+ALTER TABLE oauth2_consent_session
+ MODIFY subject CHAR(36) NOT NULL;
diff --git a/internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.up.sql b/internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.up.sql
new file mode 100644
index 000000000..fe97e2beb
--- /dev/null
+++ b/internal/storage/migrations/mysql/V0016.OAuth2ConsentSubjectNULL.up.sql
@@ -0,0 +1,2 @@
+ALTER TABLE oauth2_consent_session
+ MODIFY subject CHAR(36) NULL DEFAULT NULL;
diff --git a/internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.down.sql b/internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.down.sql
new file mode 100644
index 000000000..7e99449ec
--- /dev/null
+++ b/internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.down.sql
@@ -0,0 +1,6 @@
+DELETE FROM oauth2_consent_session
+WHERE subject IS NULL;
+
+ALTER TABLE oauth2_consent_session
+ ALTER COLUMN subject SET NOT NULL,
+ ALTER COLUMN subject DROP DEFAULT;
diff --git a/internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.up.sql b/internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.up.sql
new file mode 100644
index 000000000..c45544d39
--- /dev/null
+++ b/internal/storage/migrations/postgres/V0016.OAuth2ConsentSubjectNULL.up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE oauth2_consent_session
+ ALTER COLUMN subject DROP NOT NULL,
+ ALTER COLUMN subject SET DEFAULT NULL;
diff --git a/internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.down.sql b/internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.down.sql
new file mode 100644
index 000000000..76196c01c
--- /dev/null
+++ b/internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.down.sql
@@ -0,0 +1,382 @@
+PRAGMA foreign_keys=off;
+
+CREATE TABLE IF NOT EXISTS _bkp_DOWN_V0016_oauth2_consent_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ subject CHAR(36) NULL DEFAULT NULL,
+ authorized BOOLEAN NOT NULL DEFAULT FALSE,
+ granted BOOLEAN NOT NULL DEFAULT FALSE,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ responded_at DATETIME NULL DEFAULT NULL,
+ form_data TEXT NOT NULL,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ preconfiguration INTEGER NULL DEFAULT NULL
+);
+
+INSERT INTO _bkp_DOWN_V0016_oauth2_consent_session (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration)
+SELECT challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration
+FROM oauth2_consent_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_consent_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_DOWN_V0016_oauth2_authorization_code_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_DOWN_V0016_oauth2_authorization_code_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_authorization_code_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_authorization_code_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_DOWN_V0016_oauth2_access_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NULL DEFAULT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_DOWN_V0016_oauth2_access_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_access_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_access_token_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_DOWN_V0016_oauth2_refresh_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_DOWN_V0016_oauth2_refresh_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_refresh_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_refresh_token_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_DOWN_V0016_oauth2_pkce_request_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_DOWN_V0016_oauth2_pkce_request_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_pkce_request_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_pkce_request_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_DOWN_V0016_oauth2_openid_connect_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_DOWN_V0016_oauth2_openid_connect_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_openid_connect_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_openid_connect_session;
+
+DROP INDEX IF EXISTS oauth2_consent_session_challenge_id_key;
+
+CREATE TABLE IF NOT EXISTS oauth2_consent_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ authorized BOOLEAN NOT NULL DEFAULT FALSE,
+ granted BOOLEAN NOT NULL DEFAULT FALSE,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ responded_at DATETIME NULL DEFAULT NULL,
+ form_data TEXT NOT NULL,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ preconfiguration INTEGER NULL DEFAULT NULL,
+ CONSTRAINT oauth2_consent_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT,
+ CONSTRAINT oauth2_consent_session_preconfiguration_fkey
+ FOREIGN KEY (preconfiguration)
+ REFERENCES oauth2_consent_preconfiguration (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id);
+
+INSERT INTO oauth2_consent_session (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration)
+SELECT challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration
+FROM _bkp_DOWN_V0016_oauth2_consent_session
+WHERE subject IS NOT NULL
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_DOWN_V0016_oauth2_consent_session;
+
+DROP INDEX IF EXISTS oauth2_authorization_code_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_authorization_code_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_authorization_code_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_authorization_code_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_authorization_code_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id);
+CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id);
+CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject);
+
+INSERT INTO oauth2_authorization_code_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_DOWN_V0016_oauth2_authorization_code_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_DOWN_V0016_oauth2_authorization_code_session;
+
+DROP INDEX IF EXISTS oauth2_access_token_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_access_token_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_access_token_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NULL DEFAULT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(768) NOT NULL,
+ subject CHAR(36) NULL DEFAULT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_access_token_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_access_token_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id);
+CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id);
+CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject);
+
+INSERT INTO oauth2_access_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_DOWN_V0016_oauth2_access_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_DOWN_V0016_oauth2_access_token_session;
+
+DROP INDEX IF EXISTS oauth2_refresh_token_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_refresh_token_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_refresh_token_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_refresh_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_refresh_token_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id);
+CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id);
+CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject);
+
+INSERT INTO oauth2_refresh_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_DOWN_V0016_oauth2_refresh_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_DOWN_V0016_oauth2_refresh_token_session;
+
+DROP INDEX IF EXISTS oauth2_pkce_request_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_pkce_request_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_pkce_request_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_pkce_request_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_pkce_request_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id);
+CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id);
+CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject);
+
+INSERT INTO oauth2_pkce_request_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_DOWN_V0016_oauth2_pkce_request_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_DOWN_V0016_oauth2_pkce_request_session;
+
+DROP INDEX IF EXISTS oauth2_openid_connect_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_openid_connect_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_openid_connect_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_openid_connect_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_openid_connect_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id);
+CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id);
+CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject);
+
+INSERT INTO oauth2_openid_connect_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_DOWN_V0016_oauth2_openid_connect_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_DOWN_V0016_oauth2_openid_connect_session;
+
+PRAGMA foreign_keys=on;
diff --git a/internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.up.sql b/internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.up.sql
new file mode 100644
index 000000000..633ed83e8
--- /dev/null
+++ b/internal/storage/migrations/sqlite/V0016.OAuth2ConsentSubjectNULL.up.sql
@@ -0,0 +1,381 @@
+PRAGMA foreign_keys=off;
+
+CREATE TABLE IF NOT EXISTS _bkp_UP_V0016_oauth2_consent_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ authorized BOOLEAN NOT NULL DEFAULT FALSE,
+ granted BOOLEAN NOT NULL DEFAULT FALSE,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ responded_at DATETIME NULL DEFAULT NULL,
+ form_data TEXT NOT NULL,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ preconfiguration INTEGER NULL DEFAULT NULL
+);
+
+INSERT INTO _bkp_UP_V0016_oauth2_consent_session (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration)
+SELECT challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration
+FROM oauth2_consent_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_consent_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_UP_V0016_oauth2_authorization_code_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_UP_V0016_oauth2_authorization_code_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_authorization_code_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_authorization_code_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_UP_V0016_oauth2_access_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NULL DEFAULT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NULL DEFAULT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_UP_V0016_oauth2_access_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_access_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_access_token_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_UP_V0016_oauth2_refresh_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_UP_V0016_oauth2_refresh_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_refresh_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_refresh_token_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_UP_V0016_oauth2_pkce_request_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_UP_V0016_oauth2_pkce_request_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_pkce_request_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_pkce_request_session;
+
+CREATE TABLE IF NOT EXISTS _bkp_UP_V0016_oauth2_openid_connect_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL
+);
+
+INSERT INTO _bkp_UP_V0016_oauth2_openid_connect_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM oauth2_openid_connect_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS oauth2_openid_connect_session;
+
+DROP INDEX IF EXISTS oauth2_consent_session_challenge_id_key;
+
+CREATE TABLE IF NOT EXISTS oauth2_consent_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ subject CHAR(36) NULL DEFAULT NULL,
+ authorized BOOLEAN NOT NULL DEFAULT FALSE,
+ granted BOOLEAN NOT NULL DEFAULT FALSE,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ responded_at DATETIME NULL DEFAULT NULL,
+ form_data TEXT NOT NULL,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ preconfiguration INTEGER NULL DEFAULT NULL,
+ CONSTRAINT oauth2_consent_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT,
+ CONSTRAINT oauth2_consent_session_preconfiguration_fkey
+ FOREIGN KEY (preconfiguration)
+ REFERENCES oauth2_consent_preconfiguration (id) ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id);
+
+INSERT INTO oauth2_consent_session (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration)
+SELECT challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, form_data, requested_scopes, granted_scopes, requested_audience, granted_audience, preconfiguration
+FROM _bkp_UP_V0016_oauth2_consent_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_UP_V0016_oauth2_consent_session;
+
+DROP INDEX IF EXISTS oauth2_authorization_code_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_authorization_code_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_authorization_code_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_authorization_code_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_authorization_code_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id);
+CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id);
+CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject);
+
+INSERT INTO oauth2_authorization_code_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_UP_V0016_oauth2_authorization_code_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_UP_V0016_oauth2_authorization_code_session;
+
+DROP INDEX IF EXISTS oauth2_access_token_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_access_token_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_access_token_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_access_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NULL DEFAULT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(768) NOT NULL,
+ subject CHAR(36) NULL DEFAULT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_access_token_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_access_token_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id);
+CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id);
+CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject);
+
+INSERT INTO oauth2_access_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_UP_V0016_oauth2_access_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_UP_V0016_oauth2_access_token_session;
+
+DROP INDEX IF EXISTS oauth2_refresh_token_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_refresh_token_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_refresh_token_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_refresh_token_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_refresh_token_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id);
+CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id);
+CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject);
+
+INSERT INTO oauth2_refresh_token_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_UP_V0016_oauth2_refresh_token_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_UP_V0016_oauth2_refresh_token_session;
+
+DROP INDEX IF EXISTS oauth2_pkce_request_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_pkce_request_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_pkce_request_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_pkce_request_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_pkce_request_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id);
+CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id);
+CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject);
+
+INSERT INTO oauth2_pkce_request_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_UP_V0016_oauth2_pkce_request_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_UP_V0016_oauth2_pkce_request_session;
+
+DROP INDEX IF EXISTS oauth2_openid_connect_session_request_id_idx;
+DROP INDEX IF EXISTS oauth2_openid_connect_session_client_id_idx;
+DROP INDEX IF EXISTS oauth2_openid_connect_session_client_id_subject_idx;
+
+CREATE TABLE IF NOT EXISTS oauth2_openid_connect_session (
+ id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+ challenge_id CHAR(36) NOT NULL,
+ request_id VARCHAR(40) NOT NULL,
+ client_id VARCHAR(255) NOT NULL,
+ signature VARCHAR(255) NOT NULL,
+ subject CHAR(36) NOT NULL,
+ requested_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ requested_scopes TEXT NOT NULL,
+ granted_scopes TEXT NOT NULL,
+ requested_audience TEXT NULL DEFAULT '',
+ granted_audience TEXT NULL DEFAULT '',
+ active BOOLEAN NOT NULL DEFAULT FALSE,
+ revoked BOOLEAN NOT NULL DEFAULT FALSE,
+ form_data TEXT NOT NULL,
+ session_data BLOB NOT NULL,
+ CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey
+ FOREIGN KEY (challenge_id)
+ REFERENCES oauth2_consent_session (challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT oauth2_openid_connect_session_subject_fkey
+ FOREIGN KEY (subject)
+ REFERENCES user_opaque_identifier (identifier) ON UPDATE CASCADE ON DELETE RESTRICT
+);
+
+CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id);
+CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id);
+CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject);
+
+INSERT INTO oauth2_openid_connect_session (challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data)
+SELECT challenge_id, request_id, client_id, signature, subject, requested_at, requested_scopes, granted_scopes, requested_audience, granted_audience, active, revoked, form_data, session_data
+FROM _bkp_UP_V0016_oauth2_openid_connect_session
+ORDER BY id;
+
+DROP TABLE IF EXISTS _bkp_UP_V0016_oauth2_openid_connect_session;
+
+PRAGMA foreign_keys=on;
diff --git a/internal/storage/migrations_test.go b/internal/storage/migrations_test.go
index 7cfedace2..ab47238da 100644
--- a/internal/storage/migrations_test.go
+++ b/internal/storage/migrations_test.go
@@ -11,7 +11,7 @@ import (
const (
// This is the latest schema version for the purpose of tests.
- LatestVersion = 15
+ LatestVersion = 16
)
func TestShouldObtainCorrectMigrations(t *testing.T) {
diff --git a/internal/storage/provider.go b/internal/storage/provider.go
index 0c461ccdc..b30bf45b5 100644
--- a/internal/storage/provider.go
+++ b/internal/storage/provider.go
@@ -197,13 +197,13 @@ type Provider interface {
*/
// SaveOAuth2ConsentSession inserts an OAuth2.0 consent session to the storage provider.
- SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) (err error)
+ SaveOAuth2ConsentSession(ctx context.Context, consent *model.OAuth2ConsentSession) (err error)
// SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session in the storage provider with the subject.
- SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) (err error)
+ SaveOAuth2ConsentSessionSubject(ctx context.Context, consent *model.OAuth2ConsentSession) (err error)
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session in the storage provider with the response.
- SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, rejection bool) (err error)
+ SaveOAuth2ConsentSessionResponse(ctx context.Context, consent *model.OAuth2ConsentSession, rejection bool) (err error)
// SaveOAuth2ConsentSessionGranted updates an OAuth2.0 consent session in the storage provider recording that it
// has been granted by the authorization endpoint.
@@ -232,7 +232,7 @@ type Provider interface {
// DeactivateOAuth2SessionByRequestID marks an OAuth2.0 session as inactive in the storage provider.
DeactivateOAuth2SessionByRequestID(ctx context.Context, sessionType OAuth2SessionType, requestID string) (err error)
- // LoadOAuth2Session saves an OAuth2.0 session from the storage provider.
+ // LoadOAuth2Session loads an OAuth2.0 session from the storage provider.
LoadOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (session *model.OAuth2Session, err error)
/*
diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go
index 21c84cfe2..b5e212a28 100644
--- a/internal/storage/sql_provider.go
+++ b/internal/storage/sql_provider.go
@@ -1010,7 +1010,7 @@ func (p *SQLProvider) LoadOAuth2ConsentPreConfigurations(ctx context.Context, cl
}
// SaveOAuth2ConsentSession inserts an OAuth2.0 consent session to the storage provider.
-func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {
+func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent *model.OAuth2ConsentSession) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlInsertOAuth2ConsentSession,
consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted,
consent.RequestedAt, consent.RespondedAt, consent.Form,
@@ -1022,7 +1022,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent mode
}
// SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session in the storage provider with the subject.
-func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {
+func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent *model.OAuth2ConsentSession) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionSubject, consent.Subject, consent.ID); err != nil {
return fmt.Errorf("error updating oauth2 consent session subject with id '%d' and challenge id '%s' for subject '%s': %w", consent.ID, consent.ChallengeID, consent.Subject.UUID, err)
}
@@ -1031,7 +1031,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, conse
}
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session in the storage provider with the response.
-func (p *SQLProvider) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, authorized bool) (err error) {
+func (p *SQLProvider) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent *model.OAuth2ConsentSession, authorized bool) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.GrantedScopes, consent.GrantedAudience, consent.PreConfiguration, consent.ID); err != nil {
return fmt.Errorf("error updating oauth2 consent session (authorized '%t') with id '%d' and challenge id '%s' for subject '%s': %w", authorized, consent.ID, consent.ChallengeID, consent.Subject.UUID, err)
}
@@ -1090,7 +1090,7 @@ func (p *SQLProvider) SaveOAuth2Session(ctx context.Context, sessionType OAuth2S
session.Active, session.Revoked, session.Form, session.Session)
if err != nil {
- return fmt.Errorf("error inserting oauth2 %s session data with signature '%s' for subject '%s' and request id '%s' and challenge id '%s': %w", sessionType, session.Signature, session.Subject.String, session.RequestID, session.ChallengeID.UUID, err)
+ return fmt.Errorf("error inserting oauth2 %s session with signature '%s' for subject '%s' and request id '%s' and challenge id '%s': %w", sessionType, session.Signature, session.Subject.String, session.RequestID, session.ChallengeID.UUID, err)
}
return nil
@@ -1335,3 +1335,7 @@ func (p *SQLProvider) LoadAuthenticationLogs(ctx context.Context, username strin
return attempts, nil
}
+
+var (
+ _ Provider = (*SQLProvider)(nil)
+)