fix(handlers): consent session prevents standard flow (#3668)

This fixes an issue where consent sessions prevent the standard workflow.
This commit is contained in:
James Elliott 2022-07-26 15:43:39 +10:00 committed by GitHub
parent efe1facc35
commit b2cbcf3913
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 544 additions and 429 deletions

View File

@ -11,9 +11,9 @@ const (
// NotAuthenticated if the user is not authenticated yet. // NotAuthenticated if the user is not authenticated yet.
NotAuthenticated Level = iota NotAuthenticated Level = iota
// OneFactor if the user has passed first factor only. // OneFactor if the user has passed first factor only.
OneFactor Level = iota OneFactor
// TwoFactor if the user has passed two factors. // TwoFactor if the user has passed two factors.
TwoFactor Level = iota TwoFactor
) )
const ( const (

View File

@ -27,7 +27,7 @@ func NewAccessControlRule(pos int, rule schema.ACLRule, networksMap map[string][
Methods: schemaMethodsToACL(rule.Methods), Methods: schemaMethodsToACL(rule.Methods),
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap), Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
Subjects: schemaSubjectsToACL(rule.Subjects), Subjects: schemaSubjectsToACL(rule.Subjects),
Policy: PolicyToLevel(rule.Policy), Policy: StringToLevel(rule.Policy),
} }
} }

View File

@ -9,39 +9,48 @@ import (
type Authorizer struct { type Authorizer struct {
defaultPolicy Level defaultPolicy Level
rules []*AccessControlRule rules []*AccessControlRule
mfa bool
configuration *schema.Configuration configuration *schema.Configuration
} }
// NewAuthorizer create an instance of authorizer with a given access control configuration. // NewAuthorizer create an instance of authorizer with a given access control configuration.
func NewAuthorizer(configuration *schema.Configuration) *Authorizer { func NewAuthorizer(configuration *schema.Configuration) (authorizer *Authorizer) {
return &Authorizer{ authorizer = &Authorizer{
defaultPolicy: PolicyToLevel(configuration.AccessControl.DefaultPolicy), defaultPolicy: StringToLevel(configuration.AccessControl.DefaultPolicy),
rules: NewAccessControlRules(configuration.AccessControl), rules: NewAccessControlRules(configuration.AccessControl),
configuration: configuration, configuration: configuration,
} }
}
// IsSecondFactorEnabled return true if at least one policy is set to second factor. if authorizer.defaultPolicy == TwoFactor {
func (p Authorizer) IsSecondFactorEnabled() bool { authorizer.mfa = true
if p.defaultPolicy == TwoFactor {
return true return authorizer
} }
for _, rule := range p.rules { for _, rule := range authorizer.rules {
if rule.Policy == TwoFactor { if rule.Policy == TwoFactor {
return true authorizer.mfa = true
return authorizer
} }
} }
if p.configuration.IdentityProviders.OIDC != nil { if authorizer.configuration.IdentityProviders.OIDC != nil {
for _, client := range p.configuration.IdentityProviders.OIDC.Clients { for _, client := range authorizer.configuration.IdentityProviders.OIDC.Clients {
if client.Policy == twoFactor { if client.Policy == twoFactor {
return true authorizer.mfa = true
return authorizer
} }
} }
} }
return false return authorizer
}
// IsSecondFactorEnabled return true if at least one policy is set to second factor.
func (p Authorizer) IsSecondFactorEnabled() bool {
return p.mfa
} }
// GetRequiredLevel retrieve the required level of authorization to access the object. // GetRequiredLevel retrieve the required level of authorization to access the object.

View File

@ -865,12 +865,12 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
} }
func (s *AuthorizerSuite) TestPolicyToLevel() { func (s *AuthorizerSuite) TestPolicyToLevel() {
s.Assert().Equal(Bypass, PolicyToLevel(bypass)) s.Assert().Equal(Bypass, StringToLevel(bypass))
s.Assert().Equal(OneFactor, PolicyToLevel(oneFactor)) s.Assert().Equal(OneFactor, StringToLevel(oneFactor))
s.Assert().Equal(TwoFactor, PolicyToLevel(twoFactor)) s.Assert().Equal(TwoFactor, StringToLevel(twoFactor))
s.Assert().Equal(Denied, PolicyToLevel(deny)) s.Assert().Equal(Denied, StringToLevel(deny))
s.Assert().Equal(Denied, PolicyToLevel("whatever")) s.Assert().Equal(Denied, StringToLevel("whatever"))
} }
func TestRunSuite(t *testing.T) { func TestRunSuite(t *testing.T) {
@ -929,7 +929,8 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithNoOIDC(t *testing.T) {
authorizer := NewAuthorizer(config) authorizer := NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled()) assert.False(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = TwoFactor config.AccessControl.Rules[0].Policy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled()) assert.True(t, authorizer.IsSecondFactorEnabled())
} }
@ -958,22 +959,24 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithOIDC(t *testing.T) {
authorizer := NewAuthorizer(config) authorizer := NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled()) assert.False(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = TwoFactor config.AccessControl.Rules[0].Policy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled()) assert.True(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = OneFactor config.AccessControl.Rules[0].Policy = oneFactor
authorizer = NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled()) assert.False(t, authorizer.IsSecondFactorEnabled())
config.IdentityProviders.OIDC.Clients[0].Policy = twoFactor config.IdentityProviders.OIDC.Clients[0].Policy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled()) assert.True(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = OneFactor config.AccessControl.Rules[0].Policy = oneFactor
config.IdentityProviders.OIDC.Clients[0].Policy = oneFactor config.IdentityProviders.OIDC.Clients[0].Policy = oneFactor
authorizer = NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled()) assert.False(t, authorizer.IsSecondFactorEnabled())
authorizer.defaultPolicy = TwoFactor config.AccessControl.DefaultPolicy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled()) assert.True(t, authorizer.IsSecondFactorEnabled())
} }

View File

@ -7,11 +7,11 @@ const (
// Bypass bypass level. // Bypass bypass level.
Bypass Level = iota Bypass Level = iota
// OneFactor one factor level. // OneFactor one factor level.
OneFactor Level = iota OneFactor
// TwoFactor two factor level. // TwoFactor two factor level.
TwoFactor Level = iota TwoFactor
// Denied denied level. // Denied denied level.
Denied Level = iota Denied
) )
const ( const (

View File

@ -9,8 +9,8 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
// PolicyToLevel converts a string policy to int authorization level. // StringToLevel converts a string policy to int authorization level.
func PolicyToLevel(policy string) Level { func StringToLevel(policy string) Level {
switch policy { switch policy {
case bypass: case bypass:
return Bypass return Bypass
@ -25,8 +25,8 @@ func PolicyToLevel(policy string) Level {
return Denied return Denied
} }
// LevelToPolicy converts a int authorization level to string policy. // LevelToString converts a int authorization level to string policy.
func LevelToPolicy(level Level) (policy string) { func LevelToString(level Level) (policy string) {
switch level { switch level {
case Bypass: case Bypass:
return bypass return bypass

View File

@ -11,6 +11,25 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
func TestLevelToString(t *testing.T) {
testCases := []struct {
have Level
expected string
}{
{Bypass, "bypass"},
{OneFactor, "one_factor"},
{TwoFactor, "two_factor"},
{Denied, "deny"},
{99, "deny"},
}
for _, tc := range testCases {
t.Run("Expected_"+tc.expected, func(t *testing.T) {
assert.Equal(t, tc.expected, LevelToString(tc.have))
})
}
}
func TestShouldNotParseInvalidSubjects(t *testing.T) { func TestShouldNotParseInvalidSubjects(t *testing.T) {
subjectsSchema := [][]string{{"groups:z"}, {"group:z", "users:b"}} subjectsSchema := [][]string{{"groups:z"}, {"group:z", "users:b"}}
subjectsACL := schemaSubjectsToACL(subjectsSchema) subjectsACL := schemaSubjectsToACL(subjectsSchema)
@ -184,7 +203,7 @@ func TestShouldParseACLNetworks(t *testing.T) {
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"]) assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"])
} }
func TestShouldReturnCorrectValidationLevel(t *testing.T) { func TestIsAuthLevelSufficient(t *testing.T) {
assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Denied)) assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Denied))
assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, Denied)) assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, Denied))
assert.False(t, IsAuthLevelSufficient(authentication.TwoFactor, Denied)) assert.False(t, IsAuthLevelSufficient(authentication.TwoFactor, Denied))

View File

@ -167,11 +167,11 @@ func accessControlCheckWriteOutput(object authorization.Object, subject authoriz
switch { switch {
case appliedPos != 0 && (potentialPos == 0 || (potentialPos > appliedPos)): case appliedPos != 0 && (potentialPos == 0 || (potentialPos > appliedPos)):
fmt.Printf("\nThe policy '%s' from rule #%d will be applied to this request.\n\n", authorization.LevelToPolicy(applied.Rule.Policy), appliedPos) fmt.Printf("\nThe policy '%s' from rule #%d will be applied to this request.\n\n", authorization.LevelToString(applied.Rule.Policy), appliedPos)
case potentialPos != 0 && appliedPos != 0: case potentialPos != 0 && appliedPos != 0:
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. If not policy '%s' from rule #%d will be.\n\n", authorization.LevelToPolicy(potential.Rule.Policy), potentialPos, authorization.LevelToPolicy(applied.Rule.Policy), appliedPos) fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. If not policy '%s' from rule #%d will be.\n\n", authorization.LevelToString(potential.Rule.Policy), potentialPos, authorization.LevelToString(applied.Rule.Policy), appliedPos)
case potentialPos != 0: case potentialPos != 0:
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. Otherwise the policy '%s' from the default policy will be.\n\n", authorization.LevelToPolicy(potential.Rule.Policy), potentialPos, defaultPolicy) fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. Otherwise the policy '%s' from the default policy will be.\n\n", authorization.LevelToString(potential.Rule.Policy), potentialPos, defaultPolicy)
default: default:
fmt.Printf("\nThe policy '%s' from the default policy will be applied to this request as no rules matched the request.\n\n", defaultPolicy) fmt.Printf("\nThe policy '%s' from the default policy will be applied to this request as no rules matched the request.\n\n", defaultPolicy)
} }

View File

@ -47,6 +47,10 @@ const (
messagePasswordWeak = "Your supplied password does not meet the password policy requirements" messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
) )
const (
workflowOpenIDConnect = "openid_connect"
)
const ( const (
logFmtErrParseRequestBody = "Failed to parse %s request body: %+v" logFmtErrParseRequestBody = "Failed to parse %s request body: %+v"
logFmtErrWriteResponseBody = "Failed to write %s response body for user '%s': %+v" logFmtErrWriteResponseBody = "Failed to write %s response body for user '%s': %+v"
@ -72,11 +76,6 @@ const (
auth = "auth" auth = "auth"
) )
const (
accept = "accept"
reject = "reject"
)
const authPrefix = "Basic " const authPrefix = "Basic "
const ldapPasswordComplexityCode = "0000052D." const ldapPasswordComplexityCode = "0000052D."

View File

@ -73,7 +73,6 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
userSession := ctx.GetSession() userSession := ctx.GetSession()
newSession := session.NewDefaultUserSession() newSession := session.NewDefaultUserSession()
newSession.ConsentChallengeID = userSession.ConsentChallengeID
// Reset all values from previous session except OIDC workflow before regenerating the cookie. // Reset all values from previous session except OIDC workflow before regenerating the cookie.
if err = ctx.SaveSession(newSession); err != nil { if err = ctx.SaveSession(newSession); err != nil {
@ -135,8 +134,8 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
successful = true successful = true
if userSession.ConsentChallengeID != nil { if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx) handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
} else { } else {
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups) Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
} }

View File

@ -61,26 +61,12 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
userSession := ctx.GetSession() userSession := ctx.GetSession()
var subject model.NullUUID
if userSession.Username != "" {
if subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject for user '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject."))
return
}
subject.Valid = true
}
var ( var (
consent *model.OAuth2ConsentSession consent *model.OAuth2ConsentSession
handled bool handled bool
) )
if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, subject, rw, r, requester); handled { if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, rw, r, requester); handled {
return return
} }

View File

@ -3,6 +3,8 @@ package handlers
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"path"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
@ -19,85 +21,98 @@ import (
) )
func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client, func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
userSession session.UserSession, subject model.NullUUID, userSession session.UserSession,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
if userSession.ConsentChallengeID != nil { var (
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to lookup consent by challenge id '%s'", requester.GetID(), client.GetID(), userSession.ConsentChallengeID) issuer *url.URL
subject uuid.UUID
err error
)
return handleOIDCAuthorizationConsentWithChallengeID(ctx, rootURI, client, userSession, rw, r, requester) if issuer, err = url.Parse(rootURI); err != nil {
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not safely determine the issuer."))
return nil, true
} }
if !subject.Valid { if !strings.HasSuffix(issuer.Path, "/") {
return handleOIDCAuthorizationConsentGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester) issuer.Path += "/"
} }
return handleOIDCAuthorizationConsentOrGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester) // This prevents the consent request from being generated until the authentication level is sufficient.
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) || userSession.Username == "" {
redirectURL := getOIDCAuthorizationRedirectURL(issuer, requester)
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected due to insufficient authentication", requester.GetID(), client.GetID())
http.Redirect(rw, r, redirectURL.String(), http.StatusFound)
return nil, true
}
if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject identifier for user '%s' and sector identifier '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, client.GetSectorIdentifier(), err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject."))
return nil, true
}
var consentIDBytes []byte
if consentIDBytes = ctx.QueryArgs().Peek("consent_id"); len(consentIDBytes) != 0 {
var consentID uuid.UUID
if consentID, err = uuid.Parse(string(consentIDBytes)); err != nil {
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Consent Session ID was Malformed."))
return nil, true
}
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to lookup consent by challenge id '%s'", requester.GetID(), client.GetID(), consentID)
return handleOIDCAuthorizationConsentWithChallengeID(ctx, issuer, client, userSession, subject, consentID, rw, r, requester)
}
return handleOIDCAuthorizationConsentGenerate(ctx, issuer, client, userSession, subject, rw, r, requester)
} }
func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client, func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client,
userSession session.UserSession, userSession session.UserSession, subject, challengeID uuid.UUID,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var ( var (
err error err error
) )
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); err != nil { if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, challengeID); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred during consent session lookup: %+v", requester.GetID(), requester.GetClient().GetID(), err) ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred during consent session lookup: %+v", requester.GetID(), requester.GetClient().GetID(), err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session.")) ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session."))
userSession.ConsentChallengeID = nil return nil, true
}
if err = ctx.SaveSession(userSession); err != nil { if err = verifyOIDCUserAuthorizedForConsent(ctx, client, userSession, consent, subject); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred unlinking consent session challenge id: %+v", requester.GetID(), requester.GetClient().GetID(), err) ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not process consent session with challenge id '%s': could not authorize the user user '%s' for this consent session: %v", requester.GetID(), client.GetID(), consent.ChallengeID, userSession.Username, err)
}
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("The user is not authorized to perform consent."))
return nil, true return nil, true
} }
if !consent.Subject.Valid {
if consent.Subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject for user '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject."))
return nil, true
}
consent.Subject.Valid = true
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, *consent); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred updating consent session subject for user '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not update the consent session subject."))
return nil, true
}
}
if consent.Responded() { if consent.Responded() {
userSession.ConsentChallengeID = nil
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving session: %+v", requester.GetID(), client.GetID(), err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the session."))
return nil, true
}
if consent.Granted { if consent.Granted {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID.String()) ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted.")) ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted."))
return nil, true return nil, true
} }
ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID.String(), client.GetID(), consent.Subject.String(), strings.Join(requester.GetRequestedScopes(), " ")) ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " "))
if consent.IsDenied() { if consent.IsDenied() {
ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID.String(), client.GetID(), consent.Subject.String(), strings.Join(requester.GetRequestedScopes(), " ")) ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " "))
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied) ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied)
@ -107,13 +122,13 @@ func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx,
return consent, false return consent, false
} }
handleOIDCAuthorizationConsentRedirect(ctx, rootURI, client, userSession, rw, r, requester) handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
return consent, true return consent, true
} }
func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client, func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client,
userSession session.UserSession, subject model.NullUUID, userSession session.UserSession, subject uuid.UUID,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) { rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var ( var (
err error err error
@ -121,7 +136,7 @@ func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, root
scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester) scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester)
if consent, err = getOIDCPreConfiguredConsent(ctx, client.GetID(), subject.UUID, scopes, audience); err != nil { if consent, err = getOIDCPreConfiguredConsent(ctx, client.GetID(), subject, scopes, audience); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err) ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup the consent session.")) ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup the consent session."))
@ -137,14 +152,6 @@ func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, root
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to generate a new consent due to unsuccessful lookup of pre-configured consent", requester.GetID(), client.GetID()) ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to generate a new consent due to unsuccessful lookup of pre-configured consent", requester.GetID(), client.GetID())
return handleOIDCAuthorizationConsentGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester)
}
func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
userSession session.UserSession, subject model.NullUUID,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var err error
if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil { if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err) ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err)
@ -161,34 +168,81 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, rootUR
return nil, true return nil, true
} }
userSession.ConsentChallengeID = &consent.ChallengeID handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving user session for consent: %+v", requester.GetID(), client.GetID(), err)
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the user session."))
return nil, true
}
handleOIDCAuthorizationConsentRedirect(ctx, rootURI, client, userSession, rw, r, requester)
return consent, true return consent, true
} }
func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, destination string, client *oidc.Client, func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer *url.URL, consent *model.OAuth2ConsentSession, client *oidc.Client,
userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) { userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) {
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { var location *url.URL
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is sufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToPolicy(client.Policy))
destination = fmt.Sprintf("%s/consent", destination) if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
location, _ = url.Parse(issuer.String())
location.Path = path.Join(location.Path, "/consent")
query := location.Query()
query.Set("consent_id", consent.ChallengeID.String())
location.RawQuery = query.Encode()
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is sufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy))
} else { } else {
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is insufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToPolicy(client.Policy)) location = getOIDCAuthorizationRedirectURL(issuer, requester)
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is insufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy))
} }
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected to '%s'", requester.GetID(), client.GetID(), destination) ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected to '%s'", requester.GetID(), client.GetID(), location)
http.Redirect(rw, r, destination, http.StatusFound) http.Redirect(rw, r, location.String(), http.StatusFound)
}
func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client *oidc.Client, userSession session.UserSession, consent *model.OAuth2ConsentSession, subject uuid.UUID) (err error) {
var sid, csid uint32
csid = consent.Subject.UUID.ID()
if !consent.Subject.Valid || csid == 0 {
return fmt.Errorf("the consent subject is null for consent session with id '%d'", consent.ID)
}
if client == nil {
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID); err != nil {
return fmt.Errorf("failed to retrieve client: %w", err)
}
}
if sid = subject.ID(); sid == 0 {
if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
return fmt.Errorf("failed to lookup subject: %w", err)
}
sid = subject.ID()
}
if csid != sid {
return fmt.Errorf("the consent subject identifier '%s' isn't owned by user '%s' who has a subject identifier of '%s' with sector identifier '%s'", consent.Subject.UUID, userSession.Username, subject, client.GetSectorIdentifier())
}
return nil
}
func getOIDCAuthorizationRedirectURL(issuer *url.URL, requester fosite.AuthorizeRequester) (redirectURL *url.URL) {
redirectURL, _ = url.Parse(issuer.String())
authorizationURL, _ := url.Parse(issuer.String())
authorizationURL.Path = path.Join(authorizationURL.Path, oidc.AuthorizationPath)
authorizationURL.RawQuery = requester.GetRequestForm().Encode()
query := redirectURL.Query()
query.Set("rd", authorizationURL.String())
query.Set("workflow", workflowOpenIDConnect)
redirectURL.RawQuery = query.Encode()
return redirectURL
} }
func getOIDCExpectedScopesAndAudienceFromRequest(requester fosite.Requester) (scopes, audience []string) { func getOIDCExpectedScopesAndAudienceFromRequest(requester fosite.Requester) (scopes, audience []string) {
@ -203,19 +257,6 @@ func getOIDCExpectedScopesAndAudience(clientID string, scopes, audience []string
return scopes, audience return scopes, audience
} }
func getOIDCPreConfiguredConsentFromClientAndConsent(ctx *middlewares.AutheliaCtx, client fosite.Client, consent *model.OAuth2ConsentSession) (preConfigConsent *model.OAuth2ConsentSession, err error) {
if consent == nil || !consent.Subject.Valid {
return nil, fmt.Errorf("invalid consent provided for pre-configured consent lookup")
}
scopes, audience := getOIDCExpectedScopesAndAudience(client.GetID(), consent.RequestedScopes, consent.RequestedAudience)
// We can skip this error as it's handled at the authorization endpoint.
preConfigConsent, _ = getOIDCPreConfiguredConsent(ctx, client.GetID(), consent.Subject.UUID, scopes, audience)
return preConfigConsent, nil
}
func getOIDCPreConfiguredConsent(ctx *middlewares.AutheliaCtx, clientID string, subject uuid.UUID, scopes, audience []string) (consent *model.OAuth2ConsentSession, err error) { func getOIDCPreConfiguredConsent(ctx *middlewares.AutheliaCtx, clientID string, subject uuid.UUID, scopes, audience []string) (consent *model.OAuth2ConsentSession, err error) {
var ( var (
rows *storage.ConsentSessionRows rows *storage.ConsentSessionRows

View File

@ -3,8 +3,13 @@ package handlers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"path"
"strings"
"time" "time"
"github.com/google/uuid"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
@ -14,7 +19,19 @@ import (
// OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect. // OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect.
func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) { func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx) var (
consentID uuid.UUID
err error
)
if consentID, err = uuid.Parse(string(ctx.RequestCtx.QueryArgs().Peek("consent_id"))); err != nil {
ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().Peek("consent_id"), err)
ctx.ReplyForbidden()
return
}
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx, consentID)
if handled { if handled {
return return
} }
@ -26,26 +43,35 @@ func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
return return
} }
if err := ctx.SetJSONBody(client.GetConsentResponseBody(consent)); err != nil { if err = ctx.SetJSONBody(client.GetConsentResponseBody(consent)); err != nil {
ctx.Error(fmt.Errorf("unable to set JSON body: %v", err), "Operation failed") ctx.Error(fmt.Errorf("unable to set JSON body: %v", err), "Operation failed")
} }
} }
//nolint:gocyclo
// OpenIDConnectConsentPOST handles consent responses for OpenID Connect. // OpenIDConnectConsentPOST handles consent responses for OpenID Connect.
func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) { func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
var ( var (
body oidc.ConsentPostRequestBody consentID uuid.UUID
err error bodyJSON oidc.ConsentPostRequestBody
err error
) )
if err = json.Unmarshal(ctx.Request.Body(), &body); err != nil { if err = json.Unmarshal(ctx.Request.Body(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Failed to parse JSON body in consent POST: %+v", err) ctx.Logger.Errorf("Failed to parse JSON bodyJSON in consent POST: %+v", err)
ctx.SetJSONError(messageOperationFailed) ctx.SetJSONError(messageOperationFailed)
return return
} }
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx) if consentID, err = uuid.Parse(bodyJSON.ConsentID); err != nil {
ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().Peek("consent_id"), err)
ctx.ReplyForbidden()
return
}
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx, consentID)
if handled { if handled {
return return
} }
@ -57,36 +83,23 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if consent.ClientID != body.ClientID { if consent.ClientID != bodyJSON.ClientID {
ctx.Logger.Errorf("User '%s' consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack", ctx.Logger.Errorf("User '%s' consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
userSession.Username, body.ClientID, consent.ClientID) userSession.Username, bodyJSON.ClientID, consent.ClientID)
ctx.SetJSONError(messageOperationFailed) ctx.SetJSONError(messageOperationFailed)
return return
} }
var ( if bodyJSON.Consent {
externalRootURL string if bodyJSON.PreConfigure {
authorized = true
)
switch body.AcceptOrReject {
case accept:
if externalRootURL, err = ctx.ExternalRootURL(); err != nil {
ctx.Logger.Errorf("Could not determine the external URL during consent session processing with challenge id '%s' for user '%s': %v", consent.ChallengeID.String(), userSession.Username, err)
ctx.SetJSONError(messageOperationFailed)
return
}
if body.PreConfigure {
if client.PreConfiguredConsentDuration == nil { if client.PreConfiguredConsentDuration == nil {
ctx.Logger.Warnf("Consent session with challenge id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID.String(), userSession.Username) ctx.Logger.Warnf("Consent session with id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID, userSession.Username)
} else { } else {
expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration) expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration)
consent.ExpiresAt = &expiresAt consent.ExpiresAt = &expiresAt
ctx.Logger.Debugf("Consent session with challenge id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID.String(), userSession.Username, consent.ExpiresAt) ctx.Logger.Debugf("Consent session with id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID, userSession.Username, consent.ExpiresAt)
} }
} }
@ -96,45 +109,68 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
if !utils.IsStringInSlice(consent.ClientID, consent.GrantedAudience) { if !utils.IsStringInSlice(consent.ClientID, consent.GrantedAudience) {
consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID) consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID)
} }
case reject: }
authorized = false
default: var externalRootURL string
ctx.Logger.Warnf("User '%s' tried to reply to consent with an unexpected verb '%s'", userSession.Username, body.AcceptOrReject)
ctx.ReplyBadRequest() if externalRootURL, err = ctx.ExternalRootURL(); err != nil {
ctx.Logger.Errorf("Could not determine the external URL during consent session processing with id '%s' for user '%s': %v", consent.ChallengeID, userSession.Username, err)
ctx.SetJSONError(messageOperationFailed)
return return
} }
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, authorized); 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.Logger.Errorf("Failed to save the consent session response to the database: %+v", err)
ctx.SetJSONError(messageOperationFailed) ctx.SetJSONError(messageOperationFailed)
return return
} }
response := oidc.ConsentPostResponseBody{RedirectURI: fmt.Sprintf("%s%s?%s", externalRootURL, oidc.AuthorizationPath, consent.Form)} var (
redirectURI *url.URL
query url.Values
)
if redirectURI, err = url.ParseRequestURI(externalRootURL); err != nil {
ctx.Logger.Errorf("Failed to parse the consent redirect URL: %+v", err)
ctx.SetJSONError(messageOperationFailed)
return
}
if !strings.HasSuffix(redirectURI.Path, "/") {
redirectURI.Path += "/"
}
if query, err = url.ParseQuery(consent.Form); err != nil {
ctx.Logger.Errorf("Failed to parse the consent form values: %+v", err)
ctx.SetJSONError(messageOperationFailed)
return
}
query.Set("consent_id", consent.ChallengeID.String())
redirectURI.Path = path.Join(redirectURI.Path, oidc.AuthorizationPath)
redirectURI.RawQuery = query.Encode()
response := oidc.ConsentPostResponseBody{RedirectURI: redirectURI.String()}
if err = ctx.SetJSONBody(response); err != nil { if err = ctx.SetJSONBody(response); err != nil {
ctx.Error(fmt.Errorf("unable to set JSON body in response"), "Operation failed") ctx.Error(fmt.Errorf("unable to set JSON bodyJSON in response"), "Operation failed")
} }
} }
func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx) (userSession session.UserSession, consent *model.OAuth2ConsentSession, client *oidc.Client, handled bool) { func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx, consentID uuid.UUID) (userSession session.UserSession, consent *model.OAuth2ConsentSession, client *oidc.Client, handled bool) {
var ( var (
err error err error
) )
userSession = ctx.GetSession() userSession = ctx.GetSession()
if userSession.ConsentChallengeID == nil { if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {
ctx.Logger.Errorf("Cannot consent for user '%s' when OIDC consent session has not been initiated", userSession.Username) ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", consentID, err)
ctx.ReplyForbidden()
return userSession, nil, nil, true
}
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); err != nil {
ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", userSession.ConsentChallengeID.String(), err)
ctx.ReplyForbidden() ctx.ReplyForbidden()
return userSession, nil, nil, true return userSession, nil, nil, true
@ -147,5 +183,13 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx) (userSession
return userSession, nil, nil, true return userSession, nil, nil, true
} }
if err = verifyOIDCUserAuthorizedForConsent(ctx, client, userSession, consent, uuid.UUID{}); err != nil {
ctx.Logger.Errorf("Could not authorize the user user '%s' for the consent session with challenge id '%s' on client with id '%s': %v", userSession.Username, consent.ChallengeID, client.GetID(), err)
ctx.ReplyForbidden()
return userSession, nil, nil, true
}
return userSession, consent, client, false return userSession, consent, client, false
} }

View File

@ -16,11 +16,11 @@ import (
func DuoPOST(duoAPI duo.API) middlewares.RequestHandler { func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) { return func(ctx *middlewares.AutheliaCtx) {
var ( var (
requestBody signDuoRequestBody bodyJSON = &signDuoRequestBody{}
device, method string device, method string
) )
if err := ctx.ParseBody(&requestBody); err != nil { if err := ctx.ParseBody(bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err) ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -35,10 +35,10 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
if err != nil { if err != nil {
ctx.Logger.Debugf("Error identifying preferred device for user %s: %s", userSession.Username, err) ctx.Logger.Debugf("Error identifying preferred device for user %s: %s", userSession.Username, err)
ctx.Logger.Debugf("Starting Duo PreAuth for initial device selection of user: %s", userSession.Username) ctx.Logger.Debugf("Starting Duo PreAuth for initial device selection of user: %s", userSession.Username)
device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, requestBody.TargetURL) device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, bodyJSON)
} else { } else {
ctx.Logger.Debugf("Starting Duo PreAuth to check preferred device of user: %s", userSession.Username) ctx.Logger.Debugf("Starting Duo PreAuth to check preferred device of user: %s", userSession.Username)
device, method, err = HandlePreferredDeviceCheck(ctx, &userSession, duoAPI, duoDevice.Device, duoDevice.Method, requestBody.TargetURL) device, method, err = HandlePreferredDeviceCheck(ctx, &userSession, duoAPI, duoDevice.Device, duoDevice.Method, bodyJSON)
} }
if err != nil { if err != nil {
@ -52,7 +52,7 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
ctx.Logger.Debugf("Starting Duo Auth attempt for %s with device %s and method %s from IP %s", userSession.Username, device, method, remoteIP) ctx.Logger.Debugf("Starting Duo Auth attempt for %s with device %s and method %s from IP %s", userSession.Username, device, method, remoteIP)
values, err := SetValues(userSession, device, method, remoteIP, requestBody.TargetURL, requestBody.Passcode) values, err := SetValues(userSession, device, method, remoteIP, bodyJSON.TargetURL, bodyJSON.Passcode)
if err != nil { if err != nil {
ctx.Logger.Errorf("Failed to set values for Duo Auth Call for user '%s': %+v", userSession.Username, err) ctx.Logger.Errorf("Failed to set values for Duo Auth Call for user '%s': %+v", userSession.Username, err)
@ -85,12 +85,12 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return return
} }
HandleAllow(ctx, requestBody.TargetURL) HandleAllow(ctx, bodyJSON)
} }
} }
// HandleInitialDeviceSelection handler for retrieving all available devices. // HandleInitialDeviceSelection handler for retrieving all available devices.
func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, targetURL string) (device string, method string, err error) { func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, bodyJSON *signDuoRequestBody) (device string, method string, err error) {
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI) result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
if err != nil { if err != nil {
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err) ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
@ -119,7 +119,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses
return "", "", nil return "", "", nil
case allow: case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username) ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, targetURL) HandleAllow(ctx, bodyJSON)
return "", "", nil return "", "", nil
case auth: case auth:
@ -135,7 +135,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses
} }
// HandlePreferredDeviceCheck handler to check if the saved device and method is still valid. // HandlePreferredDeviceCheck handler to check if the saved device and method is still valid.
func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, targetURL string) (string, string, error) { func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, bodyJSON *signDuoRequestBody) (string, string, error) {
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI) result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
if err != nil { if err != nil {
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err) ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
@ -165,7 +165,7 @@ func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *sessi
return "", "", nil return "", "", nil
case allow: case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username) ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, targetURL) HandleAllow(ctx, bodyJSON)
return "", "", nil return "", "", nil
case auth: case auth:
@ -243,7 +243,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user
} }
// HandleAllow handler for successful logins. // HandleAllow handler for successful logins.
func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) { func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *signDuoRequestBody) {
userSession := ctx.GetSession() userSession := ctx.GetSession()
err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx) err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
@ -266,10 +266,10 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
return return
} }
if userSession.ConsentChallengeID != nil { if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx) handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
} else { } else {
Handle2FAResponse(ctx, targetURL) Handle2FAResponse(ctx, bodyJSON.TargetURL)
} }
} }

View File

@ -7,9 +7,9 @@ import (
// TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user. // TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user.
func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) { func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
requestBody := signTOTPRequestBody{} bodyJSON := signTOTPRequestBody{}
if err := ctx.ParseBody(&requestBody); err != nil { if err := ctx.ParseBody(&bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeTOTP, err) ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeTOTP, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -28,7 +28,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
isValid, err := ctx.Providers.TOTP.Validate(requestBody.Token, config) isValid, err := ctx.Providers.TOTP.Validate(bodyJSON.Token, config)
if err != nil { if err != nil {
ctx.Logger.Errorf("Failed to perform TOTP verification: %+v", err) ctx.Logger.Errorf("Failed to perform TOTP verification: %+v", err)
@ -78,9 +78,9 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if userSession.ConsentChallengeID != nil { if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx) handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
} else { } else {
Handle2FAResponse(ctx, requestBody.TargetURL) Handle2FAResponse(ctx, bodyJSON.TargetURL)
} }
} }

View File

@ -84,10 +84,10 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
err error err error
w *webauthn.WebAuthn w *webauthn.WebAuthn
requestBody signWebauthnRequestBody bodyJSON signWebauthnRequestBody
) )
if err = ctx.ParseBody(&requestBody); err != nil { if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err) ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -197,9 +197,9 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if userSession.ConsentChallengeID != nil { if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx) handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
} else { } else {
Handle2FAResponse(ctx, requestBody.TargetURL) Handle2FAResponse(ctx, bodyJSON.TargetURL)
} }
} }

View File

