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

This fixes an issue where consent sessions prevent the standard workflow.
pull/3746/head^2
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 Level = iota
// OneFactor if the user has passed first factor only.
OneFactor Level = iota
OneFactor
// TwoFactor if the user has passed two factors.
TwoFactor Level = iota
TwoFactor
)
const (

View File

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

View File

@ -9,39 +9,48 @@ import (
type Authorizer struct {
defaultPolicy Level
rules []*AccessControlRule
mfa bool
configuration *schema.Configuration
}
// NewAuthorizer create an instance of authorizer with a given access control configuration.
func NewAuthorizer(configuration *schema.Configuration) *Authorizer {
return &Authorizer{
defaultPolicy: PolicyToLevel(configuration.AccessControl.DefaultPolicy),
func NewAuthorizer(configuration *schema.Configuration) (authorizer *Authorizer) {
authorizer = &Authorizer{
defaultPolicy: StringToLevel(configuration.AccessControl.DefaultPolicy),
rules: NewAccessControlRules(configuration.AccessControl),
configuration: configuration,
}
if authorizer.defaultPolicy == TwoFactor {
authorizer.mfa = true
return authorizer
}
for _, rule := range authorizer.rules {
if rule.Policy == TwoFactor {
authorizer.mfa = true
return authorizer
}
}
if authorizer.configuration.IdentityProviders.OIDC != nil {
for _, client := range authorizer.configuration.IdentityProviders.OIDC.Clients {
if client.Policy == twoFactor {
authorizer.mfa = true
return authorizer
}
}
}
return authorizer
}
// IsSecondFactorEnabled return true if at least one policy is set to second factor.
func (p Authorizer) IsSecondFactorEnabled() bool {
if p.defaultPolicy == TwoFactor {
return true
}
for _, rule := range p.rules {
if rule.Policy == TwoFactor {
return true
}
}
if p.configuration.IdentityProviders.OIDC != nil {
for _, client := range p.configuration.IdentityProviders.OIDC.Clients {
if client.Policy == twoFactor {
return true
}
}
}
return false
return p.mfa
}
// 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() {
s.Assert().Equal(Bypass, PolicyToLevel(bypass))
s.Assert().Equal(OneFactor, PolicyToLevel(oneFactor))
s.Assert().Equal(TwoFactor, PolicyToLevel(twoFactor))
s.Assert().Equal(Denied, PolicyToLevel(deny))
s.Assert().Equal(Bypass, StringToLevel(bypass))
s.Assert().Equal(OneFactor, StringToLevel(oneFactor))
s.Assert().Equal(TwoFactor, StringToLevel(twoFactor))
s.Assert().Equal(Denied, StringToLevel(deny))
s.Assert().Equal(Denied, PolicyToLevel("whatever"))
s.Assert().Equal(Denied, StringToLevel("whatever"))
}
func TestRunSuite(t *testing.T) {
@ -929,7 +929,8 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithNoOIDC(t *testing.T) {
authorizer := NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = TwoFactor
config.AccessControl.Rules[0].Policy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled())
}
@ -958,22 +959,24 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithOIDC(t *testing.T) {
authorizer := NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = TwoFactor
config.AccessControl.Rules[0].Policy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = OneFactor
config.AccessControl.Rules[0].Policy = oneFactor
authorizer = NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled())
config.IdentityProviders.OIDC.Clients[0].Policy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled())
authorizer.rules[0].Policy = OneFactor
config.AccessControl.Rules[0].Policy = oneFactor
config.IdentityProviders.OIDC.Clients[0].Policy = oneFactor
authorizer = NewAuthorizer(config)
assert.False(t, authorizer.IsSecondFactorEnabled())
authorizer.defaultPolicy = TwoFactor
config.AccessControl.DefaultPolicy = twoFactor
authorizer = NewAuthorizer(config)
assert.True(t, authorizer.IsSecondFactorEnabled())
}

View File

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

View File

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

View File

@ -11,6 +11,25 @@ import (
"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) {
subjectsSchema := [][]string{{"groups:z"}, {"group:z", "users:b"}}
subjectsACL := schemaSubjectsToACL(subjectsSchema)
@ -184,7 +203,7 @@ func TestShouldParseACLNetworks(t *testing.T) {
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.OneFactor, Denied))
assert.False(t, IsAuthLevelSufficient(authentication.TwoFactor, Denied))

View File

@ -167,11 +167,11 @@ func accessControlCheckWriteOutput(object authorization.Object, subject authoriz
switch {
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:
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:
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:
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"
)
const (
workflowOpenIDConnect = "openid_connect"
)
const (
logFmtErrParseRequestBody = "Failed to parse %s request body: %+v"
logFmtErrWriteResponseBody = "Failed to write %s response body for user '%s': %+v"
@ -72,11 +76,6 @@ const (
auth = "auth"
)
const (
accept = "accept"
reject = "reject"
)
const authPrefix = "Basic "
const ldapPasswordComplexityCode = "0000052D."

View File

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

View File

@ -3,6 +3,8 @@ package handlers
import (
"fmt"
"net/http"
"net/url"
"path"
"strings"
"github.com/google/uuid"
@ -19,85 +21,98 @@ import (
)
func handleOIDCAuthorizationConsent(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) {
if userSession.ConsentChallengeID != nil {
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)
return handleOIDCAuthorizationConsentWithChallengeID(ctx, rootURI, client, userSession, rw, r, requester)
}
if !subject.Valid {
return handleOIDCAuthorizationConsentGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester)
}
return handleOIDCAuthorizationConsentOrGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester)
}
func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
userSession session.UserSession,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var (
issuer *url.URL
subject uuid.UUID
err error
)
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); 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.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session."))
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 unlinking consent session challenge id: %+v", requester.GetID(), requester.GetClient().GetID(), err)
}
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 !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)
if !strings.HasSuffix(issuer.Path, "/") {
issuer.Path += "/"
}
// 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
}
consent.Subject.Valid = true
var consentIDBytes []byte
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)
if consentIDBytes = ctx.QueryArgs().Peek("consent_id"); len(consentIDBytes) != 0 {
var consentID uuid.UUID
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not update the consent session subject."))
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, issuer *url.URL, client *oidc.Client,
userSession session.UserSession, subject, challengeID uuid.UUID,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var (
err error
)
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.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session."))
return nil, true
}
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 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
}
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 {
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."))
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() {
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)
@ -107,13 +122,13 @@ func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx,
return consent, false
}
handleOIDCAuthorizationConsentRedirect(ctx, rootURI, client, userSession, rw, r, requester)
handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
return consent, true
}
func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
userSession session.UserSession, subject model.NullUUID,
func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client,
userSession session.UserSession, subject uuid.UUID,
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
var (
err error
@ -121,7 +136,7 @@ func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, root
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.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())
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 {
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
}
userSession.ConsentChallengeID = &consent.ChallengeID
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)
handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
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) {
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
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))
var location *url.URL
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 {
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) {
@ -203,19 +257,6 @@ func getOIDCExpectedScopesAndAudience(clientID string, scopes, audience []string
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) {
var (
rows *storage.ConsentSessionRows

View File

@ -3,8 +3,13 @@ package handlers
import (
"encoding/json"
"fmt"
"net/url"
"path"
"strings"
"time"
"github.com/google/uuid"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
@ -14,7 +19,19 @@ import (
// OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect.
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 {
return
}
@ -26,26 +43,35 @@ func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
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")
}
}
//nolint:gocyclo
// OpenIDConnectConsentPOST handles consent responses for OpenID Connect.
func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
var (
body oidc.ConsentPostRequestBody
consentID uuid.UUID
bodyJSON oidc.ConsentPostRequestBody
err error
)
if err = json.Unmarshal(ctx.Request.Body(), &body); err != nil {
ctx.Logger.Errorf("Failed to parse JSON body in consent POST: %+v", err)
if err = json.Unmarshal(ctx.Request.Body(), &bodyJSON); err != nil {
ctx.Logger.Errorf("Failed to parse JSON bodyJSON in consent POST: %+v", err)
ctx.SetJSONError(messageOperationFailed)
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 {
return
}
@ -57,36 +83,23 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
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",
userSession.Username, body.ClientID, consent.ClientID)
userSession.Username, bodyJSON.ClientID, consent.ClientID)
ctx.SetJSONError(messageOperationFailed)
return
}
var (
externalRootURL string
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 bodyJSON.Consent {
if bodyJSON.PreConfigure {
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 {
expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration)
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) {
consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID)
}
case reject:
authorized = false
default:
ctx.Logger.Warnf("User '%s' tried to reply to consent with an unexpected verb '%s'", userSession.Username, body.AcceptOrReject)
ctx.ReplyBadRequest()
}
var externalRootURL string
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
}
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.SetJSONError(messageOperationFailed)
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 {
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 (
err error
)
userSession = ctx.GetSession()
if userSession.ConsentChallengeID == nil {
ctx.Logger.Errorf("Cannot consent for user '%s' when OIDC consent session has not been initiated", userSession.Username)
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)
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {
ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", consentID, err)
ctx.ReplyForbidden()
return userSession, nil, nil, true
@ -147,5 +183,13 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx) (userSession
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
}

View File

@ -16,11 +16,11 @@ import (
func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
return func(ctx *middlewares.AutheliaCtx) {
var (
requestBody signDuoRequestBody
bodyJSON = &signDuoRequestBody{}
device, method string
)
if err := ctx.ParseBody(&requestBody); err != nil {
if err := ctx.ParseBody(bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -35,10 +35,10 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
if err != nil {
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)
device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, requestBody.TargetURL)
device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, bodyJSON)
} else {
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 {
@ -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)
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 {
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
}
HandleAllow(ctx, requestBody.TargetURL)
HandleAllow(ctx, bodyJSON)
}
}
// 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)
if err != nil {
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
case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, targetURL)
HandleAllow(ctx, bodyJSON)
return "", "", nil
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.
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)
if err != nil {
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
case allow:
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
HandleAllow(ctx, targetURL)
HandleAllow(ctx, bodyJSON)
return "", "", nil
case auth:
@ -243,7 +243,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user
}
// HandleAllow handler for successful logins.
func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *signDuoRequestBody) {
userSession := ctx.GetSession()
err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
@ -266,10 +266,10 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
return
}
if userSession.ConsentChallengeID != nil {
handleOIDCWorkflowResponse(ctx)
if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
} else {
Handle2FAResponse(ctx, targetURL)
Handle2FAResponse(ctx, bodyJSON.TargetURL)
}
}

View File

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

View File

@ -84,10 +84,10 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
err error
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)
respondUnauthorized(ctx, messageMFAValidationFailed)
@ -197,9 +197,9 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return
}
if userSession.ConsentChallengeID != nil {
handleOIDCWorkflowResponse(ctx)
if bodyJSON.Workflow == workflowOpenIDConnect {
handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
} 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/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/utils"
)
// 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()
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) {
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID)
ctx.ReplyOK()
@ -60,57 +58,18 @@ func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
return
}
if consent.Subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); 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 {
if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
}
}
// Handle1FAResponse handle the redirection upon 1FA authentication.
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 != "" {
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
if err != nil {
if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
}
} else {
@ -120,9 +79,11 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
return
}
targetURL, err := url.ParseRequestURI(targetURI)
if err != nil {
var targetURL *url.URL
if targetURL, err = url.ParseRequestURI(targetURI); err != nil {
ctx.Error(fmt.Errorf("unable to parse target URL %s: %s", targetURI, err), messageAuthenticationFailed)
return
}
@ -143,63 +104,66 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
return
}
safeRedirection := utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
if !safeRedirection {
if !utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI)
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
if err != nil {
if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
}
} else {
ctx.ReplyOK()
}
return
}
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
ctx.ReplyOK()
if err != nil {
return
}
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI}); err != nil {
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
}
}
// Handle2FAResponse handle the redirection upon 2FA authentication.
func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
if targetURI == "" {
if ctx.Configuration.DefaultRedirectionURL != "" {
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
if err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
}
} else {
var err error
if len(targetURI) == 0 {
if len(ctx.Configuration.DefaultRedirectionURL) == 0 {
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
}
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)
return
}
if safe {
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)
}
} else {
ctx.ReplyOK()
return
}
ctx.ReplyOK()
}
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 {
Token string `json:"token" valid:"required"`
TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
}
// signWebauthnRequestBody model of the request body of Webauthn authentication endpoint.
type signWebauthnRequestBody struct {
TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
}
type signDuoRequestBody struct {
TargetURL string `json:"targetURL"`
Passcode string `json:"passcode"`
Workflow string `json:"workflow"`
}
// preferred2FAMethodBody the selected 2FA method.
@ -40,6 +43,7 @@ type firstFactorRequestBody struct {
Username string `json:"username" valid:"required"`
Password string `json:"password" valid:"required"`
TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"`
RequestMethod string `json:"requestMethod"`
KeepMeLoggedIn *bool `json:"keepMeLoggedIn"`
// 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.
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{
ClientID: r.GetClient().GetID(),
Subject: subject,
Subject: uuid.NullUUID{UUID: subject, Valid: valid},
Form: r.GetRequestForm().Encode(),
RequestedAt: r.GetRequestedAt(),
RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
@ -87,7 +89,7 @@ type OAuth2ConsentSession struct {
ID int `db:"id"`
ChallengeID uuid.UUID `db:"challenge_id"`
ClientID string `db:"client_id"`
Subject NullUUID `db:"subject"`
Subject uuid.NullUUID `db:"subject"`
Authorized bool `db:"authorized"`
Granted bool `db:"granted"`

View File

@ -7,37 +7,9 @@ import (
"fmt"
"net"
"github.com/google/uuid"
"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.
func NewIP(value net.IP) (ip IP) {
return IP{IP: value}

View File

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

View File

@ -28,7 +28,7 @@ func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider s
}
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)
store.clients[client.ID] = NewClient(client)

View File

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

View File

@ -56,7 +56,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
consent := &model.OAuth2ConsentSession{
ChallengeID: uuid.New(),
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)

View File

@ -7,7 +7,6 @@ import (
"github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/redis"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/authelia/authelia/v4/internal/authentication"
@ -43,9 +42,6 @@ type UserSession struct {
// Webauthn holds the session registration data for this session.
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
// while doing the query actually updating the password.
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.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form,
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
@ -407,7 +407,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent mode
// SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session with the subject.
func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionSubject, consent.Subject, consent.ID); err != nil {
return fmt.Errorf("error updating oauth2 consent session subject with id '%d' and challenge id '%s' for subject '%s': %w", consent.ID, consent.ChallengeID, consent.Subject, 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
@ -416,7 +416,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, conse
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session with the response.
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 {
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

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() {
const location = useLocation();
const queryParams = queryString.parse(location.search);
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 {
client_id: string;
accept_or_reject: "accept" | "reject";
consent_id?: string;
consent: boolean;
pre_configure: boolean;
}
@ -11,7 +12,7 @@ interface ConsentPostResponseBody {
redirect_uri: string;
}
interface ConsentGetResponseBody {
export interface ConsentGetResponseBody {
client_id: string;
client_description: string;
scopes: string[];
@ -19,20 +20,26 @@ interface ConsentGetResponseBody {
pre_configuration: boolean;
}
export function getConsentResponse() {
return Get<ConsentGetResponseBody>(ConsentPath);
export function getConsentResponse(consentID: string) {
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 = {
client_id: clientID,
accept_or_reject: "accept",
consent_id: consentID,
consent: true,
pre_configure: preConfigure,
};
return Post<ConsentPostResponseBody>(ConsentPath, body);
}
export function rejectConsent(clientID: string) {
const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "reject", pre_configure: false };
export function rejectConsent(clientID: string, consentID?: string) {
const body: ConsentPostRequestBody = {
client_id: clientID,
consent_id: consentID,
consent: false,
pre_configure: false,
};
return Post<ConsentPostResponseBody>(ConsentPath, body);
}

View File

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

View File

@ -5,12 +5,18 @@ import { SignInResponse } from "@services/SignIn";
interface CompleteTOTPSigninBody {
token: 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}` };
if (targetURL) {
body.targetURL = targetURL;
}
if (workflow) {
body.workflow = workflow;
}
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import PushNotificationIcon from "@components/PushNotificationIcon";
import SuccessIcon from "@components/SuccessIcon";
import { useIsMountedRef } from "@hooks/Mounted";
import { useRedirectionURL } from "@hooks/RedirectionURL";
import { useWorkflow } from "@hooks/Workflow";
import {
completePushNotificationSignIn,
completeDuoDeviceSelectionProcess,
@ -44,6 +45,7 @@ const PushNotificationMethod = function (props: Props) {
const styles = useStyles();
const [state, setState] = useState(State.SignInInProgress);
const redirectionURL = useRedirectionURL();
const workflow = useWorkflow();
const mounted = useIsMountedRef();
const [enroll_url, setEnrollUrl] = useState("");
const [devices, setDevices] = useState([] as SelectableDevice[]);
@ -93,7 +95,7 @@ const PushNotificationMethod = function (props: Props) {
try {
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,
// the process is interrupted to avoid updating state of unmounted component.
if (!mounted.current) return;
@ -136,6 +138,7 @@ const PushNotificationMethod = function (props: Props) {
props.authenticationLevel,
props.duoSelfEnrollment,
redirectionURL,
workflow,
mounted,
onSignInErrorCallback,
onSignInSuccessCallback,