fix(handlers): consent session prevents standard flow (#3668)
This fixes an issue where consent sessions prevent the standard workflow.pull/3746/head^2
parent
efe1facc35
commit
b2cbcf3913
|
@ -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 (
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue