mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
fix(handlers): consent session prevents standard flow (#3668)
This fixes an issue where consent sessions prevent the standard workflow.
This commit is contained in:
parent
efe1facc35
commit
b2cbcf3913
|
@ -11,9 +11,9 @@ const (
|
||||||
// NotAuthenticated if the user is not authenticated yet.
|
// NotAuthenticated if the user is not authenticated yet.
|
||||||
NotAuthenticated Level = iota
|
NotAuthenticated Level = iota
|
||||||
// OneFactor if the user has passed first factor only.
|
// OneFactor if the user has passed first factor only.
|
||||||
OneFactor Level = iota
|
OneFactor
|
||||||
// TwoFactor if the user has passed two factors.
|
// TwoFactor if the user has passed two factors.
|
||||||
TwoFactor Level = iota
|
TwoFactor
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -27,7 +27,7 @@ func NewAccessControlRule(pos int, rule schema.ACLRule, networksMap map[string][
|
||||||
Methods: schemaMethodsToACL(rule.Methods),
|
Methods: schemaMethodsToACL(rule.Methods),
|
||||||
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
|
Networks: schemaNetworksToACL(rule.Networks, networksMap, networksCacheMap),
|
||||||
Subjects: schemaSubjectsToACL(rule.Subjects),
|
Subjects: schemaSubjectsToACL(rule.Subjects),
|
||||||
Policy: PolicyToLevel(rule.Policy),
|
Policy: StringToLevel(rule.Policy),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,39 +9,48 @@ import (
|
||||||
type Authorizer struct {
|
type Authorizer struct {
|
||||||
defaultPolicy Level
|
defaultPolicy Level
|
||||||
rules []*AccessControlRule
|
rules []*AccessControlRule
|
||||||
|
mfa bool
|
||||||
configuration *schema.Configuration
|
configuration *schema.Configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuthorizer create an instance of authorizer with a given access control configuration.
|
// NewAuthorizer create an instance of authorizer with a given access control configuration.
|
||||||
func NewAuthorizer(configuration *schema.Configuration) *Authorizer {
|
func NewAuthorizer(configuration *schema.Configuration) (authorizer *Authorizer) {
|
||||||
return &Authorizer{
|
authorizer = &Authorizer{
|
||||||
defaultPolicy: PolicyToLevel(configuration.AccessControl.DefaultPolicy),
|
defaultPolicy: StringToLevel(configuration.AccessControl.DefaultPolicy),
|
||||||
rules: NewAccessControlRules(configuration.AccessControl),
|
rules: NewAccessControlRules(configuration.AccessControl),
|
||||||
configuration: configuration,
|
configuration: configuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.
|
// IsSecondFactorEnabled return true if at least one policy is set to second factor.
|
||||||
func (p Authorizer) IsSecondFactorEnabled() bool {
|
func (p Authorizer) IsSecondFactorEnabled() bool {
|
||||||
if p.defaultPolicy == TwoFactor {
|
return p.mfa
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRequiredLevel retrieve the required level of authorization to access the object.
|
// GetRequiredLevel retrieve the required level of authorization to access the object.
|
||||||
|
|
|
@ -865,12 +865,12 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AuthorizerSuite) TestPolicyToLevel() {
|
func (s *AuthorizerSuite) TestPolicyToLevel() {
|
||||||
s.Assert().Equal(Bypass, PolicyToLevel(bypass))
|
s.Assert().Equal(Bypass, StringToLevel(bypass))
|
||||||
s.Assert().Equal(OneFactor, PolicyToLevel(oneFactor))
|
s.Assert().Equal(OneFactor, StringToLevel(oneFactor))
|
||||||
s.Assert().Equal(TwoFactor, PolicyToLevel(twoFactor))
|
s.Assert().Equal(TwoFactor, StringToLevel(twoFactor))
|
||||||
s.Assert().Equal(Denied, PolicyToLevel(deny))
|
s.Assert().Equal(Denied, StringToLevel(deny))
|
||||||
|
|
||||||
s.Assert().Equal(Denied, PolicyToLevel("whatever"))
|
s.Assert().Equal(Denied, StringToLevel("whatever"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunSuite(t *testing.T) {
|
func TestRunSuite(t *testing.T) {
|
||||||
|
@ -929,7 +929,8 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithNoOIDC(t *testing.T) {
|
||||||
authorizer := NewAuthorizer(config)
|
authorizer := NewAuthorizer(config)
|
||||||
assert.False(t, authorizer.IsSecondFactorEnabled())
|
assert.False(t, authorizer.IsSecondFactorEnabled())
|
||||||
|
|
||||||
authorizer.rules[0].Policy = TwoFactor
|
config.AccessControl.Rules[0].Policy = twoFactor
|
||||||
|
authorizer = NewAuthorizer(config)
|
||||||
assert.True(t, authorizer.IsSecondFactorEnabled())
|
assert.True(t, authorizer.IsSecondFactorEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -958,22 +959,24 @@ func TestAuthorizerIsSecondFactorEnabledRuleWithOIDC(t *testing.T) {
|
||||||
authorizer := NewAuthorizer(config)
|
authorizer := NewAuthorizer(config)
|
||||||
assert.False(t, authorizer.IsSecondFactorEnabled())
|
assert.False(t, authorizer.IsSecondFactorEnabled())
|
||||||
|
|
||||||
authorizer.rules[0].Policy = TwoFactor
|
config.AccessControl.Rules[0].Policy = twoFactor
|
||||||
|
authorizer = NewAuthorizer(config)
|
||||||
assert.True(t, authorizer.IsSecondFactorEnabled())
|
assert.True(t, authorizer.IsSecondFactorEnabled())
|
||||||
|
|
||||||
authorizer.rules[0].Policy = OneFactor
|
config.AccessControl.Rules[0].Policy = oneFactor
|
||||||
|
authorizer = NewAuthorizer(config)
|
||||||
assert.False(t, authorizer.IsSecondFactorEnabled())
|
assert.False(t, authorizer.IsSecondFactorEnabled())
|
||||||
|
|
||||||
config.IdentityProviders.OIDC.Clients[0].Policy = twoFactor
|
config.IdentityProviders.OIDC.Clients[0].Policy = twoFactor
|
||||||
|
authorizer = NewAuthorizer(config)
|
||||||
assert.True(t, authorizer.IsSecondFactorEnabled())
|
assert.True(t, authorizer.IsSecondFactorEnabled())
|
||||||
|
|
||||||
authorizer.rules[0].Policy = OneFactor
|
config.AccessControl.Rules[0].Policy = oneFactor
|
||||||
config.IdentityProviders.OIDC.Clients[0].Policy = oneFactor
|
config.IdentityProviders.OIDC.Clients[0].Policy = oneFactor
|
||||||
|
authorizer = NewAuthorizer(config)
|
||||||
assert.False(t, authorizer.IsSecondFactorEnabled())
|
assert.False(t, authorizer.IsSecondFactorEnabled())
|
||||||
|
|
||||||
authorizer.defaultPolicy = TwoFactor
|
config.AccessControl.DefaultPolicy = twoFactor
|
||||||
|
authorizer = NewAuthorizer(config)
|
||||||
assert.True(t, authorizer.IsSecondFactorEnabled())
|
assert.True(t, authorizer.IsSecondFactorEnabled())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,11 @@ const (
|
||||||
// Bypass bypass level.
|
// Bypass bypass level.
|
||||||
Bypass Level = iota
|
Bypass Level = iota
|
||||||
// OneFactor one factor level.
|
// OneFactor one factor level.
|
||||||
OneFactor Level = iota
|
OneFactor
|
||||||
// TwoFactor two factor level.
|
// TwoFactor two factor level.
|
||||||
TwoFactor Level = iota
|
TwoFactor
|
||||||
// Denied denied level.
|
// Denied denied level.
|
||||||
Denied Level = iota
|
Denied
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PolicyToLevel converts a string policy to int authorization level.
|
// StringToLevel converts a string policy to int authorization level.
|
||||||
func PolicyToLevel(policy string) Level {
|
func StringToLevel(policy string) Level {
|
||||||
switch policy {
|
switch policy {
|
||||||
case bypass:
|
case bypass:
|
||||||
return Bypass
|
return Bypass
|
||||||
|
@ -25,8 +25,8 @@ func PolicyToLevel(policy string) Level {
|
||||||
return Denied
|
return Denied
|
||||||
}
|
}
|
||||||
|
|
||||||
// LevelToPolicy converts a int authorization level to string policy.
|
// LevelToString converts a int authorization level to string policy.
|
||||||
func LevelToPolicy(level Level) (policy string) {
|
func LevelToString(level Level) (policy string) {
|
||||||
switch level {
|
switch level {
|
||||||
case Bypass:
|
case Bypass:
|
||||||
return bypass
|
return bypass
|
||||||
|
|
|
@ -11,6 +11,25 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestLevelToString(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
have Level
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{Bypass, "bypass"},
|
||||||
|
{OneFactor, "one_factor"},
|
||||||
|
{TwoFactor, "two_factor"},
|
||||||
|
{Denied, "deny"},
|
||||||
|
{99, "deny"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run("Expected_"+tc.expected, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.expected, LevelToString(tc.have))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestShouldNotParseInvalidSubjects(t *testing.T) {
|
func TestShouldNotParseInvalidSubjects(t *testing.T) {
|
||||||
subjectsSchema := [][]string{{"groups:z"}, {"group:z", "users:b"}}
|
subjectsSchema := [][]string{{"groups:z"}, {"group:z", "users:b"}}
|
||||||
subjectsACL := schemaSubjectsToACL(subjectsSchema)
|
subjectsACL := schemaSubjectsToACL(subjectsSchema)
|
||||||
|
@ -184,7 +203,7 @@ func TestShouldParseACLNetworks(t *testing.T) {
|
||||||
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"])
|
assert.Equal(t, fourthNetwork, networksCacheMap["fec0::1/128"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShouldReturnCorrectValidationLevel(t *testing.T) {
|
func TestIsAuthLevelSufficient(t *testing.T) {
|
||||||
assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Denied))
|
assert.False(t, IsAuthLevelSufficient(authentication.NotAuthenticated, Denied))
|
||||||
assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, Denied))
|
assert.False(t, IsAuthLevelSufficient(authentication.OneFactor, Denied))
|
||||||
assert.False(t, IsAuthLevelSufficient(authentication.TwoFactor, Denied))
|
assert.False(t, IsAuthLevelSufficient(authentication.TwoFactor, Denied))
|
||||||
|
|
|
@ -167,11 +167,11 @@ func accessControlCheckWriteOutput(object authorization.Object, subject authoriz
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case appliedPos != 0 && (potentialPos == 0 || (potentialPos > appliedPos)):
|
case appliedPos != 0 && (potentialPos == 0 || (potentialPos > appliedPos)):
|
||||||
fmt.Printf("\nThe policy '%s' from rule #%d will be applied to this request.\n\n", authorization.LevelToPolicy(applied.Rule.Policy), appliedPos)
|
fmt.Printf("\nThe policy '%s' from rule #%d will be applied to this request.\n\n", authorization.LevelToString(applied.Rule.Policy), appliedPos)
|
||||||
case potentialPos != 0 && appliedPos != 0:
|
case potentialPos != 0 && appliedPos != 0:
|
||||||
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. If not policy '%s' from rule #%d will be.\n\n", authorization.LevelToPolicy(potential.Rule.Policy), potentialPos, authorization.LevelToPolicy(applied.Rule.Policy), appliedPos)
|
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. If not policy '%s' from rule #%d will be.\n\n", authorization.LevelToString(potential.Rule.Policy), potentialPos, authorization.LevelToString(applied.Rule.Policy), appliedPos)
|
||||||
case potentialPos != 0:
|
case potentialPos != 0:
|
||||||
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. Otherwise the policy '%s' from the default policy will be.\n\n", authorization.LevelToPolicy(potential.Rule.Policy), potentialPos, defaultPolicy)
|
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. Otherwise the policy '%s' from the default policy will be.\n\n", authorization.LevelToString(potential.Rule.Policy), potentialPos, defaultPolicy)
|
||||||
default:
|
default:
|
||||||
fmt.Printf("\nThe policy '%s' from the default policy will be applied to this request as no rules matched the request.\n\n", defaultPolicy)
|
fmt.Printf("\nThe policy '%s' from the default policy will be applied to this request as no rules matched the request.\n\n", defaultPolicy)
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,10 @@ const (
|
||||||
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
|
messagePasswordWeak = "Your supplied password does not meet the password policy requirements"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
workflowOpenIDConnect = "openid_connect"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
logFmtErrParseRequestBody = "Failed to parse %s request body: %+v"
|
logFmtErrParseRequestBody = "Failed to parse %s request body: %+v"
|
||||||
logFmtErrWriteResponseBody = "Failed to write %s response body for user '%s': %+v"
|
logFmtErrWriteResponseBody = "Failed to write %s response body for user '%s': %+v"
|
||||||
|
@ -72,11 +76,6 @@ const (
|
||||||
auth = "auth"
|
auth = "auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
accept = "accept"
|
|
||||||
reject = "reject"
|
|
||||||
)
|
|
||||||
|
|
||||||
const authPrefix = "Basic "
|
const authPrefix = "Basic "
|
||||||
|
|
||||||
const ldapPasswordComplexityCode = "0000052D."
|
const ldapPasswordComplexityCode = "0000052D."
|
||||||
|
|
|
@ -73,7 +73,6 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
||||||
|
|
||||||
userSession := ctx.GetSession()
|
userSession := ctx.GetSession()
|
||||||
newSession := session.NewDefaultUserSession()
|
newSession := session.NewDefaultUserSession()
|
||||||
newSession.ConsentChallengeID = userSession.ConsentChallengeID
|
|
||||||
|
|
||||||
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
|
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
|
||||||
if err = ctx.SaveSession(newSession); err != nil {
|
if err = ctx.SaveSession(newSession); err != nil {
|
||||||
|
@ -135,8 +134,8 @@ func FirstFactorPOST(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
||||||
|
|
||||||
successful = true
|
successful = true
|
||||||
|
|
||||||
if userSession.ConsentChallengeID != nil {
|
if bodyJSON.Workflow == workflowOpenIDConnect {
|
||||||
handleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
|
||||||
} else {
|
} else {
|
||||||
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,26 +61,12 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
|
||||||
|
|
||||||
userSession := ctx.GetSession()
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
var subject model.NullUUID
|
|
||||||
|
|
||||||
if userSession.Username != "" {
|
|
||||||
if subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
|
|
||||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject for user '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, err)
|
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject."))
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
subject.Valid = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
consent *model.OAuth2ConsentSession
|
consent *model.OAuth2ConsentSession
|
||||||
handled bool
|
handled bool
|
||||||
)
|
)
|
||||||
|
|
||||||
if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, subject, rw, r, requester); handled {
|
if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, rw, r, requester); handled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -19,85 +21,98 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
||||||
userSession session.UserSession, subject model.NullUUID,
|
|
||||||
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,
|
userSession session.UserSession,
|
||||||
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
||||||
var (
|
var (
|
||||||
|
issuer *url.URL
|
||||||
|
subject uuid.UUID
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); err != nil {
|
if issuer, err = url.Parse(rootURI); 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("Could not safely determine the issuer."))
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !consent.Subject.Valid {
|
if !strings.HasSuffix(issuer.Path, "/") {
|
||||||
if consent.Subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
|
issuer.Path += "/"
|
||||||
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)
|
}
|
||||||
|
|
||||||
|
// 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."))
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject."))
|
||||||
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
consent.Subject.Valid = true
|
var consentIDBytes []byte
|
||||||
|
|
||||||
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionSubject(ctx, *consent); err != nil {
|
if consentIDBytes = ctx.QueryArgs().Peek("consent_id"); len(consentIDBytes) != 0 {
|
||||||
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)
|
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
|
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() {
|
if consent.Responded() {
|
||||||
userSession.ConsentChallengeID = nil
|
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
|
||||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving session: %+v", requester.GetID(), client.GetID(), err)
|
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the session."))
|
|
||||||
|
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if consent.Granted {
|
if consent.Granted {
|
||||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID.String())
|
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID)
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted."))
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted."))
|
||||||
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID.String(), client.GetID(), consent.Subject.String(), strings.Join(requester.GetRequestedScopes(), " "))
|
ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " "))
|
||||||
|
|
||||||
if consent.IsDenied() {
|
if consent.IsDenied() {
|
||||||
ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID.String(), client.GetID(), consent.Subject.String(), strings.Join(requester.GetRequestedScopes(), " "))
|
ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID, client.GetID(), consent.Subject.UUID, strings.Join(requester.GetRequestedScopes(), " "))
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied)
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied)
|
||||||
|
|
||||||
|
@ -107,13 +122,13 @@ func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx,
|
||||||
return consent, false
|
return consent, false
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOIDCAuthorizationConsentRedirect(ctx, rootURI, client, userSession, rw, r, requester)
|
handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
|
||||||
|
|
||||||
return consent, true
|
return consent, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, issuer *url.URL, client *oidc.Client,
|
||||||
userSession session.UserSession, subject model.NullUUID,
|
userSession session.UserSession, subject uuid.UUID,
|
||||||
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
|
@ -121,7 +136,7 @@ func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, root
|
||||||
|
|
||||||
scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester)
|
scopes, audience := getOIDCExpectedScopesAndAudienceFromRequest(requester)
|
||||||
|
|
||||||
if consent, err = getOIDCPreConfiguredConsent(ctx, client.GetID(), subject.UUID, scopes, audience); err != nil {
|
if consent, err = getOIDCPreConfiguredConsent(ctx, client.GetID(), subject, scopes, audience); err != nil {
|
||||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup the consent session."))
|
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup the consent session."))
|
||||||
|
@ -137,14 +152,6 @@ func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, root
|
||||||
|
|
||||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to generate a new consent due to unsuccessful lookup of pre-configured consent", requester.GetID(), client.GetID())
|
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' proceeding to generate a new consent due to unsuccessful lookup of pre-configured consent", requester.GetID(), client.GetID())
|
||||||
|
|
||||||
return handleOIDCAuthorizationConsentGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
|
||||||
userSession session.UserSession, subject model.NullUUID,
|
|
||||||
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil {
|
if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil {
|
||||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||||
|
|
||||||
|
@ -161,34 +168,81 @@ func handleOIDCAuthorizationConsentGenerate(ctx *middlewares.AutheliaCtx, rootUR
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.ConsentChallengeID = &consent.ChallengeID
|
handleOIDCAuthorizationConsentRedirect(ctx, issuer, consent, client, userSession, rw, r, requester)
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
|
||||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving user session for consent: %+v", requester.GetID(), client.GetID(), err)
|
|
||||||
|
|
||||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the user session."))
|
|
||||||
|
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
|
|
||||||
handleOIDCAuthorizationConsentRedirect(ctx, rootURI, client, userSession, rw, r, requester)
|
|
||||||
|
|
||||||
return consent, true
|
return consent, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, destination string, client *oidc.Client,
|
func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer *url.URL, consent *model.OAuth2ConsentSession, client *oidc.Client,
|
||||||
userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) {
|
userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) {
|
||||||
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
var location *url.URL
|
||||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is sufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToPolicy(client.Policy))
|
|
||||||
|
|
||||||
destination = fmt.Sprintf("%s/consent", destination)
|
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||||
|
location, _ = url.Parse(issuer.String())
|
||||||
|
location.Path = path.Join(location.Path, "/consent")
|
||||||
|
|
||||||
|
query := location.Query()
|
||||||
|
query.Set("consent_id", consent.ChallengeID.String())
|
||||||
|
|
||||||
|
location.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is sufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy))
|
||||||
} else {
|
} else {
|
||||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is insufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToPolicy(client.Policy))
|
location = getOIDCAuthorizationRedirectURL(issuer, requester)
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' authentication level '%s' is insufficient for client level '%s'", requester.GetID(), client.GetID(), authentication.LevelToString(userSession.AuthenticationLevel), authorization.LevelToString(client.Policy))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected to '%s'", requester.GetID(), client.GetID(), destination)
|
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being redirected to '%s'", requester.GetID(), client.GetID(), location)
|
||||||
|
|
||||||
http.Redirect(rw, r, destination, http.StatusFound)
|
http.Redirect(rw, r, location.String(), http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyOIDCUserAuthorizedForConsent(ctx *middlewares.AutheliaCtx, client *oidc.Client, userSession session.UserSession, consent *model.OAuth2ConsentSession, subject uuid.UUID) (err error) {
|
||||||
|
var sid, csid uint32
|
||||||
|
|
||||||
|
csid = consent.Subject.UUID.ID()
|
||||||
|
|
||||||
|
if !consent.Subject.Valid || csid == 0 {
|
||||||
|
return fmt.Errorf("the consent subject is null for consent session with id '%d'", consent.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID); err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve client: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sid = subject.ID(); sid == 0 {
|
||||||
|
if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
|
||||||
|
return fmt.Errorf("failed to lookup subject: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sid = subject.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
if csid != sid {
|
||||||
|
return fmt.Errorf("the consent subject identifier '%s' isn't owned by user '%s' who has a subject identifier of '%s' with sector identifier '%s'", consent.Subject.UUID, userSession.Username, subject, client.GetSectorIdentifier())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOIDCAuthorizationRedirectURL(issuer *url.URL, requester fosite.AuthorizeRequester) (redirectURL *url.URL) {
|
||||||
|
redirectURL, _ = url.Parse(issuer.String())
|
||||||
|
|
||||||
|
authorizationURL, _ := url.Parse(issuer.String())
|
||||||
|
|
||||||
|
authorizationURL.Path = path.Join(authorizationURL.Path, oidc.AuthorizationPath)
|
||||||
|
authorizationURL.RawQuery = requester.GetRequestForm().Encode()
|
||||||
|
|
||||||
|
query := redirectURL.Query()
|
||||||
|
query.Set("rd", authorizationURL.String())
|
||||||
|
query.Set("workflow", workflowOpenIDConnect)
|
||||||
|
|
||||||
|
redirectURL.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
return redirectURL
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOIDCExpectedScopesAndAudienceFromRequest(requester fosite.Requester) (scopes, audience []string) {
|
func getOIDCExpectedScopesAndAudienceFromRequest(requester fosite.Requester) (scopes, audience []string) {
|
||||||
|
@ -203,19 +257,6 @@ func getOIDCExpectedScopesAndAudience(clientID string, scopes, audience []string
|
||||||
return scopes, audience
|
return scopes, audience
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOIDCPreConfiguredConsentFromClientAndConsent(ctx *middlewares.AutheliaCtx, client fosite.Client, consent *model.OAuth2ConsentSession) (preConfigConsent *model.OAuth2ConsentSession, err error) {
|
|
||||||
if consent == nil || !consent.Subject.Valid {
|
|
||||||
return nil, fmt.Errorf("invalid consent provided for pre-configured consent lookup")
|
|
||||||
}
|
|
||||||
|
|
||||||
scopes, audience := getOIDCExpectedScopesAndAudience(client.GetID(), consent.RequestedScopes, consent.RequestedAudience)
|
|
||||||
|
|
||||||
// We can skip this error as it's handled at the authorization endpoint.
|
|
||||||
preConfigConsent, _ = getOIDCPreConfiguredConsent(ctx, client.GetID(), consent.Subject.UUID, scopes, audience)
|
|
||||||
|
|
||||||
return preConfigConsent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getOIDCPreConfiguredConsent(ctx *middlewares.AutheliaCtx, clientID string, subject uuid.UUID, scopes, audience []string) (consent *model.OAuth2ConsentSession, err error) {
|
func getOIDCPreConfiguredConsent(ctx *middlewares.AutheliaCtx, clientID string, subject uuid.UUID, scopes, audience []string) (consent *model.OAuth2ConsentSession, err error) {
|
||||||
var (
|
var (
|
||||||
rows *storage.ConsentSessionRows
|
rows *storage.ConsentSessionRows
|
||||||
|
|
|
@ -3,8 +3,13 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||||
"github.com/authelia/authelia/v4/internal/model"
|
"github.com/authelia/authelia/v4/internal/model"
|
||||||
"github.com/authelia/authelia/v4/internal/oidc"
|
"github.com/authelia/authelia/v4/internal/oidc"
|
||||||
|
@ -14,7 +19,19 @@ import (
|
||||||
|
|
||||||
// OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect.
|
// OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect.
|
||||||
func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
|
func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
|
||||||
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx)
|
var (
|
||||||
|
consentID uuid.UUID
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if consentID, err = uuid.Parse(string(ctx.RequestCtx.QueryArgs().Peek("consent_id"))); err != nil {
|
||||||
|
ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().Peek("consent_id"), err)
|
||||||
|
ctx.ReplyForbidden()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx, consentID)
|
||||||
if handled {
|
if handled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -26,26 +43,35 @@ func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.SetJSONBody(client.GetConsentResponseBody(consent)); err != nil {
|
if err = ctx.SetJSONBody(client.GetConsentResponseBody(consent)); err != nil {
|
||||||
ctx.Error(fmt.Errorf("unable to set JSON body: %v", err), "Operation failed")
|
ctx.Error(fmt.Errorf("unable to set JSON body: %v", err), "Operation failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:gocyclo
|
||||||
// OpenIDConnectConsentPOST handles consent responses for OpenID Connect.
|
// OpenIDConnectConsentPOST handles consent responses for OpenID Connect.
|
||||||
func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
body oidc.ConsentPostRequestBody
|
consentID uuid.UUID
|
||||||
|
bodyJSON oidc.ConsentPostRequestBody
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = json.Unmarshal(ctx.Request.Body(), &body); err != nil {
|
if err = json.Unmarshal(ctx.Request.Body(), &bodyJSON); err != nil {
|
||||||
ctx.Logger.Errorf("Failed to parse JSON body in consent POST: %+v", err)
|
ctx.Logger.Errorf("Failed to parse JSON bodyJSON in consent POST: %+v", err)
|
||||||
ctx.SetJSONError(messageOperationFailed)
|
ctx.SetJSONError(messageOperationFailed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx)
|
if consentID, err = uuid.Parse(bodyJSON.ConsentID); err != nil {
|
||||||
|
ctx.Logger.Errorf("Unable to convert '%s' into a UUID: %+v", ctx.RequestCtx.QueryArgs().Peek("consent_id"), err)
|
||||||
|
ctx.ReplyForbidden()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx, consentID)
|
||||||
if handled {
|
if handled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -57,36 +83,23 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if consent.ClientID != body.ClientID {
|
if consent.ClientID != bodyJSON.ClientID {
|
||||||
ctx.Logger.Errorf("User '%s' consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
|
ctx.Logger.Errorf("User '%s' consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
|
||||||
userSession.Username, body.ClientID, consent.ClientID)
|
userSession.Username, bodyJSON.ClientID, consent.ClientID)
|
||||||
ctx.SetJSONError(messageOperationFailed)
|
ctx.SetJSONError(messageOperationFailed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
if bodyJSON.Consent {
|
||||||
externalRootURL string
|
if bodyJSON.PreConfigure {
|
||||||
authorized = true
|
|
||||||
)
|
|
||||||
|
|
||||||
switch body.AcceptOrReject {
|
|
||||||
case accept:
|
|
||||||
if externalRootURL, err = ctx.ExternalRootURL(); err != nil {
|
|
||||||
ctx.Logger.Errorf("Could not determine the external URL during consent session processing with challenge id '%s' for user '%s': %v", consent.ChallengeID.String(), userSession.Username, err)
|
|
||||||
ctx.SetJSONError(messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.PreConfigure {
|
|
||||||
if client.PreConfiguredConsentDuration == nil {
|
if client.PreConfiguredConsentDuration == nil {
|
||||||
ctx.Logger.Warnf("Consent session with challenge id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID.String(), userSession.Username)
|
ctx.Logger.Warnf("Consent session with id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID, userSession.Username)
|
||||||
} else {
|
} else {
|
||||||
expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration)
|
expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration)
|
||||||
consent.ExpiresAt = &expiresAt
|
consent.ExpiresAt = &expiresAt
|
||||||
|
|
||||||
ctx.Logger.Debugf("Consent session with challenge id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID.String(), userSession.Username, consent.ExpiresAt)
|
ctx.Logger.Debugf("Consent session with id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID, userSession.Username, consent.ExpiresAt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,45 +109,68 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
if !utils.IsStringInSlice(consent.ClientID, consent.GrantedAudience) {
|
if !utils.IsStringInSlice(consent.ClientID, consent.GrantedAudience) {
|
||||||
consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID)
|
consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID)
|
||||||
}
|
}
|
||||||
case reject:
|
}
|
||||||
authorized = false
|
|
||||||
default:
|
var externalRootURL string
|
||||||
ctx.Logger.Warnf("User '%s' tried to reply to consent with an unexpected verb '%s'", userSession.Username, body.AcceptOrReject)
|
|
||||||
ctx.ReplyBadRequest()
|
if externalRootURL, err = ctx.ExternalRootURL(); err != nil {
|
||||||
|
ctx.Logger.Errorf("Could not determine the external URL during consent session processing with id '%s' for user '%s': %v", consent.ChallengeID, userSession.Username, err)
|
||||||
|
ctx.SetJSONError(messageOperationFailed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, authorized); err != nil {
|
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, bodyJSON.Consent); err != nil {
|
||||||
ctx.Logger.Errorf("Failed to save the consent session response to the database: %+v", err)
|
ctx.Logger.Errorf("Failed to save the consent session response to the database: %+v", err)
|
||||||
ctx.SetJSONError(messageOperationFailed)
|
ctx.SetJSONError(messageOperationFailed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := oidc.ConsentPostResponseBody{RedirectURI: fmt.Sprintf("%s%s?%s", externalRootURL, oidc.AuthorizationPath, consent.Form)}
|
var (
|
||||||
|
redirectURI *url.URL
|
||||||
|
query url.Values
|
||||||
|
)
|
||||||
|
|
||||||
|
if redirectURI, err = url.ParseRequestURI(externalRootURL); err != nil {
|
||||||
|
ctx.Logger.Errorf("Failed to parse the consent redirect URL: %+v", err)
|
||||||
|
ctx.SetJSONError(messageOperationFailed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(redirectURI.Path, "/") {
|
||||||
|
redirectURI.Path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
if query, err = url.ParseQuery(consent.Form); err != nil {
|
||||||
|
ctx.Logger.Errorf("Failed to parse the consent form values: %+v", err)
|
||||||
|
ctx.SetJSONError(messageOperationFailed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Set("consent_id", consent.ChallengeID.String())
|
||||||
|
|
||||||
|
redirectURI.Path = path.Join(redirectURI.Path, oidc.AuthorizationPath)
|
||||||
|
redirectURI.RawQuery = query.Encode()
|
||||||
|
|
||||||
|
response := oidc.ConsentPostResponseBody{RedirectURI: redirectURI.String()}
|
||||||
|
|
||||||
if err = ctx.SetJSONBody(response); err != nil {
|
if err = ctx.SetJSONBody(response); err != nil {
|
||||||
ctx.Error(fmt.Errorf("unable to set JSON body in response"), "Operation failed")
|
ctx.Error(fmt.Errorf("unable to set JSON bodyJSON in response"), "Operation failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx) (userSession session.UserSession, consent *model.OAuth2ConsentSession, client *oidc.Client, handled bool) {
|
func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx, consentID uuid.UUID) (userSession session.UserSession, consent *model.OAuth2ConsentSession, client *oidc.Client, handled bool) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
userSession = ctx.GetSession()
|
userSession = ctx.GetSession()
|
||||||
|
|
||||||
if userSession.ConsentChallengeID == nil {
|
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, consentID); err != nil {
|
||||||
ctx.Logger.Errorf("Cannot consent for user '%s' when OIDC consent session has not been initiated", userSession.Username)
|
ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", consentID, err)
|
||||||
ctx.ReplyForbidden()
|
|
||||||
|
|
||||||
return userSession, nil, nil, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", userSession.ConsentChallengeID.String(), err)
|
|
||||||
ctx.ReplyForbidden()
|
ctx.ReplyForbidden()
|
||||||
|
|
||||||
return userSession, nil, nil, true
|
return userSession, nil, nil, true
|
||||||
|
@ -147,5 +183,13 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx) (userSession
|
||||||
return userSession, nil, nil, true
|
return userSession, nil, nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = verifyOIDCUserAuthorizedForConsent(ctx, client, userSession, consent, uuid.UUID{}); err != nil {
|
||||||
|
ctx.Logger.Errorf("Could not authorize the user user '%s' for the consent session with challenge id '%s' on client with id '%s': %v", userSession.Username, consent.ChallengeID, client.GetID(), err)
|
||||||
|
|
||||||
|
ctx.ReplyForbidden()
|
||||||
|
|
||||||
|
return userSession, nil, nil, true
|
||||||
|
}
|
||||||
|
|
||||||
return userSession, consent, client, false
|
return userSession, consent, client, false
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,11 +16,11 @@ import (
|
||||||
func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
|
func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
|
||||||
return func(ctx *middlewares.AutheliaCtx) {
|
return func(ctx *middlewares.AutheliaCtx) {
|
||||||
var (
|
var (
|
||||||
requestBody signDuoRequestBody
|
bodyJSON = &signDuoRequestBody{}
|
||||||
device, method string
|
device, method string
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := ctx.ParseBody(&requestBody); err != nil {
|
if err := ctx.ParseBody(bodyJSON); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err)
|
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeDuo, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -35,10 +35,10 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Debugf("Error identifying preferred device for user %s: %s", userSession.Username, err)
|
ctx.Logger.Debugf("Error identifying preferred device for user %s: %s", userSession.Username, err)
|
||||||
ctx.Logger.Debugf("Starting Duo PreAuth for initial device selection of user: %s", userSession.Username)
|
ctx.Logger.Debugf("Starting Duo PreAuth for initial device selection of user: %s", userSession.Username)
|
||||||
device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, requestBody.TargetURL)
|
device, method, err = HandleInitialDeviceSelection(ctx, &userSession, duoAPI, bodyJSON)
|
||||||
} else {
|
} else {
|
||||||
ctx.Logger.Debugf("Starting Duo PreAuth to check preferred device of user: %s", userSession.Username)
|
ctx.Logger.Debugf("Starting Duo PreAuth to check preferred device of user: %s", userSession.Username)
|
||||||
device, method, err = HandlePreferredDeviceCheck(ctx, &userSession, duoAPI, duoDevice.Device, duoDevice.Method, requestBody.TargetURL)
|
device, method, err = HandlePreferredDeviceCheck(ctx, &userSession, duoAPI, duoDevice.Device, duoDevice.Method, bodyJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -52,7 +52,7 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
|
||||||
|
|
||||||
ctx.Logger.Debugf("Starting Duo Auth attempt for %s with device %s and method %s from IP %s", userSession.Username, device, method, remoteIP)
|
ctx.Logger.Debugf("Starting Duo Auth attempt for %s with device %s and method %s from IP %s", userSession.Username, device, method, remoteIP)
|
||||||
|
|
||||||
values, err := SetValues(userSession, device, method, remoteIP, requestBody.TargetURL, requestBody.Passcode)
|
values, err := SetValues(userSession, device, method, remoteIP, bodyJSON.TargetURL, bodyJSON.Passcode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Failed to set values for Duo Auth Call for user '%s': %+v", userSession.Username, err)
|
ctx.Logger.Errorf("Failed to set values for Duo Auth Call for user '%s': %+v", userSession.Username, err)
|
||||||
|
|
||||||
|
@ -85,12 +85,12 @@ func DuoPOST(duoAPI duo.API) middlewares.RequestHandler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
HandleAllow(ctx, requestBody.TargetURL)
|
HandleAllow(ctx, bodyJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleInitialDeviceSelection handler for retrieving all available devices.
|
// HandleInitialDeviceSelection handler for retrieving all available devices.
|
||||||
func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, targetURL string) (device string, method string, err error) {
|
func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, bodyJSON *signDuoRequestBody) (device string, method string, err error) {
|
||||||
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
|
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
|
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
|
||||||
|
@ -119,7 +119,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
case allow:
|
case allow:
|
||||||
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
|
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
|
||||||
HandleAllow(ctx, targetURL)
|
HandleAllow(ctx, bodyJSON)
|
||||||
|
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
case auth:
|
case auth:
|
||||||
|
@ -135,7 +135,7 @@ func HandleInitialDeviceSelection(ctx *middlewares.AutheliaCtx, userSession *ses
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlePreferredDeviceCheck handler to check if the saved device and method is still valid.
|
// HandlePreferredDeviceCheck handler to check if the saved device and method is still valid.
|
||||||
func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, targetURL string) (string, string, error) {
|
func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *session.UserSession, duoAPI duo.API, device string, method string, bodyJSON *signDuoRequestBody) (string, string, error) {
|
||||||
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
|
result, message, devices, enrollURL, err := DuoPreAuth(ctx, duoAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
|
ctx.Logger.Errorf("Failed to perform Duo PreAuth for user '%s': %+v", userSession.Username, err)
|
||||||
|
@ -165,7 +165,7 @@ func HandlePreferredDeviceCheck(ctx *middlewares.AutheliaCtx, userSession *sessi
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
case allow:
|
case allow:
|
||||||
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
|
ctx.Logger.Debugf("Duo authentication was bypassed for user: %s", userSession.Username)
|
||||||
HandleAllow(ctx, targetURL)
|
HandleAllow(ctx, bodyJSON)
|
||||||
|
|
||||||
return "", "", nil
|
return "", "", nil
|
||||||
case auth:
|
case auth:
|
||||||
|
@ -243,7 +243,7 @@ func HandleAutoSelection(ctx *middlewares.AutheliaCtx, devices []DuoDevice, user
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleAllow handler for successful logins.
|
// HandleAllow handler for successful logins.
|
||||||
func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
|
func HandleAllow(ctx *middlewares.AutheliaCtx, bodyJSON *signDuoRequestBody) {
|
||||||
userSession := ctx.GetSession()
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
|
err := ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
|
||||||
|
@ -266,10 +266,10 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.ConsentChallengeID != nil {
|
if bodyJSON.Workflow == workflowOpenIDConnect {
|
||||||
handleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
|
||||||
} else {
|
} else {
|
||||||
Handle2FAResponse(ctx, targetURL)
|
Handle2FAResponse(ctx, bodyJSON.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import (
|
||||||
|
|
||||||
// TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user.
|
// TimeBasedOneTimePasswordPOST validate the TOTP passcode provided by the user.
|
||||||
func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
|
func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
requestBody := signTOTPRequestBody{}
|
bodyJSON := signTOTPRequestBody{}
|
||||||
|
|
||||||
if err := ctx.ParseBody(&requestBody); err != nil {
|
if err := ctx.ParseBody(&bodyJSON); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeTOTP, err)
|
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeTOTP, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -28,7 +28,7 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid, err := ctx.Providers.TOTP.Validate(requestBody.Token, config)
|
isValid, err := ctx.Providers.TOTP.Validate(bodyJSON.Token, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Errorf("Failed to perform TOTP verification: %+v", err)
|
ctx.Logger.Errorf("Failed to perform TOTP verification: %+v", err)
|
||||||
|
|
||||||
|
@ -78,9 +78,9 @@ func TimeBasedOneTimePasswordPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.ConsentChallengeID != nil {
|
if bodyJSON.Workflow == workflowOpenIDConnect {
|
||||||
handleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
|
||||||
} else {
|
} else {
|
||||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, bodyJSON.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,10 +84,10 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
err error
|
err error
|
||||||
w *webauthn.WebAuthn
|
w *webauthn.WebAuthn
|
||||||
|
|
||||||
requestBody signWebauthnRequestBody
|
bodyJSON signWebauthnRequestBody
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = ctx.ParseBody(&requestBody); err != nil {
|
if err = ctx.ParseBody(&bodyJSON); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err)
|
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err)
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageMFAValidationFailed)
|
respondUnauthorized(ctx, messageMFAValidationFailed)
|
||||||
|
@ -197,9 +197,9 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userSession.ConsentChallengeID != nil {
|
if bodyJSON.Workflow == workflowOpenIDConnect {
|
||||||
handleOIDCWorkflowResponse(ctx)
|
handleOIDCWorkflowResponse(ctx, bodyJSON.TargetURL)
|
||||||
} else {
|
} else {
|
||||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
Handle2FAResponse(ctx, bodyJSON.TargetURL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,50 +9,48 @@ import (
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||||
"github.com/authelia/authelia/v4/internal/model"
|
|
||||||
"github.com/authelia/authelia/v4/internal/oidc"
|
"github.com/authelia/authelia/v4/internal/oidc"
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
|
// handleOIDCWorkflowResponse handle the redirection upon authentication in the OIDC workflow.
|
||||||
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
||||||
|
if len(targetURI) == 0 {
|
||||||
|
ctx.Error(fmt.Errorf("unable to parse target URL %s: empty value", targetURI), messageAuthenticationFailed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
targetURL *url.URL
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if targetURL, err = url.ParseRequestURI(targetURI); err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unable to parse target URL %s: %w", targetURI, err), messageAuthenticationFailed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
id string
|
||||||
|
client *oidc.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
if id = targetURL.Query().Get("client_id"); len(id) == 0 {
|
||||||
|
ctx.Error(fmt.Errorf("unable to get client id from from URL '%s'", targetURL), messageAuthenticationFailed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(id); err != nil {
|
||||||
|
ctx.Error(fmt.Errorf("unable to get client for client with id '%s' from URL '%s': %w", id, targetURL, err), messageAuthenticationFailed)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
userSession := ctx.GetSession()
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
if userSession.ConsentChallengeID == nil {
|
|
||||||
ctx.Logger.Errorf("Unable to handle OIDC workflow response because the user session doesn't contain a consent challenge id")
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
externalRootURL, err := ctx.ExternalRootURL()
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to determine external Base URL: %v", err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
consent, err := ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to load consent session from database: %v", err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID)
|
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to find client for the consent session: %v", err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||||
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID)
|
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID)
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
|
@ -60,57 +58,18 @@ func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if consent.Subject.UUID, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
|
if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to find subject for the consent session: %v", err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
consent.Subject.Valid = true
|
|
||||||
|
|
||||||
var preConsent *model.OAuth2ConsentSession
|
|
||||||
|
|
||||||
if preConsent, err = getOIDCPreConfiguredConsentFromClientAndConsent(ctx, client, consent); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to lookup pre-configured consent for the consent session: %v", err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userSession.ConsentChallengeID != nil && preConsent == nil {
|
|
||||||
if err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s/consent", externalRootURL)}); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if userSession.ConsentChallengeID != nil {
|
|
||||||
userSession.ConsentChallengeID = nil
|
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to update user session: %v", err)
|
|
||||||
|
|
||||||
respondUnauthorized(ctx, messageOperationFailed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s%s?%s", externalRootURL, oidc.AuthorizationPath, consent.Form)}); err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle1FAResponse handle the redirection upon 1FA authentication.
|
// Handle1FAResponse handle the redirection upon 1FA authentication.
|
||||||
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) {
|
func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod string, username string, groups []string) {
|
||||||
if targetURI == "" {
|
var err error
|
||||||
|
|
||||||
|
if len(targetURI) == 0 {
|
||||||
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
|
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
|
||||||
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -120,9 +79,11 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetURL, err := url.ParseRequestURI(targetURI)
|
var targetURL *url.URL
|
||||||
if err != nil {
|
|
||||||
|
if targetURL, err = url.ParseRequestURI(targetURI); err != nil {
|
||||||
ctx.Error(fmt.Errorf("unable to parse target URL %s: %s", targetURI, err), messageAuthenticationFailed)
|
ctx.Error(fmt.Errorf("unable to parse target URL %s: %s", targetURI, err), messageAuthenticationFailed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,63 +104,66 @@ func Handle1FAResponse(ctx *middlewares.AutheliaCtx, targetURI, requestMethod st
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
safeRedirection := utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
|
if !utils.IsRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
||||||
|
|
||||||
if !safeRedirection {
|
|
||||||
ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI)
|
ctx.Logger.Debugf("Redirection URL %s is not safe", targetURI)
|
||||||
|
|
||||||
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
|
if !ctx.Providers.Authorizer.IsSecondFactorEnabled() && ctx.Configuration.DefaultRedirectionURL != "" {
|
||||||
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
|
||||||
if err != nil {
|
|
||||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
ctx.ReplyOK()
|
||||||
err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
|
||||||
|
|
||||||
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)
|
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle2FAResponse handle the redirection upon 2FA authentication.
|
// Handle2FAResponse handle the redirection upon 2FA authentication.
|
||||||
func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
func Handle2FAResponse(ctx *middlewares.AutheliaCtx, targetURI string) {
|
||||||
if targetURI == "" {
|
var err error
|
||||||
if ctx.Configuration.DefaultRedirectionURL != "" {
|
|
||||||
err := ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL})
|
if len(targetURI) == 0 {
|
||||||
if err != nil {
|
if len(ctx.Configuration.DefaultRedirectionURL) == 0 {
|
||||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = ctx.SetJSONBody(redirectResponse{Redirect: ctx.Configuration.DefaultRedirectionURL}); err != nil {
|
||||||
|
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
safe, err := utils.IsRedirectionURISafe(targetURI, ctx.Configuration.Session.Domain)
|
var safe bool
|
||||||
|
|
||||||
if err != nil {
|
if safe, err = utils.IsRedirectionURISafe(targetURI, ctx.Configuration.Session.Domain); err != nil {
|
||||||
ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed)
|
ctx.Error(fmt.Errorf("unable to check target URL: %s", err), messageMFAValidationFailed)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if safe {
|
if safe {
|
||||||
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
ctx.Logger.Debugf("Redirection URL %s is safe", targetURI)
|
||||||
err := ctx.SetJSONBody(redirectResponse{Redirect: targetURI})
|
|
||||||
|
|
||||||
if err != nil {
|
if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURI}); err != nil {
|
||||||
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
ctx.Logger.Errorf("Unable to set redirection URL in body: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ctx.ReplyOK()
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.ReplyOK()
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) {
|
func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) {
|
||||||
|
|
|
@ -18,16 +18,19 @@ type configurationBody struct {
|
||||||
type signTOTPRequestBody struct {
|
type signTOTPRequestBody struct {
|
||||||
Token string `json:"token" valid:"required"`
|
Token string `json:"token" valid:"required"`
|
||||||
TargetURL string `json:"targetURL"`
|
TargetURL string `json:"targetURL"`
|
||||||
|
Workflow string `json:"workflow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// signWebauthnRequestBody model of the request body of Webauthn authentication endpoint.
|
// signWebauthnRequestBody model of the request body of Webauthn authentication endpoint.
|
||||||
type signWebauthnRequestBody struct {
|
type signWebauthnRequestBody struct {
|
||||||
TargetURL string `json:"targetURL"`
|
TargetURL string `json:"targetURL"`
|
||||||
|
Workflow string `json:"workflow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type signDuoRequestBody struct {
|
type signDuoRequestBody struct {
|
||||||
TargetURL string `json:"targetURL"`
|
TargetURL string `json:"targetURL"`
|
||||||
Passcode string `json:"passcode"`
|
Passcode string `json:"passcode"`
|
||||||
|
Workflow string `json:"workflow"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// preferred2FAMethodBody the selected 2FA method.
|
// preferred2FAMethodBody the selected 2FA method.
|
||||||
|
@ -40,6 +43,7 @@ type firstFactorRequestBody struct {
|
||||||
Username string `json:"username" valid:"required"`
|
Username string `json:"username" valid:"required"`
|
||||||
Password string `json:"password" valid:"required"`
|
Password string `json:"password" valid:"required"`
|
||||||
TargetURL string `json:"targetURL"`
|
TargetURL string `json:"targetURL"`
|
||||||
|
Workflow string `json:"workflow"`
|
||||||
RequestMethod string `json:"requestMethod"`
|
RequestMethod string `json:"requestMethod"`
|
||||||
KeepMeLoggedIn *bool `json:"keepMeLoggedIn"`
|
KeepMeLoggedIn *bool `json:"keepMeLoggedIn"`
|
||||||
// KeepMeLoggedIn: Cannot require this field because of https://github.com/asaskevich/govalidator/pull/329
|
// KeepMeLoggedIn: Cannot require this field because of https://github.com/asaskevich/govalidator/pull/329
|
||||||
|
|
|
@ -17,10 +17,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewOAuth2ConsentSession creates a new OAuth2ConsentSession.
|
// NewOAuth2ConsentSession creates a new OAuth2ConsentSession.
|
||||||
func NewOAuth2ConsentSession(subject NullUUID, r fosite.Requester) (consent *OAuth2ConsentSession, err error) {
|
func NewOAuth2ConsentSession(subject uuid.UUID, r fosite.Requester) (consent *OAuth2ConsentSession, err error) {
|
||||||
|
valid := subject.ID() != 0
|
||||||
|
|
||||||
consent = &OAuth2ConsentSession{
|
consent = &OAuth2ConsentSession{
|
||||||
ClientID: r.GetClient().GetID(),
|
ClientID: r.GetClient().GetID(),
|
||||||
Subject: subject,
|
Subject: uuid.NullUUID{UUID: subject, Valid: valid},
|
||||||
Form: r.GetRequestForm().Encode(),
|
Form: r.GetRequestForm().Encode(),
|
||||||
RequestedAt: r.GetRequestedAt(),
|
RequestedAt: r.GetRequestedAt(),
|
||||||
RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
|
RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
|
||||||
|
@ -87,7 +89,7 @@ type OAuth2ConsentSession struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
ChallengeID uuid.UUID `db:"challenge_id"`
|
ChallengeID uuid.UUID `db:"challenge_id"`
|
||||||
ClientID string `db:"client_id"`
|
ClientID string `db:"client_id"`
|
||||||
Subject NullUUID `db:"subject"`
|
Subject uuid.NullUUID `db:"subject"`
|
||||||
|
|
||||||
Authorized bool `db:"authorized"`
|
Authorized bool `db:"authorized"`
|
||||||
Granted bool `db:"granted"`
|
Granted bool `db:"granted"`
|
||||||
|
|
|
@ -7,37 +7,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NullUUID is a nullable uuid.UUID.
|
|
||||||
type NullUUID struct {
|
|
||||||
uuid.UUID
|
|
||||||
Valid bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value is the NullUUID implementation of the databases/sql driver.Valuer.
|
|
||||||
func (u NullUUID) Value() (value driver.Value, err error) {
|
|
||||||
if !u.Valid {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.UUID.Value()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan is the NullUUID implementation of the sql.Scanner.
|
|
||||||
func (u *NullUUID) Scan(src interface{}) (err error) {
|
|
||||||
if src == nil {
|
|
||||||
u.UUID, u.Valid = uuid.UUID{}, false
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.UUID.Scan(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIP easily constructs a new IP.
|
// NewIP easily constructs a new IP.
|
||||||
func NewIP(value net.IP) (ip IP) {
|
func NewIP(value net.IP) (ip IP) {
|
||||||
return IP{IP: value}
|
return IP{IP: value}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
|
||||||
|
|
||||||
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
|
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
|
||||||
|
|
||||||
Policy: authorization.PolicyToLevel(config.Policy),
|
Policy: authorization.StringToLevel(config.Policy),
|
||||||
|
|
||||||
PreConfiguredConsentDuration: config.PreConfiguredConsentDuration,
|
PreConfiguredConsentDuration: config.PreConfiguredConsentDuration,
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider s
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, client := range config.Clients {
|
for _, client := range config.Clients {
|
||||||
policy := authorization.PolicyToLevel(client.Policy)
|
policy := authorization.StringToLevel(client.Policy)
|
||||||
logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy)
|
logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy)
|
||||||
|
|
||||||
store.clients[client.ID] = NewClient(client)
|
store.clients[client.ID] = NewClient(client)
|
||||||
|
|
|
@ -41,7 +41,7 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,
|
||||||
session = &model.OpenIDSession{
|
session = &model.OpenIDSession{
|
||||||
DefaultSession: &openid.DefaultSession{
|
DefaultSession: &openid.DefaultSession{
|
||||||
Claims: &jwt.IDTokenClaims{
|
Claims: &jwt.IDTokenClaims{
|
||||||
Subject: consent.Subject.String(),
|
Subject: consent.Subject.UUID.String(),
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
AuthTime: authTime,
|
AuthTime: authTime,
|
||||||
RequestedAt: consent.RequestedAt,
|
RequestedAt: consent.RequestedAt,
|
||||||
|
@ -57,7 +57,7 @@ func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string,
|
||||||
"kid": kid,
|
"kid": kid,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Subject: consent.Subject.String(),
|
Subject: consent.Subject.UUID.String(),
|
||||||
Username: username,
|
Username: username,
|
||||||
},
|
},
|
||||||
Extra: map[string]interface{}{},
|
Extra: map[string]interface{}{},
|
||||||
|
@ -143,7 +143,8 @@ type ConsentGetResponseBody struct {
|
||||||
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
||||||
type ConsentPostRequestBody struct {
|
type ConsentPostRequestBody struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
AcceptOrReject string `json:"accept_or_reject"`
|
ConsentID string `json:"consent_id"`
|
||||||
|
Consent bool `json:"consent"`
|
||||||
PreConfigure bool `json:"pre_configure"`
|
PreConfigure bool `json:"pre_configure"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
|
||||||
consent := &model.OAuth2ConsentSession{
|
consent := &model.OAuth2ConsentSession{
|
||||||
ChallengeID: uuid.New(),
|
ChallengeID: uuid.New(),
|
||||||
RequestedAt: requested,
|
RequestedAt: requested,
|
||||||
Subject: model.NullUUID{UUID: subject, Valid: true},
|
Subject: uuid.NullUUID{UUID: subject, Valid: true},
|
||||||
}
|
}
|
||||||
|
|
||||||
session := NewSessionWithAuthorizeRequest(issuer, "primary", "john", amr, extra, authAt, consent, request)
|
session := NewSessionWithAuthorizeRequest(issuer, "primary", "john", amr, extra, authAt, consent, request)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/fasthttp/session/v2"
|
"github.com/fasthttp/session/v2"
|
||||||
"github.com/fasthttp/session/v2/providers/redis"
|
"github.com/fasthttp/session/v2/providers/redis"
|
||||||
"github.com/go-webauthn/webauthn/webauthn"
|
"github.com/go-webauthn/webauthn/webauthn"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/authentication"
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
|
@ -43,9 +42,6 @@ type UserSession struct {
|
||||||
// Webauthn holds the session registration data for this session.
|
// Webauthn holds the session registration data for this session.
|
||||||
Webauthn *webauthn.SessionData
|
Webauthn *webauthn.SessionData
|
||||||
|
|
||||||
// ConsentChallengeID is the OpenID Connect Consent Session challenge ID.
|
|
||||||
ConsentChallengeID *uuid.UUID
|
|
||||||
|
|
||||||
// This boolean is set to true after identity verification and checked
|
// This boolean is set to true after identity verification and checked
|
||||||
// while doing the query actually updating the password.
|
// while doing the query actually updating the password.
|
||||||
PasswordResetUsername *string
|
PasswordResetUsername *string
|
||||||
|
|
|
@ -398,7 +398,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent mode
|
||||||
consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted,
|
consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted,
|
||||||
consent.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form,
|
consent.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form,
|
||||||
consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience); err != nil {
|
consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience); err != nil {
|
||||||
return fmt.Errorf("error inserting oauth2 consent session with challenge id '%s' for subject '%s': %w", consent.ChallengeID.String(), consent.Subject.String(), err)
|
return fmt.Errorf("error inserting oauth2 consent session with challenge id '%s' for subject '%s': %w", consent.ChallengeID.String(), consent.Subject.UUID.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -407,7 +407,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent mode
|
||||||
// SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session with the subject.
|
// SaveOAuth2ConsentSessionSubject updates an OAuth2.0 consent session with the subject.
|
||||||
func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {
|
func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {
|
||||||
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionSubject, consent.Subject, consent.ID); err != nil {
|
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionSubject, consent.Subject, consent.ID); err != nil {
|
||||||
return fmt.Errorf("error updating oauth2 consent session subject with id '%d' and challenge id '%s' for subject '%s': %w", consent.ID, consent.ChallengeID, consent.Subject, err)
|
return fmt.Errorf("error updating oauth2 consent session subject with id '%d' and challenge id '%s' for subject '%s': %w", consent.ID, consent.ChallengeID, consent.Subject.UUID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -416,7 +416,7 @@ func (p *SQLProvider) SaveOAuth2ConsentSessionSubject(ctx context.Context, conse
|
||||||
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session with the response.
|
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent session with the response.
|
||||||
func (p *SQLProvider) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, authorized bool) (err error) {
|
func (p *SQLProvider) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, authorized bool) (err error) {
|
||||||
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.ExpiresAt, consent.GrantedScopes, consent.GrantedAudience, consent.ID); err != nil {
|
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.ExpiresAt, consent.GrantedScopes, consent.GrantedAudience, consent.ID); err != nil {
|
||||||
return fmt.Errorf("error updating oauth2 consent session (authorized '%t') with id '%d' and challenge id '%s' for subject '%s': %w", authorized, consent.ID, consent.ChallengeID, consent.Subject, err)
|
return fmt.Errorf("error updating oauth2 consent session (authorized '%t') with id '%d' and challenge id '%s' for subject '%s': %w", authorized, consent.ID, consent.ChallengeID, consent.Subject.UUID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
8
web/src/hooks/ConsentID.ts
Normal file
8
web/src/hooks/ConsentID.ts
Normal 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;
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export function useRedirectionURL() {
|
export function useRedirectionURL() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const queryParams = queryString.parse(location.search);
|
const queryParams = queryString.parse(location.search);
|
||||||
|
|
||||||
return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined;
|
return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined;
|
||||||
}
|
}
|
||||||
|
|
8
web/src/hooks/Workflow.ts
Normal file
8
web/src/hooks/Workflow.ts
Normal 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;
|
||||||
|
}
|
|
@ -3,7 +3,8 @@ import { Post, Get } from "@services/Client";
|
||||||
|
|
||||||
interface ConsentPostRequestBody {
|
interface ConsentPostRequestBody {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
accept_or_reject: "accept" | "reject";
|
consent_id?: string;
|
||||||
|
consent: boolean;
|
||||||
pre_configure: boolean;
|
pre_configure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ interface ConsentPostResponseBody {
|
||||||
redirect_uri: string;
|
redirect_uri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConsentGetResponseBody {
|
export interface ConsentGetResponseBody {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
client_description: string;
|
client_description: string;
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
|
@ -19,20 +20,26 @@ interface ConsentGetResponseBody {
|
||||||
pre_configuration: boolean;
|
pre_configuration: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConsentResponse() {
|
export function getConsentResponse(consentID: string) {
|
||||||
return Get<ConsentGetResponseBody>(ConsentPath);
|
return Get<ConsentGetResponseBody>(ConsentPath + "?consent_id=" + consentID);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function acceptConsent(clientID: string, preConfigure: boolean) {
|
export function acceptConsent(preConfigure: boolean, clientID: string, consentID?: string) {
|
||||||
const body: ConsentPostRequestBody = {
|
const body: ConsentPostRequestBody = {
|
||||||
client_id: clientID,
|
client_id: clientID,
|
||||||
accept_or_reject: "accept",
|
consent_id: consentID,
|
||||||
|
consent: true,
|
||||||
pre_configure: preConfigure,
|
pre_configure: preConfigure,
|
||||||
};
|
};
|
||||||
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rejectConsent(clientID: string) {
|
export function rejectConsent(clientID: string, consentID?: string) {
|
||||||
const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "reject", pre_configure: false };
|
const body: ConsentPostRequestBody = {
|
||||||
|
client_id: clientID,
|
||||||
|
consent_id: consentID,
|
||||||
|
consent: false,
|
||||||
|
pre_configure: false,
|
||||||
|
};
|
||||||
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ interface PostFirstFactorBody {
|
||||||
keepMeLoggedIn: boolean;
|
keepMeLoggedIn: boolean;
|
||||||
targetURL?: string;
|
targetURL?: string;
|
||||||
requestMethod?: string;
|
requestMethod?: string;
|
||||||
|
workflow?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postFirstFactor(
|
export async function postFirstFactor(
|
||||||
|
@ -16,6 +17,7 @@ export async function postFirstFactor(
|
||||||
rememberMe: boolean,
|
rememberMe: boolean,
|
||||||
targetURL?: string,
|
targetURL?: string,
|
||||||
requestMethod?: string,
|
requestMethod?: string,
|
||||||
|
workflow?: string,
|
||||||
) {
|
) {
|
||||||
const data: PostFirstFactorBody = {
|
const data: PostFirstFactorBody = {
|
||||||
username,
|
username,
|
||||||
|
@ -31,6 +33,10 @@ export async function postFirstFactor(
|
||||||
data.requestMethod = requestMethod;
|
data.requestMethod = requestMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workflow) {
|
||||||
|
data.workflow = workflow;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await PostWithOptionalResponse<SignInResponse>(FirstFactorPath, data);
|
const res = await PostWithOptionalResponse<SignInResponse>(FirstFactorPath, data);
|
||||||
return res ? res : ({} as SignInResponse);
|
return res ? res : ({} as SignInResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,18 @@ import { SignInResponse } from "@services/SignIn";
|
||||||
interface CompleteTOTPSigninBody {
|
interface CompleteTOTPSigninBody {
|
||||||
token: string;
|
token: string;
|
||||||
targetURL?: string;
|
targetURL?: string;
|
||||||
|
workflow?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function completeTOTPSignIn(passcode: string, targetURL: string | undefined) {
|
export function completeTOTPSignIn(passcode: string, targetURL?: string, workflow?: string) {
|
||||||
const body: CompleteTOTPSigninBody = { token: `${passcode}` };
|
const body: CompleteTOTPSigninBody = { token: `${passcode}` };
|
||||||
if (targetURL) {
|
if (targetURL) {
|
||||||
body.targetURL = targetURL;
|
body.targetURL = targetURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workflow) {
|
||||||
|
body.workflow = workflow;
|
||||||
|
}
|
||||||
|
|
||||||
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
|
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,19 @@ import { Get, PostWithOptionalResponse } from "@services/Client";
|
||||||
|
|
||||||
interface CompletePushSigninBody {
|
interface CompletePushSigninBody {
|
||||||
targetURL?: string;
|
targetURL?: string;
|
||||||
|
workflow?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function completePushNotificationSignIn(targetURL: string | undefined) {
|
export function completePushNotificationSignIn(targetURL?: string, workflow?: string) {
|
||||||
const body: CompletePushSigninBody = {};
|
const body: CompletePushSigninBody = {};
|
||||||
if (targetURL) {
|
if (targetURL) {
|
||||||
body.targetURL = targetURL;
|
body.targetURL = targetURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workflow) {
|
||||||
|
body.workflow = workflow;
|
||||||
|
}
|
||||||
|
|
||||||
return PostWithOptionalResponse<DuoSignInResponse>(CompletePushNotificationSignInPath, body);
|
return PostWithOptionalResponse<DuoSignInResponse>(CompletePushNotificationSignInPath, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +41,7 @@ export interface DuoDevice {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
capabilities: string[];
|
capabilities: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initiateDuoDeviceSelectionProcess() {
|
export async function initiateDuoDeviceSelectionProcess() {
|
||||||
return Get<DuoDevicesGetResponse>(InitiateDuoDeviceSelectionPath);
|
return Get<DuoDevicesGetResponse>(InitiateDuoDeviceSelectionPath);
|
||||||
}
|
}
|
||||||
|
@ -43,6 +50,7 @@ export interface DuoDevicePostRequest {
|
||||||
device: string;
|
device: string;
|
||||||
method: string;
|
method: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function completeDuoDeviceSelectionProcess(device: DuoDevicePostRequest) {
|
export async function completeDuoDeviceSelectionProcess(device: DuoDevicePostRequest) {
|
||||||
return PostWithOptionalResponse(CompleteDuoDeviceSelectionPath, { device: device.device, method: device.method });
|
return PostWithOptionalResponse(CompleteDuoDeviceSelectionPath, { device: device.device, method: device.method });
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,12 @@ import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { IndexRoute } from "@constants/Routes";
|
import { IndexRoute } from "@constants/Routes";
|
||||||
import { useConsentResponse } from "@hooks/Consent";
|
import { useConsentID } from "@hooks/ConsentID";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { useRedirector } from "@hooks/Redirector";
|
import { useRedirector } from "@hooks/Redirector";
|
||||||
import { useUserInfoGET } from "@hooks/UserInfo";
|
import { useUserInfoGET } from "@hooks/UserInfo";
|
||||||
import LoginLayout from "@layouts/LoginLayout";
|
import LoginLayout from "@layouts/LoginLayout";
|
||||||
import { acceptConsent, rejectConsent } from "@services/Consent";
|
import { acceptConsent, ConsentGetResponseBody, getConsentResponse, rejectConsent } from "@services/Consent";
|
||||||
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||||
|
|
||||||
export interface Props {}
|
export interface Props {}
|
||||||
|
@ -48,12 +48,13 @@ function scopeNameToAvatar(id: string) {
|
||||||
|
|
||||||
const ConsentView = function (props: Props) {
|
const ConsentView = function (props: Props) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
|
const { t: translate } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const redirect = useRedirector();
|
const redirect = useRedirector();
|
||||||
|
const consentID = useConsentID();
|
||||||
const { createErrorNotification, resetNotification } = useNotifications();
|
const { createErrorNotification, resetNotification } = useNotifications();
|
||||||
const [resp, fetch, , err] = useConsentResponse();
|
const [response, setResponse] = useState<ConsentGetResponseBody | undefined>(undefined);
|
||||||
const { t: translate } = useTranslation();
|
const [error, setError] = useState<any>(undefined);
|
||||||
|
|
||||||
const [preConfigure, setPreConfigure] = useState(false);
|
const [preConfigure, setPreConfigure] = useState(false);
|
||||||
|
|
||||||
const handlePreConfigureChanged = () => {
|
const handlePreConfigureChanged = () => {
|
||||||
|
@ -66,22 +67,30 @@ const ConsentView = function (props: Props) {
|
||||||
fetchUserInfo();
|
fetchUserInfo();
|
||||||
}, [fetchUserInfo]);
|
}, [fetchUserInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (consentID) {
|
||||||
|
getConsentResponse(consentID)
|
||||||
|
.then((r) => {
|
||||||
|
setResponse(r);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [consentID]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
navigate(IndexRoute);
|
||||||
|
console.error(`Unable to display consent screen: ${error.message}`);
|
||||||
|
}
|
||||||
|
}, [navigate, resetNotification, createErrorNotification, error]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fetchUserInfoError) {
|
if (fetchUserInfoError) {
|
||||||
createErrorNotification("There was an issue retrieving user preferences");
|
createErrorNotification("There was an issue retrieving user preferences");
|
||||||
}
|
}
|
||||||
}, [fetchUserInfoError, createErrorNotification]);
|
}, [fetchUserInfoError, resetNotification, createErrorNotification]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (err) {
|
|
||||||
navigate(IndexRoute);
|
|
||||||
console.error(`Unable to display consent screen: ${err.message}`);
|
|
||||||
}
|
|
||||||
}, [navigate, resetNotification, createErrorNotification, err]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch();
|
|
||||||
}, [fetch]);
|
|
||||||
|
|
||||||
const translateScopeNameToDescription = (id: string): string => {
|
const translateScopeNameToDescription = (id: string): string => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
|
@ -102,10 +111,10 @@ const ConsentView = function (props: Props) {
|
||||||
|
|
||||||
const handleAcceptConsent = async () => {
|
const handleAcceptConsent = async () => {
|
||||||
// This case should not happen in theory because the buttons are disabled when response is undefined.
|
// This case should not happen in theory because the buttons are disabled when response is undefined.
|
||||||
if (!resp) {
|
if (!response) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await acceptConsent(resp.client_id, preConfigure);
|
const res = await acceptConsent(preConfigure, response.client_id, consentID);
|
||||||
if (res.redirect_uri) {
|
if (res.redirect_uri) {
|
||||||
redirect(res.redirect_uri);
|
redirect(res.redirect_uri);
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,10 +123,10 @@ const ConsentView = function (props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRejectConsent = async () => {
|
const handleRejectConsent = async () => {
|
||||||
if (!resp) {
|
if (!response) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await rejectConsent(resp.client_id);
|
const res = await rejectConsent(response.client_id, consentID);
|
||||||
if (res.redirect_uri) {
|
if (res.redirect_uri) {
|
||||||
redirect(res.redirect_uri);
|
redirect(res.redirect_uri);
|
||||||
} else {
|
} else {
|
||||||
|
@ -126,7 +135,7 @@ const ConsentView = function (props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ComponentOrLoading ready={resp !== undefined && userInfo !== undefined}>
|
<ComponentOrLoading ready={response !== undefined && userInfo !== undefined}>
|
||||||
<LoginLayout
|
<LoginLayout
|
||||||
id="consent-stage"
|
id="consent-stage"
|
||||||
title={`${translate("Hi")} ${userInfo?.display_name}`}
|
title={`${translate("Hi")} ${userInfo?.display_name}`}
|
||||||
|
@ -138,14 +147,14 @@ const ConsentView = function (props: Props) {
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
translate("Client ID", { client_id: resp?.client_id }) ||
|
translate("Client ID", { client_id: response?.client_id }) ||
|
||||||
"Client ID: " + resp?.client_id
|
"Client ID: " + response?.client_id
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Typography className={styles.clientDescription}>
|
<Typography className={styles.clientDescription}>
|
||||||
{resp !== undefined && resp.client_description !== ""
|
{response !== undefined && response.client_description !== ""
|
||||||
? resp.client_description
|
? response.client_description
|
||||||
: resp?.client_id}
|
: response?.client_id}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,7 +165,7 @@ const ConsentView = function (props: Props) {
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<div className={styles.scopesListContainer}>
|
<div className={styles.scopesListContainer}>
|
||||||
<List className={styles.scopesList}>
|
<List className={styles.scopesList}>
|
||||||
{resp?.scopes.map((scope: string) => (
|
{response?.scopes.map((scope: string) => (
|
||||||
<Tooltip title={"Scope " + scope}>
|
<Tooltip title={"Scope " + scope}>
|
||||||
<ListItem id={"scope-" + scope} dense>
|
<ListItem id={"scope-" + scope} dense>
|
||||||
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
|
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
|
||||||
|
@ -167,7 +176,7 @@ const ConsentView = function (props: Props) {
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
{resp?.pre_configuration ? (
|
{response?.pre_configuration ? (
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
|
@ -197,7 +206,7 @@ const ConsentView = function (props: Props) {
|
||||||
<Button
|
<Button
|
||||||
id="accept-button"
|
id="accept-button"
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
disabled={!resp}
|
disabled={!response}
|
||||||
onClick={handleAcceptConsent}
|
onClick={handleAcceptConsent}
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -209,7 +218,7 @@ const ConsentView = function (props: Props) {
|
||||||
<Button
|
<Button
|
||||||
id="deny-button"
|
id="deny-button"
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
disabled={!resp}
|
disabled={!response}
|
||||||
onClick={handleRejectConsent}
|
onClick={handleRejectConsent}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { usePageVisibility } from "@hooks/PageVisibility";
|
||||||
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
||||||
import { useRequestMethod } from "@hooks/RequestMethod";
|
import { useRequestMethod } from "@hooks/RequestMethod";
|
||||||
import { useAutheliaState } from "@hooks/State";
|
import { useAutheliaState } from "@hooks/State";
|
||||||
|
import { useWorkflow } from "@hooks/Workflow";
|
||||||
import LoginLayout from "@layouts/LoginLayout";
|
import LoginLayout from "@layouts/LoginLayout";
|
||||||
import { postFirstFactor } from "@services/FirstFactor";
|
import { postFirstFactor } from "@services/FirstFactor";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
|
@ -34,6 +35,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
const requestMethod = useRequestMethod();
|
const requestMethod = useRequestMethod();
|
||||||
|
const workflow = useWorkflow();
|
||||||
|
|
||||||
const [state, fetchState, ,] = useAutheliaState();
|
const [state, fetchState, ,] = useAutheliaState();
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
|
@ -87,7 +89,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
|
|
||||||
props.onAuthenticationStart();
|
props.onAuthenticationStart();
|
||||||
try {
|
try {
|
||||||
const res = await postFirstFactor(username, password, rememberMe, redirectionURL, requestMethod);
|
const res = await postFirstFactor(username, password, rememberMe, redirectionURL, requestMethod, workflow);
|
||||||
props.onAuthenticationSuccess(res ? res.redirect : undefined);
|
props.onAuthenticationSuccess(res ? res.redirect : undefined);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { useRedirector } from "@hooks/Redirector";
|
||||||
import { useRequestMethod } from "@hooks/RequestMethod";
|
import { useRequestMethod } from "@hooks/RequestMethod";
|
||||||
import { useAutheliaState } from "@hooks/State";
|
import { useAutheliaState } from "@hooks/State";
|
||||||
import { useUserInfoPOST } from "@hooks/UserInfo";
|
import { useUserInfoPOST } from "@hooks/UserInfo";
|
||||||
|
import { useWorkflow } from "@hooks/Workflow";
|
||||||
import { SecondFactorMethod } from "@models/Methods";
|
import { SecondFactorMethod } from "@models/Methods";
|
||||||
import { checkSafeRedirection } from "@services/SafeRedirection";
|
import { checkSafeRedirection } from "@services/SafeRedirection";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
|
@ -41,6 +42,7 @@ const LoginPortal = function (props: Props) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
const requestMethod = useRequestMethod();
|
const requestMethod = useRequestMethod();
|
||||||
|
const workflow = useWorkflow();
|
||||||
const { createErrorNotification } = useNotifications();
|
const { createErrorNotification } = useNotifications();
|
||||||
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
|
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
|
||||||
const redirector = useRedirector();
|
const redirector = useRedirector();
|
||||||
|
@ -49,7 +51,16 @@ const LoginPortal = function (props: Props) {
|
||||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoPOST();
|
||||||
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
|
const [configuration, fetchConfiguration, , fetchConfigurationError] = useConfiguration();
|
||||||
|
|
||||||
const redirect = useCallback((url: string) => navigate(url), [navigate]);
|
const redirect = useCallback(
|
||||||
|
(pathname: string, search?: string) => {
|
||||||
|
if (search) {
|
||||||
|
navigate({ pathname: pathname, search: search });
|
||||||
|
} else {
|
||||||
|
navigate({ pathname: pathname });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
// Fetch the state when portal is mounted.
|
// Fetch the state when portal is mounted.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -119,23 +130,25 @@ const LoginPortal = function (props: Props) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectionSuffix = redirectionURL
|
const search = redirectionURL
|
||||||
? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}`
|
? `?rd=${encodeURIComponent(redirectionURL)}${requestMethod ? `&rm=${requestMethod}` : ""}${
|
||||||
: "";
|
workflow ? `&workflow=${workflow}` : ""
|
||||||
|
}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
||||||
setFirstFactorDisabled(false);
|
setFirstFactorDisabled(false);
|
||||||
redirect(`${IndexRoute}${redirectionSuffix}`);
|
redirect(IndexRoute, search);
|
||||||
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
|
} else if (state.authentication_level >= AuthenticationLevel.OneFactor && userInfo && configuration) {
|
||||||
if (configuration.available_methods.size === 0) {
|
if (configuration.available_methods.size === 0) {
|
||||||
redirect(AuthenticatedRoute);
|
redirect(AuthenticatedRoute);
|
||||||
} else {
|
} else {
|
||||||
if (userInfo.method === SecondFactorMethod.Webauthn) {
|
if (userInfo.method === SecondFactorMethod.Webauthn) {
|
||||||
redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}${redirectionSuffix}`);
|
redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`, search);
|
||||||
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
} else if (userInfo.method === SecondFactorMethod.MobilePush) {
|
||||||
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}${redirectionSuffix}`);
|
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`, search);
|
||||||
} else {
|
} else {
|
||||||
redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}${redirectionSuffix}`);
|
redirect(`${SecondFactorRoute}${SecondFactorTOTPSubRoute}`, search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +157,7 @@ const LoginPortal = function (props: Props) {
|
||||||
state,
|
state,
|
||||||
redirectionURL,
|
redirectionURL,
|
||||||
requestMethod,
|
requestMethod,
|
||||||
|
workflow,
|
||||||
redirect,
|
redirect,
|
||||||
userInfo,
|
userInfo,
|
||||||
setFirstFactorDisabled,
|
setFirstFactorDisabled,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
||||||
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
|
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
|
||||||
|
import { useWorkflow } from "@hooks/Workflow";
|
||||||
import { completeTOTPSignIn } from "@services/OneTimePassword";
|
import { completeTOTPSignIn } from "@services/OneTimePassword";
|
||||||
import { AuthenticationLevel } from "@services/State";
|
import { AuthenticationLevel } from "@services/State";
|
||||||
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
import LoadingPage from "@views/LoadingPage/LoadingPage";
|
||||||
|
@ -33,6 +34,7 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
|
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
|
||||||
);
|
);
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
|
const workflow = useWorkflow();
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
const { onSignInSuccess, onSignInError } = props;
|
const { onSignInSuccess, onSignInError } = props;
|
||||||
|
@ -67,7 +69,7 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(State.InProgress);
|
setState(State.InProgress);
|
||||||
const res = await completeTOTPSignIn(passcodeStr, redirectionURL);
|
const res = await completeTOTPSignIn(passcodeStr, redirectionURL, workflow);
|
||||||
setState(State.Success);
|
setState(State.Success);
|
||||||
onSignInSuccessCallback(res ? res.redirect : undefined);
|
onSignInSuccessCallback(res ? res.redirect : undefined);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -81,6 +83,7 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
onSignInSuccessCallback,
|
onSignInSuccessCallback,
|
||||||
passcode,
|
passcode,
|
||||||
redirectionURL,
|
redirectionURL,
|
||||||
|
workflow,
|
||||||
resp,
|
resp,
|
||||||
props.authenticationLevel,
|
props.authenticationLevel,
|
||||||
props.registered,
|
props.registered,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import PushNotificationIcon from "@components/PushNotificationIcon";
|
||||||
import SuccessIcon from "@components/SuccessIcon";
|
import SuccessIcon from "@components/SuccessIcon";
|
||||||
import { useIsMountedRef } from "@hooks/Mounted";
|
import { useIsMountedRef } from "@hooks/Mounted";
|
||||||
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
import { useRedirectionURL } from "@hooks/RedirectionURL";
|
||||||
|
import { useWorkflow } from "@hooks/Workflow";
|
||||||
import {
|
import {
|
||||||
completePushNotificationSignIn,
|
completePushNotificationSignIn,
|
||||||
completeDuoDeviceSelectionProcess,
|
completeDuoDeviceSelectionProcess,
|
||||||
|
@ -44,6 +45,7 @@ const PushNotificationMethod = function (props: Props) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [state, setState] = useState(State.SignInInProgress);
|
const [state, setState] = useState(State.SignInInProgress);
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
|
const workflow = useWorkflow();
|
||||||
const mounted = useIsMountedRef();
|
const mounted = useIsMountedRef();
|
||||||
const [enroll_url, setEnrollUrl] = useState("");
|
const [enroll_url, setEnrollUrl] = useState("");
|
||||||
const [devices, setDevices] = useState([] as SelectableDevice[]);
|
const [devices, setDevices] = useState([] as SelectableDevice[]);
|
||||||
|
@ -93,7 +95,7 @@ const PushNotificationMethod = function (props: Props) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setState(State.SignInInProgress);
|
setState(State.SignInInProgress);
|
||||||
const res = await completePushNotificationSignIn(redirectionURL);
|
const res = await completePushNotificationSignIn(redirectionURL, workflow);
|
||||||
// If the request was initiated and the user changed 2FA method in the meantime,
|
// If the request was initiated and the user changed 2FA method in the meantime,
|
||||||
// the process is interrupted to avoid updating state of unmounted component.
|
// the process is interrupted to avoid updating state of unmounted component.
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
|
@ -136,6 +138,7 @@ const PushNotificationMethod = function (props: Props) {
|
||||||
props.authenticationLevel,
|
props.authenticationLevel,
|
||||||
props.duoSelfEnrollment,
|
props.duoSelfEnrollment,
|
||||||
redirectionURL,
|
redirectionURL,
|
||||||
|
workflow,
|
||||||
mounted,
|
mounted,
|
||||||
onSignInErrorCallback,
|
onSignInErrorCallback,
|
||||||
onSignInSuccessCallback,
|
onSignInSuccessCallback,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user