@ -9,50 +9,48 @@ import (
"github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow. // handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) { func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
if len(targetURI) == 0 {
ctx.Error(fmt.Errorf("unable to parse target URL %s: empty value", targetURI), messageAuthenticationFailed)
return
}
var (
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
}
var (
id string
client *oidc.Client
)
if id = targetURL.Query().Get("client_id"); len(id) == 0 {
ctx.Error(fmt.Errorf("unable to get client id from from URL '%s'", targetURL), messageAuthenticationFailed)
return
}
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(id); err != nil {
ctx.Error(fmt.Errorf("unable to get client for client with id '%s' from URL '%s': %w", id, targetURL, err), messageAuthenticationFailed)
return
}
userSession := ctx.GetSession() userSession := ctx.GetSession()
if userSession.ConsentChallengeID == nil {
ctx.Logger.Errorf("Unable to handle OIDC workflow response because the user session doesn't contain a consent challenge id")
respondUnauthorized(ctx, messageOperationFailed)
return
}
externalRootURL, err := ctx.ExternalRootURL()
if err != nil {
ctx.Logger.Errorf("Unable to determine external Base URL: %v", err)
respondUnauthorized(ctx, messageOperationFailed)
return
}
consent, err := ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID)
if err != nil {
ctx.Logger.Errorf("Unable to load consent session from database: %v", err)
respondUnauthorized(ctx, messageOperationFailed)
return
}
client, err := ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID)
if err != nil {
ctx.Logger.Errorf("Unable to find client for the consent session: %v", err)
respondUnauthorized(ctx, messageOperationFailed)
return
}
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID) ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID)
ctx.ReplyOK() ctx.ReplyOK()
@ -60,57 +58,18 @@ func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
return return
} }
if consent.Subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
ctx.Logger.Errorf("Unable to find subject for the consent session: %v", err)
respondUnauthorized(ctx, messageOperationFailed)
return
}
consent.Subject.Valid = true
var preConsent *model.OAuth2ConsentSession
if preConsent, err = getOIDCPreConfiguredConsentFromClientAndConsent(ctx, client, consent); err != nil {
ctx.Logger.Errorf("Unable to lookup pre-configured consent for the consent session: %v", err)
respondUnauthorized(ctx, messageOperationFailed)
return
}
if userSession.ConsentChallengeID != nil && preConsent == nil {
if err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s/consent", externalRootURL)}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
}
return
}
if userSession.ConsentChallengeID != nil {
userSession.ConsentChallengeID = nil
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf("Unable to update user session: %v", err)
respondUnauthorized(ctx, messageOperationFailed)
return
}
}
if err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s%s?%s", externalRootURL, oidc.AuthorizationPath, consent.Form)}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
} }
} }
// Handle1FAResponse handle the redirection upon 1FA authentication. // Handle1FAResponse handle the redirection upon 1FA authentication.
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) { func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) {
if targetURI == "" { var err error
if len(targetURI) == 0 {
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" { if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}) if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
if err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
} }
} else { } else {
@ -120,9 +79,11 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
return return
} }
targetURL, err := url.ParseRequestURI(targetURI) var targetURL *url.URL
if err != nil {
if targetURL, err = url.ParseRequestURI(targetURI); err != nil {
ctx.Error(fmt.Errorf("unable to parse target URL %s: %s", targetURI, err), messageAuthenticationFailed) ctx.Error(fmt.Errorf("unable to parse target URL %s: %s", targetURI, err), messageAuthenticationFailed)
return return
} }
@ -143,63 +104,66 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
return return
} }
safeRedirection := utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) if !utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
if !safeRedirection {
ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI) ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI)
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" { if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}) if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
if err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
} }
} else {
ctx.ReplyOK() return
} }
ctx.ReplyOK()
return return
} }
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI) ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
if err != nil { if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI}); err != nil {
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err) ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
} }
} }
// Handle2FAResponse handle the redirection upon 2FA authentication. // Handle2FAResponse handle the redirection upon 2FA authentication.
func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) { func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
if targetURI == "" { var err error
if ctx.Configuration.DefaultRedirectionURL != "" {
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}) if len(targetURI) == 0 {
if err != nil { if len(ctx.Configuration.DefaultRedirectionURL) == 0 {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
}
} else {
ctx.ReplyOK() ctx.ReplyOK()
return
}
if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
} }
return return
} }
safe, err := utils.IsRedirectionURISafe(targetURI, ctx.Configuration.Session.Domain) var safe bool
if err != nil { if safe, err = utils.IsRedirectionURISafe(targetURI, ctx.Configuration.Session.Domain); err != nil {
ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed) ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed)
return return
} }
if safe { if safe {
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI) ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
err := ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
if err != nil { if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI}); err != nil {
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err) ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
} }
} else {
ctx.ReplyOK() return
} }
ctx.ReplyOK()
} }
func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) { func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) {

View File

@ -18,16 +18,19 @@ type configurationBody struct {
type signTOTPRequestBody struct { type signTOTPRequestBody struct {
Token string `json:"token" valid:"required"` Token string `json:"token" valid:"required"`
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
} }
// signWebauthnRequestBody model of the request body of Webauthn authentication endpoint. // signWebauthnRequestBody model of the request body of Webauthn authentication endpoint.
type signWebauthnRequestBody struct { type signWebauthnRequestBody struct {
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
} }
type signDuoRequestBody struct { type signDuoRequestBody struct {
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
Passcode string `json:"passcode"` Passcode string `json:"passcode"`
Workflow string `json:"workflow"`
} }
// preferred2FAMethodBody the selected 2FA method. // preferred2FAMethodBody the selected 2FA method.
@ -40,6 +43,7 @@ type firstFactorRequestBody struct {
Username string `json:"username" valid:"required"` Username string `json:"username" valid:"required"`
Password string `json:"password" valid:"required"` Password string `json:"password" valid:"required"`
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
RequestMethod string `json:"requestMethod"` RequestMethod string `json:"requestMethod"`
KeepMeLoggedIn *bool `json:"keepMeLoggedIn"` KeepMeLoggedIn *bool `json:"keepMeLoggedIn"`
// KeepMeLoggedIn: Cannot require this field because of https://github.com/asaskevich/govalidator/pull/329 // KeepMeLoggedIn: Cannot require this field because of https://github.com/asaskevich/govalidator/pull/329

View File

@ -17,10 +17,12 @@ import (
) )
// NewOAuth2ConsentSession creates a new OAuth2ConsentSession. // NewOAuth2ConsentSession creates a new OAuth2ConsentSession.
func NewOAuth2ConsentSession(subject NullUUID, r fosite.Requester) (consent *OAuth2ConsentSession, err error) { func NewOAuth2ConsentSession(subject uuid.UUID, r fosite.Requester) (consent *OAuth2ConsentSession, err error) {
valid := subject.ID() != 0
consent = &OAuth2ConsentSession{ consent = &OAuth2ConsentSession{
ClientID: r.GetClient().GetID(), ClientID: r.GetClient().GetID(),
Subject: subject, Subject: uuid.NullUUID{UUID: subject, Valid: valid},
Form: r.GetRequestForm().Encode(), Form: r.GetRequestForm().Encode(),
RequestedAt: r.GetRequestedAt(), RequestedAt: r.GetRequestedAt(),
RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()), RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
@ -84,10 +86,10 @@ func NewOAuth2BlacklistedJTI(jti string, exp time.Time) (jtiBlacklist OAuth2Blac
// OAuth2ConsentSession stores information about an OAuth2.0 Consent. // OAuth2ConsentSession stores information about an OAuth2.0 Consent.
type OAuth2ConsentSession struct { type OAuth2ConsentSession struct {
ID int `db:"id"` ID int `db:"id"`
ChallengeID uuid.UUID `db:"challenge_id"` ChallengeID uuid.UUID `db:"challenge_id"`
ClientID string `db:"client_id"` ClientID string `db:"client_id"`
Subject NullUUID `db:"subject"` Subject uuid.NullUUID `db:"subject"`
Authorized bool `db:"authorized"` Authorized bool `db:"authorized"`
Granted bool `db:"granted"` Granted bool `db:"granted"`

View File

@ -7,37 +7,9 @@ import (
"fmt" "fmt"
"net" "net"
"github.com/google/uuid"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
// NullUUID is a nullable uuid.UUID.
type NullUUID struct {
uuid.UUID
Valid bool
}
// Value is the NullUUID implementation of the databases/sql driver.Valuer.
func (u NullUUID) Value() (value driver.Value, err error) {
if !u.Valid {
return nil, nil
}
return u.UUID.Value()
}
// Scan is the NullUUID implementation of the sql.Scanner.
func (u *NullUUID) Scan(src interface{}) (err error) {
if src == nil {
u.UUID, u.Valid = uuid.UUID{}, false
return nil
}
return u.UUID.Scan(src)
}
// NewIP easily constructs a new IP. // NewIP easily constructs a new IP.
func NewIP(value net.IP) (ip IP) { func NewIP(value net.IP) (ip IP) {
return IP{IP: value} return IP{IP: value}

View File

@ -27,7 +27,7 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm, UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
Policy: authorization.PolicyToLevel(config.Policy), Policy: authorization.StringToLevel(config.Policy),
PreConfiguredConsentDuration: config.PreConfiguredConsentDuration, PreConfiguredConsentDuration: config.PreConfiguredConsentDuration,
} }

View File

@ -28,7 +28,7 @@ func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider s
} }
for _, client := range config.Clients { for _, client := range config.Clients {
policy := authorization.PolicyToLevel(client.Policy) policy := authorization.StringToLevel(client.Policy)
logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy) logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy)
store.clients[client.ID] = NewClient(client) store.clients[client.ID] = NewClient(client)

View File

@ -41,7 +41,7 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,
session = &model.OpenIDSession{ session = &model.OpenIDSession{
DefaultSession: &openid.DefaultSession{ DefaultSession: &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{ Claims: &jwt.IDTokenClaims{
Subject: consent.Subject.String(), Subject: consent.Subject.UUID.String(),
Issuer: issuer, Issuer: issuer,
AuthTime: authTime, AuthTime: authTime,
RequestedAt: consent.RequestedAt, RequestedAt: consent.RequestedAt,
@ -57,7 +57,7 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,
"kid": kid, "kid": kid,
}, },
}, },
Subject: consent.Subject.String(), Subject: consent.Subject.UUID.String(),
Username: username, Username: username,
}, },
Extra: map[string]interface{}{}, Extra: map[string]interface{}{},
@ -142,9 +142,10 @@ type ConsentGetResponseBody struct {
// ConsentPostRequestBody schema of the request body of the consent POST endpoint. // ConsentPostRequestBody schema of the request body of the consent POST endpoint.
type ConsentPostRequestBody struct { type ConsentPostRequestBody struct {
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
AcceptOrReject string `json:"accept_or_reject"` ConsentID string `json:"consent_id"`
PreConfigure bool `json:"pre_configure"` Consent bool `json:"consent"`
PreConfigure bool `json:"pre_configure"`
} }
// ConsentPostResponseBody schema of the response body of the consent POST endpoint. // ConsentPostResponseBody schema of the response body of the consent POST endpoint.

View File

@ -56,7 +56,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
consent := &model.OAuth2ConsentSession{ consent := &model.OAuth2ConsentSession{
ChallengeID: uuid.New(), ChallengeID: uuid.New(),
RequestedAt: requested, RequestedAt: requested,
Subject: model.NullUUID{UUID: subject, Valid: true}, Subject: uuid.NullUUID{UUID: subject, Valid: true},
} }
session := NewSessionWithAuthorizeRequest(issuer, "primary", "john", amr, extra, authAt, consent, request) session := NewSessionWithAuthorizeRequest(issuer, "primary", "john", amr, extra, authAt, consent, request)

View File

@ -7,7 +7,6 @@ import (
"github.com/fasthttp/session/v2" "github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/redis" "github.com/fasthttp/session/v2/providers/redis"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
@ -43,9 +42,6 @@ type UserSession struct {
// Webauthn holds the session registration data for this session. // Webauthn holds the session registration data for this session.
Webauthn *webauthn.SessionData Webauthn *webauthn.SessionData
// ConsentChallengeID is the OpenID Connect Consent Session challenge ID.
ConsentChallengeID *uuid.UUID
// This boolean is set to true after identity verification and checked // This boolean is set to true after identity verification and checked
// while doing the query actually updating the password. // while doing the query actually updating the password.
PasswordResetUsername *string PasswordResetUsername *string

View File

@ -398,7 +398,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent mode
consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted, consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted,
consent.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form, consent.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form,
consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience); err != nil { consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience); err != nil {
return fmt.Errorf("error inserting oauth2 consent session with challenge id '%s' for subject '%s': %w", consent.ChallengeID.String(), consent.Subject.String(), err) return fmt.Errorf("error inserting oauth2 consent session with challenge id '%s' for subject '%s': %w", consent.ChallengeID.String(), consent.Subject.UUID.String(), err)
} }
return nil return nil
@ -407,7 +407,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent mode
// SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session with the subject. // SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session 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 { 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, err) 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)
} }
return nil return nil
@ -416,7 +416,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, conse
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session with the response. // SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session 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.ExpiresAt, consent.GrantedScopes, consent.GrantedAudience, consent.ID); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.ExpiresAt, consent.GrantedScopes, consent.GrantedAudience, 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, err) 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)
} }
return nil return nil

View File

@ -0,0 +1,8 @@
import queryString from "query-string";
import { useLocation } from "react-router-dom";
export function useConsentID() {
const location = useLocation();
const queryParams = queryString.parse(location.search);
return queryParams && "consent_id" in queryParams ? (queryParams["consent_id"] as string) : undefined;
}

View File

@ -3,6 +3,8 @@ import { useLocation } from "react-router-dom";
export function useRedirectionURL() { export function useRedirectionURL() {
const location = useLocation(); const location = useLocation();
const queryParams = queryString.parse(location.search); const queryParams = queryString.parse(location.search);
return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined; return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined;
} }

View File

@ -0,0 +1,8 @@
import queryString from "query-string";
import { useLocation } from "react-router-dom";
export function useWorkflow() {
const location = useLocation();
const queryParams = queryString.parse(location.search);
return queryParams && "workflow" in queryParams ? (queryParams["workflow"] as string) : undefined;
}

View File

@ -3,7 +3,8 @@ import { Post, Get } from "@services/Client";
interface ConsentPostRequestBody { interface ConsentPostRequestBody {
client_id: string; client_id: string;
accept_or_reject: "accept" | "reject"; consent_id?: string;
consent: boolean;
pre_configure: boolean; pre_configure: boolean;
} }
@ -11,7 +12,7 @@ interface ConsentPostResponseBody {
redirect_uri: string; redirect_uri: string;
} }
interface ConsentGetResponseBody { export interface ConsentGetResponseBody {
client_id: string; client_id: string;
client_description: string; client_description: string;
scopes: string[]; scopes: string[];
@ -19,20 +20,26 @@ interface ConsentGetResponseBody {
pre_configuration: boolean; pre_configuration: boolean;
} }
export function getConsentResponse() { export function getConsentResponse(consentID: string) {
return Get<ConsentGetResponseBody>(ConsentPath); return Get<ConsentGetResponseBody>(ConsentPath + "?consent_id=" + consentID);
} }
export function acceptConsent(clientID: string, preConfigure: boolean) { export function acceptConsent(preConfigure: boolean, clientID: string, consentID?: string) {
const body: ConsentPostRequestBody = { const body: ConsentPostRequestBody = {
client_id: clientID, client_id: clientID,
accept_or_reject: "accept", consent_id: consentID,
consent: true,
pre_configure: preConfigure, pre_configure: preConfigure,
}; };
return Post<ConsentPostResponseBody>(ConsentPath, body); return Post<ConsentPostResponseBody>(ConsentPath, body);
} }
export function rejectConsent(clientID: string) { export function rejectConsent(clientID: string, consentID?: string) {
const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "reject", pre_configure: false }; const body: ConsentPostRequestBody = {
client_id: clientID,
consent_id: consentID,
consent: false,
pre_configure: false,
};
return Post<ConsentPostResponseBody>(ConsentPath, body); return Post<ConsentPostResponseBody>(ConsentPath, body);
} }

View File

@ -8,6 +8,7 @@ interface PostFirstFactorBody {
keepMeLoggedIn: boolean; keepMeLoggedIn: boolean;
targetURL?: string; targetURL?: string;
requestMethod?: string; requestMethod?: string;
workflow?: string;
} }
export async function postFirstFactor( export async function postFirstFactor(
@ -16,6 +17,7 @@ export async function postFirstFactor(
rememberMe: boolean, rememberMe: boolean,
targetURL?: string, targetURL?: string,
requestMethod?: string, requestMethod?: string,
workflow?: string,
) { ) {
const data: PostFirstFactorBody = { const data: PostFirstFactorBody = {
username, username,
@ -31,6 +33,10 @@ export async function postFirstFactor(
data.requestMethod = requestMethod; data.requestMethod = requestMethod;
} }
if (workflow) {
data.workflow = workflow;
}
const res = await PostWithOptionalResponse<SignInResponse>(FirstFactorPath, data); const res = await PostWithOptionalResponse<SignInResponse>(FirstFactorPath, data);
return res ? res : ({} as SignInResponse); return res ? res : ({} as SignInResponse);
} }

View File

@ -5,12 +5,18 @@ import { SignInResponse } from "@services/SignIn";
interface CompleteTOTPSigninBody { interface CompleteTOTPSigninBody {
token: string; token: string;
targetURL?: string; targetURL?: string;
workflow?: string;
} }
export function completeTOTPSignIn(passcode: string, targetURL: string | undefined) { export function completeTOTPSignIn(passcode: string, targetURL?: string, workflow?: string) {
const body: CompleteTOTPSigninBody = { token: `${passcode}` }; const body: CompleteTOTPSigninBody = { token: `${passcode}` };
if (targetURL) { if (targetURL) {
body.targetURL = targetURL; body.targetURL = targetURL;
} }
if (workflow) {
body.workflow = workflow;
}
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body); return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
} }

View File

@ -7,13 +7,19 @@ import { Get, PostWithOptionalResponse } from "@services/Client";
interface CompletePushSigninBody { interface CompletePushSigninBody {
targetURL?: string; targetURL?: string;
workflow?: string;
} }
export function completePushNotificationSignIn(targetURL: string | undefined) { export function completePushNotificationSignIn(targetURL?: string, workflow?: string) {
const body: CompletePushSigninBody = {}; const body: CompletePushSigninBody = {};
if (targetURL) { if (targetURL) {
body.targetURL = targetURL; body.targetURL = targetURL;
} }
if (workflow) {
body.workflow = workflow;
}
return PostWithOptionalResponse<DuoSignInResponse>(CompletePushNotificationSignInPath, body); return PostWithOptionalResponse<DuoSignInResponse>(CompletePushNotificationSignInPath, body);
} }
@ -35,6 +41,7 @@ export interface DuoDevice {
display_name: string; display_name: string;
capabilities: string[]; capabilities: string[];
} }
export async function initiateDuoDeviceSelectionProcess() { export async function initiateDuoDeviceSelectionProcess() {
return Get<DuoDevicesGetResponse>(InitiateDuoDeviceSelectionPath); return Get<DuoDevicesGetResponse>(InitiateDuoDeviceSelectionPath);
} }
@ -43,6 +50,7 @@ export interface DuoDevicePostRequest {
device: string; device: string;
method: string; method: string;
} }
export async function completeDuoDeviceSelectionProcess(device: DuoDevicePostRequest) { export async function completeDuoDeviceSelectionProcess(device: DuoDevicePostRequest) {
return PostWithOptionalResponse(CompleteDuoDeviceSelectionPath, { device: device.device, method: device.method }); return PostWithOptionalResponse(CompleteDuoDeviceSelectionPath, { device: device.device, method: device.method });
} }

View File

@ -19,12 +19,12 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { IndexRoute } from "@constants/Routes"; import { IndexRoute } from "@constants/Routes";
import { useConsentResponse } from "@hooks/Consent"; import { useConsentID } from "@hooks/ConsentID";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useRedirector } from "@hooks/Redirector"; import { useRedirector } from "@hooks/Redirector";
import { useUserInfoGET } from "@hooks/UserInfo"; import { useUserInfoGET } from "@hooks/UserInfo";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { acceptConsent, rejectConsent } from "@services/Consent"; import { acceptConsent, ConsentGetResponseBody, getConsentResponse, rejectConsent } from "@services/Consent";
import LoadingPage from "@views/LoadingPage/LoadingPage"; import LoadingPage from "@views/LoadingPage/LoadingPage";
export interface Props {} export interface Props {}
@ -48,12 +48,13 @@ function scopeNameToAvatar(id: string) {
const ConsentView = function (props: Props) { const ConsentView = function (props: Props) {
const styles = useStyles(); const styles = useStyles();
const { t: translate } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const redirect = useRedirector(); const redirect = useRedirector();
const consentID = useConsentID();
const { createErrorNotification, resetNotification } = useNotifications(); const { createErrorNotification, resetNotification } = useNotifications();
const [resp, fetch, , err] = useConsentResponse(); const [response, setResponse] = useState<ConsentGetResponseBody | undefined>(undefined);
const { t: translate } = useTranslation(); const [error, setError] = useState<any>(undefined);
const [preConfigure, setPreConfigure] = useState(false); const [preConfigure, setPreConfigure] = useState(false);
const handlePreConfigureChanged = () => { const handlePreConfigureChanged = () => {
@ -66,22 +67,30 @@ const ConsentView = function (props: Props) {
fetchUserInfo(); fetchUserInfo();
}, [fetchUserInfo]); }, [fetchUserInfo]);
useEffect(() => {
if (consentID) {
getConsentResponse(consentID)
.then((r) => {
setResponse(r);
})
.catch((error) => {
setError(error);
});
}
}, [consentID]);
useEffect(() => {
if (error) {
navigate(IndexRoute);
console.error(`Unable to display consent screen: ${error.message}`);
}
}, [navigate, resetNotification, createErrorNotification, error]);
useEffect(() => { useEffect(() => {
if (fetchUserInfoError) { if (fetchUserInfoError) {
createErrorNotification("There was an issue retrieving user preferences"); createErrorNotification("There was an issue retrieving user preferences");
} }
}, [fetchUserInfoError, createErrorNotification]); }, [fetchUserInfoError, resetNotification, createErrorNotification]);
useEffect(() => {
if (err) {
navigate(IndexRoute);
console.error(`Unable to display consent screen: ${err.message}`);
}
}, [navigate, resetNotification, createErrorNotification, err]);
useEffect(() => {
fetch();
}, [fetch]);
const translateScopeNameToDescription = (id: string): string => { const translateScopeNameToDescription = (id: string): string => {
switch (id) { switch (id) {
@ -102,10 +111,10 @@ const ConsentView = function (props: Props) {
const handleAcceptConsent = async () => { const handleAcceptConsent = async () => {
// This case should not happen in theory because the buttons are disabled when response is undefined. // This case should not happen in theory because the buttons are disabled when response is undefined.
if (!resp) { if (!response) {
return; return;
} }
const res = await acceptConsent(resp.client_id, preConfigure); const res = await acceptConsent(preConfigure, response.client_id, consentID);
if (res.redirect_uri) { if (res.redirect_uri) {
redirect(res.redirect_uri); redirect(res.redirect_uri);
} else { } else {
@ -114,10 +123,10 @@ const ConsentView = function (props: Props) {
}; };
const handleRejectConsent = async () => { const handleRejectConsent = async () => {
if (!resp) { if (!response) {
return; return;
} }
const res = await rejectConsent(resp.client_id); const res = await rejectConsent(response.client_id, consentID);
if (res.redirect_uri) { if (res.redirect_uri) {
redirect(res.redirect_uri); redirect(res.redirect_uri);
} else { } else {
@ -126,7 +135,7 @@ const ConsentView = function (props: Props) {
}; };
return ( return (
<ComponentOrLoading ready={resp !== undefined && userInfo !== undefined}> <ComponentOrLoading ready={response !== undefined && userInfo !== undefined}>
<LoginLayout <LoginLayout
id="consent-stage" id="consent-stage"
title={`${translate("Hi")} ${userInfo?.display_name}`} title={`${translate("Hi")} ${userInfo?.display_name}`}
@ -138,14 +147,14 @@ const ConsentView = function (props: Props) {
<div> <div>
<Tooltip <Tooltip
title={ title={
translate("Client ID", { client_id: resp?.client_id }) || translate("Client ID", { client_id: response?.client_id }) ||
"Client ID: " + resp?.client_id "Client ID: " + response?.client_id
} }
> >
<Typography className={styles.clientDescription}> <Typography className={styles.clientDescription}>
{resp !== undefined && resp.client_description !== "" {response !== undefined && response.client_description !== ""
? resp.client_description ? response.client_description
: resp?.client_id} : response?.client_id}
</Typography> </Typography>
</Tooltip> </Tooltip>
</div> </div>
@ -156,7 +165,7 @@ const ConsentView = function (props: Props) {
<Grid item xs={12}> <Grid item xs={12}>
<div className={styles.scopesListContainer}> <div className={styles.scopesListContainer}>
<List className={styles.scopesList}> <List className={styles.scopesList}>
{resp?.scopes.map((scope: string) => ( {response?.scopes.map((scope: string) => (
<Tooltip title={"Scope " + scope}> <Tooltip title={"Scope " + scope}>
<ListItem id={"scope-" + scope} dense> <ListItem id={"scope-" + scope} dense>
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon> <ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
@ -167,7 +176,7 @@ const ConsentView = function (props: Props) {
</List> </List>
</div> </div>
</Grid> </Grid>
{resp?.pre_configuration ? ( {response?.pre_configuration ? (
<Grid item xs={12}> <Grid item xs={12}>
<Tooltip <Tooltip
title={ title={
@ -197,7 +206,7 @@ const ConsentView = function (props: Props) {
<Button <Button
id="accept-button" id="accept-button"
className={styles.button} className={styles.button}
disabled={!resp} disabled={!response}
onClick={handleAcceptConsent} onClick={handleAcceptConsent}
color="primary" color="primary"
variant="contained" variant="contained"
@ -209,7 +218,7 @@ const ConsentView = function (props: Props) {
<Button <Button
id="deny-button" id="deny-button"
className={styles.button} className={styles.button}
disabled={!resp} disabled={!response}
onClick={handleRejectConsent} onClick={handleRejectConsent}
color="secondary" color="secondary"
variant="contained" variant="contained"

View File

@ -13,6 +13,7 @@ import { usePageVisibility } from "@hooks/PageVisibility";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useRequestMethod } from "@hooks/RequestMethod"; import { useRequestMethod } from "@hooks/RequestMethod";
import { useAutheliaState } from "@hooks/State"; import { useAutheliaState } from "@hooks/State";
import { useWorkflow } from "@hooks/Workflow";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { postFirstFactor } from "@services/FirstFactor"; import { postFirstFactor } from "@services/FirstFactor";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
@ -34,6 +35,7 @@ const FirstFactorForm = function (props: Props) {
const navigate = useNavigate(); const navigate = useNavigate();
const redirectionURL = useRedirectionURL(); const redirectionURL = useRedirectionURL();
const requestMethod = useRequestMethod(); const requestMethod = useRequestMethod();
const workflow = useWorkflow();
const [state, fetchState, ,] = useAutheliaState(); const [state, fetchState, ,] = useAutheliaState();
const [rememberMe, setRememberMe] = useState(false); const [rememberMe, setRememberMe] = useState(false);
@ -87,7 +89,7 @@ const FirstFactorForm = function (props: Props) {
props.onAuthenticationStart(); props.onAuthenticationStart();
try { try {
const res = await postFirstFactor(username, password, rememberMe, redirectionURL, requestMethod); const res = await postFirstFactor(username, password, rememberMe, redirectionURL, requestMethod, workflow);
props.onAuthenticationSuccess(res ? res.redirect : undefined); props.onAuthenticationSuccess(res ? res.redirect : undefined);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@ -17,6 +17,7 @@ import { useRedirector } from "@hooks/Redirector";
import { useRequestMethod } from "@hooks/RequestMethod"; import { useRequestMethod } from "@hooks/RequestMethod";
import { useAutheliaState } from "@hooks/State"; import { useAutheliaState } from "@hooks/State";
import { useUserInfoPOST } from "@hooks/UserInfo"; import { useUserInfoPOST } from "@hooks/UserInfo";
import { useWorkflow } from "@hooks/Workflow";
import { SecondFactorMethod } from "@models/Methods"; import { SecondFactorMethod } from "@models/Methods";
import { checkSafeRedirection } from "@services/SafeRedirection"; import { checkSafeRedirection } from "@services/SafeRedirection";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
@ -41,6 +42,7 @@ const LoginPortal = function (props: Props) {
const location = useLocation(); const location = useLocation();
const redirectionURL = useRedirectionURL(); const redirectionURL = useRedirectionURL();
const requestMethod = useRequestMethod(); const requestMethod = useRequestMethod();
const workflow = useWorkflow();
const { createErrorNotification } = useNotifications(); const { createErrorNotification } = useNotifications();
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true); const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
const redirector = useRedirector(); const redirector = useRedirector();
@ -49,7 +51,16 @@ const LoginPortal = function (props: Props) {
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST(); const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration(); const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
const redirect = useCallback((url: string) => navigate(url), [navigate]); const redirect = useCallback(
(pathname: string, search?: string) => {
if (search) {
navigate({ pathname: pathname, search: search });
} else {
navigate({ pathname: pathname });
}
},
[navigate],
);
// Fetch the state when portal is mounted. // Fetch the state when portal is mounted.
useEffect(() => { useEffect(() => {
@ -119,23 +130,25 @@ const LoginPortal = function (props: Props) {
return; return;
} }
const redirectionSuffix = redirectionURL const search = redirectionURL
? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}` ? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}${
: ""; workflow ? `&workflow=${workflow}` : ""
}`
: undefined;
if (state.authentication_level === AuthenticationLevel.Unauthenticated) { if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
setFirstFactorDisabled(false); setFirstFactorDisabled(false);
redirect(`${IndexRoute}${redirectionSuffix}`); redirect(IndexRoute, search);
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) { } else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
if (configuration.available_methods.size === 0) { if (configuration.available_methods.size === 0) {
redirect(AuthenticatedRoute); redirect(AuthenticatedRoute);
} else { } else {
if (userInfo.method === SecondFactorMethod.Webauthn) { if (userInfo.method === SecondFactorMethod.Webauthn) {
redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}${redirectionSuffix}`); redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`, search);
} else if (userInfo.method === SecondFactorMethod.MobilePush) { } else if (userInfo.method === SecondFactorMethod.MobilePush) {
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}${redirectionSuffix}`); redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`, search);
} else { } else {
redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}${redirectionSuffix}`); redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`, search);
} }
} }
} }
@ -144,6 +157,7 @@ const LoginPortal = function (props: Props) {
state, state,
redirectionURL, redirectionURL,
requestMethod, requestMethod,
workflow,
redirect, redirect,
userInfo, userInfo,
setFirstFactorDisabled, setFirstFactorDisabled,

View File

@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration"; import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
import { useWorkflow } from "@hooks/Workflow";
import { completeTOTPSignIn } from "@services/OneTimePassword"; import { completeTOTPSignIn } from "@services/OneTimePassword";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
import LoadingPage from "@views/LoadingPage/LoadingPage"; import LoadingPage from "@views/LoadingPage/LoadingPage";
@ -33,6 +34,7 @@ const OneTimePasswordMethod = function (props: Props) {
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle, props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
); );
const redirectionURL = useRedirectionURL(); const redirectionURL = useRedirectionURL();
const workflow = useWorkflow();
const { t: translate } = useTranslation(); const { t: translate } = useTranslation();
const { onSignInSuccess, onSignInError } = props; const { onSignInSuccess, onSignInError } = props;
@ -67,7 +69,7 @@ const OneTimePasswordMethod = function (props: Props) {
try { try {
setState(State.InProgress); setState(State.InProgress);
const res = await completeTOTPSignIn(passcodeStr, redirectionURL); const res = await completeTOTPSignIn(passcodeStr, redirectionURL, workflow);
setState(State.Success); setState(State.Success);
onSignInSuccessCallback(res ? res.redirect : undefined); onSignInSuccessCallback(res ? res.redirect : undefined);
} catch (err) { } catch (err) {
@ -81,6 +83,7 @@ const OneTimePasswordMethod = function (props: Props) {
onSignInSuccessCallback, onSignInSuccessCallback,
passcode, passcode,
redirectionURL, redirectionURL,
workflow,
resp, resp,
props.authenticationLevel, props.authenticationLevel,
props.registered, props.registered,

View File

@ -8,6 +8,7 @@ import PushNotificationIcon from "@components/PushNotificationIcon";
import SuccessIcon from "@components/SuccessIcon"; import SuccessIcon from "@components/SuccessIcon";
import { useIsMountedRef } from "@hooks/Mounted"; import { useIsMountedRef } from "@hooks/Mounted";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useWorkflow } from "@hooks/Workflow";
import { import {
completePushNotificationSignIn, completePushNotificationSignIn,
completeDuoDeviceSelectionProcess, completeDuoDeviceSelectionProcess,
@ -44,6 +45,7 @@ const PushNotificationMethod = function (props: Props) {
const styles = useStyles(); const styles = useStyles();
const [state, setState] = useState(State.SignInInProgress); const [state, setState] = useState(State.SignInInProgress);
const redirectionURL = useRedirectionURL(); const redirectionURL = useRedirectionURL();
const workflow = useWorkflow();
const mounted = useIsMountedRef(); const mounted = useIsMountedRef();
const [enroll_url, setEnrollUrl] = useState(""); const [enroll_url, setEnrollUrl] = useState("");
const [devices, setDevices] = useState([] as SelectableDevice[]); const [devices, setDevices] = useState([] as SelectableDevice[]);
@ -93,7 +95,7 @@ const PushNotificationMethod = function (props: Props) {
try { try {
setState(State.SignInInProgress); setState(State.SignInInProgress);
const res = await completePushNotificationSignIn(redirectionURL); const res = await completePushNotificationSignIn(redirectionURL, workflow);
// If the request was initiated and the user changed 2FA method in the meantime, // If the request was initiated and the user changed 2FA method in the meantime,
// the process is interrupted to avoid updating state of unmounted component. // the process is interrupted to avoid updating state of unmounted component.
if (!mounted.current) return; if (!mounted.current) return;
@ -136,6 +138,7 @@ const PushNotificationMethod = function (props: Props) {
props.authenticationLevel, props.authenticationLevel,
props.duoSelfEnrollment, props.duoSelfEnrollment,
redirectionURL, redirectionURL,
workflow,
mounted, mounted,
onSignInErrorCallback, onSignInErrorCallback,
onSignInSuccessCallback, onSignInSuccessCallback,