feat(oidc): implement amr claim (#2969)
This adds the amr claim which stores methods used to authenticate with Authelia by the users session.pull/2789/head^2
parent
b2d35d88ec
commit
0116506330
|
@ -422,8 +422,8 @@ _**Important Note:** The claim `sub` is planned to be changed in the future to a
|
||||||
individual user. Please use the claim `preferred_username` instead._
|
individual user. Please use the claim `preferred_username` instead._
|
||||||
|
|
||||||
| Claim | JWT Type | Authelia Attribute | Description |
|
| Claim | JWT Type | Authelia Attribute | Description |
|
||||||
|:------------------:|:-------------:|:------------------:|:---------------------------------------------:|
|
|:---------:|:-------------:|:------------------:|:-----------------------------------------------------------:|
|
||||||
| sub | string | username | The username the user used to login with |
|
| sub | string | username | A unique value linked to the user who logged in |
|
||||||
| scope | string | scopes | Granted scopes (space delimited) |
|
| scope | string | scopes | Granted scopes (space delimited) |
|
||||||
| scp | array[string] | scopes | Granted scopes |
|
| scp | array[string] | scopes | Granted scopes |
|
||||||
| iss | string | hostname | The issuer name, determined by URL |
|
| iss | string | hostname | The issuer name, determined by URL |
|
||||||
|
@ -434,6 +434,7 @@ individual user. Please use the claim `preferred_username` instead._
|
||||||
| rat | number | _N/A_ | The time when the token was requested |
|
| rat | number | _N/A_ | The time when the token was requested |
|
||||||
| iat | number | _N/A_ | The time when the token was issued |
|
| iat | number | _N/A_ | The time when the token was issued |
|
||||||
| jti | string(uuid) | _N/A_ | JWT Identifier |
|
| jti | string(uuid) | _N/A_ | JWT Identifier |
|
||||||
|
| amr | array[string] | _N/A_ | An [RFC8176] list of authentication method reference values |
|
||||||
|
|
||||||
### groups
|
### groups
|
||||||
|
|
||||||
|
@ -462,6 +463,28 @@ This scope includes the profile information the authentication backend reports a
|
||||||
| preferred_username | string | username | The username the user used to login with |
|
| preferred_username | string | username | The username the user used to login with |
|
||||||
| name | string | display_name | The users display name |
|
| name | string | display_name | The users display name |
|
||||||
|
|
||||||
|
## Authentication Method References
|
||||||
|
|
||||||
|
Authelia currently supports adding the `amr` claim to the [ID Token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken)
|
||||||
|
utilizing the [RFC8176] Authentication Method Reference values.
|
||||||
|
|
||||||
|
The values this claim has are not strictly defined by the [OpenID Connect] specification. As such, some backends may
|
||||||
|
expect a specification other than [RFC8176] for this purpose. If you have such an application and wish for us to support
|
||||||
|
it then you're encouraged to create an issue.
|
||||||
|
|
||||||
|
Below is a list of the potential values we place in the claim and their meaning:
|
||||||
|
|
||||||
|
| Value | Description | Factor | Channel |
|
||||||
|
|:-----:|:----------------------------------------------------------------:|:------:|:--------:|
|
||||||
|
| mfa | User used multiple factors to login (see factor column) | N/A | N/A |
|
||||||
|
| mca | User used multiple channels to login (see channel column) | N/A | N/A |
|
||||||
|
| user | User confirmed they were present when using their hardware key | N/A | N/A |
|
||||||
|
| pin | User confirmed they are the owner of the hardware key with a pin | N/A | N/A |
|
||||||
|
| pwd | User used a username and password to login | Know | Browser |
|
||||||
|
| otp | User used TOTP to login | Have | Browser |
|
||||||
|
| hwk | User used a hardware key to login | Have | Browser |
|
||||||
|
| sms | User used Duo to login | Have | External |
|
||||||
|
|
||||||
## Endpoint Implementations
|
## Endpoint Implementations
|
||||||
|
|
||||||
This is a table of the endpoints we currently support and their paths. This can be requrired information for some RP's,
|
This is a table of the endpoints we currently support and their paths. This can be requrired information for some RP's,
|
||||||
|
@ -482,3 +505,4 @@ Authelia via https://auth.example.com, the discovery URL is https://auth.example
|
||||||
|
|
||||||
[OpenID Connect]: https://openid.net/connect/
|
[OpenID Connect]: https://openid.net/connect/
|
||||||
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
|
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
|
||||||
|
[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176
|
|
@ -9,7 +9,9 @@ import (
|
||||||
|
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
|
|
||||||
|
"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/session"
|
"github.com/authelia/authelia/v4/internal/session"
|
||||||
)
|
)
|
||||||
|
@ -97,7 +99,7 @@ func oidcAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *
|
||||||
|
|
||||||
subject := userSession.Username
|
subject := userSession.Username
|
||||||
oidcSession := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
|
oidcSession := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
|
||||||
subject, userSession.Username, extraClaims, authTime, workflowCreated, requester)
|
subject, userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, workflowCreated, requester)
|
||||||
|
|
||||||
ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v",
|
ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v",
|
||||||
requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims)
|
requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims)
|
||||||
|
@ -126,13 +128,13 @@ func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
|
||||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' requires user '%s' provides consent for scopes '%s'",
|
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' requires user '%s' provides consent for scopes '%s'",
|
||||||
requester.GetID(), client.GetID(), userSession.Username, strings.Join(requester.GetRequestedScopes(), "', '"))
|
requester.GetID(), client.GetID(), userSession.Username, strings.Join(requester.GetRequestedScopes(), "', '"))
|
||||||
|
|
||||||
userSession.OIDCWorkflowSession = &session.OIDCWorkflowSession{
|
userSession.OIDCWorkflowSession = &model.OIDCWorkflowSession{
|
||||||
ClientID: client.GetID(),
|
ClientID: client.GetID(),
|
||||||
RequestedScopes: requester.GetRequestedScopes(),
|
RequestedScopes: requester.GetRequestedScopes(),
|
||||||
RequestedAudience: requester.GetRequestedAudience(),
|
RequestedAudience: requester.GetRequestedAudience(),
|
||||||
AuthURI: redirectURL,
|
AuthURI: redirectURL,
|
||||||
TargetURI: requester.GetRedirectURI().String(),
|
TargetURI: requester.GetRedirectURI().String(),
|
||||||
RequiredAuthorizationLevel: client.Policy,
|
Require2FA: client.Policy == authorization.TwoFactor,
|
||||||
CreatedTimestamp: time.Now().Unix(),
|
CreatedTimestamp: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ func oidcConsent(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||||
ctx.Logger.Debugf("Insufficient permissions to give consent v2 %d -> %d", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel)
|
ctx.Logger.Debugf("Insufficient permissions to give consent during GET current level: %d, require 2FA: %t", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.Require2FA)
|
||||||
ctx.ReplyForbidden()
|
ctx.ReplyForbidden()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -59,7 +59,7 @@ func oidcConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||||
ctx.Logger.Debugf("Insufficient permissions to give consent v1 %d -> %d", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel)
|
ctx.Logger.Debugf("Insufficient permissions to give consent during POST current level: %d, require 2FA: %t", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.Require2FA)
|
||||||
ctx.ReplyForbidden()
|
ctx.ReplyForbidden()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -255,7 +255,7 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.SetTwoFactor(ctx.Clock.Now())
|
userSession.SetTwoFactorDuo(ctx.Clock.Now())
|
||||||
|
|
||||||
err = ctx.SaveSession(userSession)
|
err = ctx.SaveSession(userSession)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -68,7 +68,7 @@ func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.SetTwoFactor(ctx.Clock.Now())
|
userSession.SetTwoFactorTOTP(ctx.Clock.Now())
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
if err = ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrSessionSave, "authentication time", regulation.AuthTypeTOTP, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrSessionSave, "authentication time", regulation.AuthTypeTOTP, userSession.Username, err)
|
||||||
|
|
|
@ -185,8 +185,9 @@ func SecondFactorWebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userSession.SetTwoFactor(ctx.Clock.Now())
|
userSession.SetTwoFactorWebauthn(ctx.Clock.Now(),
|
||||||
userSession.Webauthn = nil
|
assertionResponse.Response.AuthenticatorData.Flags.UserPresent(),
|
||||||
|
assertionResponse.Response.AuthenticatorData.Flags.UserVerified())
|
||||||
|
|
||||||
if err = ctx.SaveSession(userSession); err != nil {
|
if err = ctx.SaveSession(userSession); err != nil {
|
||||||
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err)
|
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"github.com/ory/fosite"
|
"github.com/ory/fosite"
|
||||||
|
|
||||||
|
"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/session"
|
"github.com/authelia/authelia/v4/internal/session"
|
||||||
"github.com/authelia/authelia/v4/internal/utils"
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
|
@ -10,7 +11,7 @@ import (
|
||||||
|
|
||||||
// isConsentMissing compares the requestedScopes and requestedAudience to the workflows
|
// isConsentMissing compares the requestedScopes and requestedAudience to the workflows
|
||||||
// GrantedScopes and GrantedAudience and returns true if they do not match or the workflow is nil.
|
// GrantedScopes and GrantedAudience and returns true if they do not match or the workflow is nil.
|
||||||
func isConsentMissing(workflow *session.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
|
func isConsentMissing(workflow *model.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
|
||||||
if workflow == nil {
|
if workflow == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,19 +6,20 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"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/session"
|
"github.com/authelia/authelia/v4/internal/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldDetectIfConsentIsMissing(t *testing.T) {
|
func TestShouldDetectIfConsentIsMissing(t *testing.T) {
|
||||||
var workflow *session.OIDCWorkflowSession
|
var workflow *model.OIDCWorkflowSession
|
||||||
|
|
||||||
requestedScopes := []string{"openid", "profile"}
|
requestedScopes := []string{"openid", "profile"}
|
||||||
requestedAudience := []string{"https://authelia.com"}
|
requestedAudience := []string{"https://authelia.com"}
|
||||||
|
|
||||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||||
|
|
||||||
workflow = &session.OIDCWorkflowSession{
|
workflow = &model.OIDCWorkflowSession{
|
||||||
GrantedScopes: []string{"openid", "profile"},
|
GrantedScopes: []string{"openid", "profile"},
|
||||||
GrantedAudience: []string{"https://authelia.com"},
|
GrantedAudience: []string{"https://authelia.com"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
"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/utils"
|
"github.com/authelia/authelia/v4/internal/utils"
|
||||||
|
@ -16,7 +17,7 @@ import (
|
||||||
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
||||||
userSession := ctx.GetSession()
|
userSession := ctx.GetSession()
|
||||||
|
|
||||||
if !authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.RequiredAuthorizationLevel) {
|
if userSession.OIDCWorkflowSession.Require2FA && userSession.AuthenticationLevel != authentication.TwoFactor {
|
||||||
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", userSession.OIDCWorkflowSession.ClientID)
|
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", userSession.OIDCWorkflowSession.ClientID)
|
||||||
ctx.ReplyOK()
|
ctx.ReplyOK()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// OIDCWorkflowSession represent an OIDC workflow session.
|
||||||
|
type OIDCWorkflowSession struct {
|
||||||
|
ClientID string
|
||||||
|
RequestedScopes []string
|
||||||
|
GrantedScopes []string
|
||||||
|
RequestedAudience []string
|
||||||
|
GrantedAudience []string
|
||||||
|
TargetURI string
|
||||||
|
AuthURI string
|
||||||
|
Require2FA bool
|
||||||
|
CreatedTimestamp int64
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
// AuthenticationMethodsReferences holds AMR information.
|
||||||
|
type AuthenticationMethodsReferences struct {
|
||||||
|
UsernameAndPassword bool
|
||||||
|
TOTP bool
|
||||||
|
Duo bool
|
||||||
|
Webauthn bool
|
||||||
|
WebauthnUserPresence bool
|
||||||
|
WebauthnUserVerified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FactorKnowledge returns true if a "something you know" factor of authentication was used.
|
||||||
|
func (r AuthenticationMethodsReferences) FactorKnowledge() bool {
|
||||||
|
return r.UsernameAndPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// FactorPossession returns true if a "something you have" factor of authentication was used.
|
||||||
|
func (r AuthenticationMethodsReferences) FactorPossession() bool {
|
||||||
|
return r.TOTP || r.Webauthn || r.Duo
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiFactorAuthentication returns true if multiple factors were used.
|
||||||
|
func (r AuthenticationMethodsReferences) MultiFactorAuthentication() bool {
|
||||||
|
return r.FactorKnowledge() && r.FactorPossession()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelBrowser returns true if a browser was used to authenticate.
|
||||||
|
func (r AuthenticationMethodsReferences) ChannelBrowser() bool {
|
||||||
|
return r.UsernameAndPassword || r.TOTP || r.Webauthn
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChannelService returns true if a non-browser service was used to authenticate.
|
||||||
|
func (r AuthenticationMethodsReferences) ChannelService() bool {
|
||||||
|
return r.Duo
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiChannelAuthentication returns true if the user used more than one channel to authenticate.
|
||||||
|
func (r AuthenticationMethodsReferences) MultiChannelAuthentication() bool {
|
||||||
|
return r.ChannelBrowser() && r.ChannelService()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalRFC8176 returns the AMR claim slice of strings in the RFC8176 format.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
func (r AuthenticationMethodsReferences) MarshalRFC8176() []string {
|
||||||
|
var amr []string
|
||||||
|
|
||||||
|
if r.UsernameAndPassword {
|
||||||
|
amr = append(amr, AMRPasswordBasedAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.TOTP {
|
||||||
|
amr = append(amr, AMROneTimePassword)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Duo {
|
||||||
|
amr = append(amr, AMRShortMessageService)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Webauthn {
|
||||||
|
amr = append(amr, AMRHardwareSecuredKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.WebauthnUserPresence {
|
||||||
|
amr = append(amr, AMRUserPresence)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.WebauthnUserVerified {
|
||||||
|
amr = append(amr, AMRPersonalIdentificationNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MultiFactorAuthentication() {
|
||||||
|
amr = append(amr, AMRMultiFactorAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.MultiChannelAuthentication() {
|
||||||
|
amr = append(amr, AMRMultiChannelAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
|
return amr
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
package oidc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testAMRWant struct {
|
||||||
|
FactorKnowledge, FactorPossession, MultiFactorAuthentication bool
|
||||||
|
ChannelBrowser, ChannelService, MultiChannelAuthentication bool
|
||||||
|
|
||||||
|
RFC8176 []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthenticationMethodsReferences(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
is AuthenticationMethodsReferences
|
||||||
|
want testAMRWant
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "Username and Password",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{UsernameAndPassword: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: true,
|
||||||
|
FactorPossession: false,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: false,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"pwd"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "TOTP",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{TOTP: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: false,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"otp"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Webauthn",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{Webauthn: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: false,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"hwk"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Webauthn User Presence",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{WebauthnUserPresence: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: false,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: false,
|
||||||
|
ChannelService: false,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"user"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Webauthn User Verified",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{WebauthnUserVerified: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: false,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: false,
|
||||||
|
ChannelService: false,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"pin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Webauthn with User Presence and Verified",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{Webauthn: true, WebauthnUserVerified: true, WebauthnUserPresence: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: false,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"hwk", "user", "pin"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Duo",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{Duo: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: false,
|
||||||
|
ChannelService: true,
|
||||||
|
MultiChannelAuthentication: false,
|
||||||
|
RFC8176: []string{"sms"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Duo Webauthn TOTP",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{Duo: true, Webauthn: true, TOTP: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: true,
|
||||||
|
MultiChannelAuthentication: true,
|
||||||
|
RFC8176: []string{"sms", "hwk", "otp", "mca"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Duo TOTP",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{Duo: true, TOTP: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: false,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: false,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: true,
|
||||||
|
MultiChannelAuthentication: true,
|
||||||
|
RFC8176: []string{"sms", "otp", "mca"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Username and Password with Duo",
|
||||||
|
|
||||||
|
is: AuthenticationMethodsReferences{Duo: true, UsernameAndPassword: true},
|
||||||
|
want: testAMRWant{
|
||||||
|
FactorKnowledge: true,
|
||||||
|
FactorPossession: true,
|
||||||
|
MultiFactorAuthentication: true,
|
||||||
|
ChannelBrowser: true,
|
||||||
|
ChannelService: true,
|
||||||
|
MultiChannelAuthentication: true,
|
||||||
|
RFC8176: []string{"pwd", "sms", "mfa", "mca"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.desc, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tc.want.FactorKnowledge, tc.is.FactorKnowledge())
|
||||||
|
assert.Equal(t, tc.want.FactorPossession, tc.is.FactorPossession())
|
||||||
|
assert.Equal(t, tc.want.MultiFactorAuthentication, tc.is.MultiFactorAuthentication())
|
||||||
|
assert.Equal(t, tc.want.ChannelBrowser, tc.is.ChannelBrowser())
|
||||||
|
assert.Equal(t, tc.want.ChannelService, tc.is.ChannelService())
|
||||||
|
assert.Equal(t, tc.want.MultiChannelAuthentication, tc.is.MultiChannelAuthentication())
|
||||||
|
|
||||||
|
isRFC8176 := tc.is.MarshalRFC8176()
|
||||||
|
|
||||||
|
for _, amr := range tc.want.RFC8176 {
|
||||||
|
t.Run(fmt.Sprintf("has all wanted/%s", amr), func(t *testing.T) {
|
||||||
|
assert.Contains(t, isRFC8176, amr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, amr := range isRFC8176 {
|
||||||
|
t.Run(fmt.Sprintf("only has wanted/%s", amr), func(t *testing.T) {
|
||||||
|
assert.Contains(t, tc.want.RFC8176, amr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/authentication"
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/v4/internal/session"
|
"github.com/authelia/authelia/v4/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewClient creates a new InternalClient.
|
// NewClient creates a new InternalClient.
|
||||||
|
@ -46,8 +46,8 @@ func (c InternalClient) GetID() string {
|
||||||
return c.ID
|
return c.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConsentResponseBody returns the proper consent response body for this session.OIDCWorkflowSession.
|
// GetConsentResponseBody returns the proper consent response body for this model.OIDCWorkflowSession.
|
||||||
func (c InternalClient) GetConsentResponseBody(session *session.OIDCWorkflowSession) ConsentGetResponseBody {
|
func (c InternalClient) GetConsentResponseBody(session *model.OIDCWorkflowSession) ConsentGetResponseBody {
|
||||||
body := ConsentGetResponseBody{
|
body := ConsentGetResponseBody{
|
||||||
ClientID: c.ID,
|
ClientID: c.ID,
|
||||||
ClientDescription: c.Description,
|
ClientDescription: c.Description,
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/authentication"
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
"github.com/authelia/authelia/v4/internal/session"
|
"github.com/authelia/authelia/v4/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
|
@ -79,7 +79,7 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
|
||||||
c.ID = "myclient"
|
c.ID = "myclient"
|
||||||
c.Description = "My Client"
|
c.Description = "My Client"
|
||||||
|
|
||||||
workflow := &session.OIDCWorkflowSession{
|
workflow := &model.OIDCWorkflowSession{
|
||||||
RequestedAudience: []string{"https://example.com"},
|
RequestedAudience: []string{"https://example.com"},
|
||||||
RequestedScopes: []string{"openid", "groups"},
|
RequestedScopes: []string{"openid", "groups"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,3 +31,97 @@ const (
|
||||||
RevocationPath = "/api/oidc/revocation"
|
RevocationPath = "/api/oidc/revocation"
|
||||||
UserinfoPath = "/api/oidc/userinfo"
|
UserinfoPath = "/api/oidc/userinfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Authentication Method Reference Values https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
const (
|
||||||
|
// AMRMultiFactorAuthentication is an RFC8176 Authentication Method Reference Value that represents multiple-factor
|
||||||
|
// authentication as per NIST.800-63-2 and ISO29115. When this is present, specific authentication methods used may
|
||||||
|
// also be included.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has performed any 2 AMR's with different factor values (excluding meta).
|
||||||
|
// Factor: Meta, Channel: Meta.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
//
|
||||||
|
// NIST.800-63-2: http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63-2.pdf
|
||||||
|
//
|
||||||
|
// ISO29115: https://www.iso.org/standard/45138.html
|
||||||
|
AMRMultiFactorAuthentication = "mfa"
|
||||||
|
|
||||||
|
// AMRMultiChannelAuthentication is an RFC8176 Authentication Method Reference Value that represents
|
||||||
|
// multiple-channel authentication. The authentication involves communication over more than one distinct
|
||||||
|
// communication channel. For instance, a multiple-channel authentication might involve both entering information
|
||||||
|
// into a workstation's browser and providing information on a telephone call to a pre-registered number.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has performed any 2 AMR's with different channel values (excluding meta).
|
||||||
|
// Factor: Meta, Channel: Meta.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
AMRMultiChannelAuthentication = "mca"
|
||||||
|
|
||||||
|
// AMRUserPresence is an RFC8176 Authentication Method Reference Value that represents authentication that included
|
||||||
|
// a user presence test. Evidence that the end user is present and interacting with the device. This is sometimes
|
||||||
|
// also referred to as "test of user presence" as per W3C.WD-webauthn-20170216.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has used Webauthn to authenticate and the user presence flag was set.
|
||||||
|
// Factor: Meta, Channel: Meta.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
//
|
||||||
|
// W3C.WD-webauthn-20170216: https://datatracker.ietf.org/doc/html/rfc8176#ref-W3C.WD-webauthn-20170216
|
||||||
|
AMRUserPresence = "user"
|
||||||
|
|
||||||
|
// AMRPersonalIdentificationNumber is an RFC8176 Authentication Method Reference Value that represents
|
||||||
|
// authentication that included a personal Identification Number (PIN) as per RFC4949 or pattern (not restricted to
|
||||||
|
// containing only numbers) that a user enters to unlock a key on the device. This mechanism should have a way to
|
||||||
|
// deter an attacker from obtaining the PIN by trying repeated guesses.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has used Webauthn to authenticate and the user verified flag was set.
|
||||||
|
// Factor: Meta, Channel: Meta.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
//
|
||||||
|
// RFC4949: https://datatracker.ietf.org/doc/html/rfc4949
|
||||||
|
AMRPersonalIdentificationNumber = "pin"
|
||||||
|
|
||||||
|
// AMRPasswordBasedAuthentication is an RFC8176 Authentication Method Reference Value that represents password-based
|
||||||
|
// authentication as per RFC4949.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has performed 1FA. Factor: Know, Channel: Browser.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
//
|
||||||
|
// RFC4949: https://datatracker.ietf.org/doc/html/rfc4949
|
||||||
|
AMRPasswordBasedAuthentication = "pwd"
|
||||||
|
|
||||||
|
// AMROneTimePassword is an RFC8176 Authentication Method Reference Value that represents authentication via a
|
||||||
|
// one-time password as per RFC4949. One-time password specifications that this authentication method applies to
|
||||||
|
// include RFC4226 and RFC6238.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has used TOTP to authenticate. Factor: Have, Channel: Browser.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
//
|
||||||
|
// RFC4949: https://datatracker.ietf.org/doc/html/rfc4949
|
||||||
|
//
|
||||||
|
// RFC4226: https://datatracker.ietf.org/doc/html/rfc4226
|
||||||
|
//
|
||||||
|
// RFC6238: https://datatracker.ietf.org/doc/html/rfc6238
|
||||||
|
AMROneTimePassword = "otp"
|
||||||
|
|
||||||
|
// AMRHardwareSecuredKey is an RFC8176 Authentication Method Reference Value that
|
||||||
|
// represents authentication via a proof-of-Possession (PoP) of a hardware-secured key.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has used Webauthn to authenticate. Factor: Have, Channel: Browser.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
AMRHardwareSecuredKey = "hwk"
|
||||||
|
|
||||||
|
// AMRShortMessageService is an RFC8176 Authentication Method Reference Value that
|
||||||
|
// represents authentication via confirmation using SMS text message to the user at a registered number.
|
||||||
|
//
|
||||||
|
// Authelia utilizes this when a user has used Duo to authenticate. Factor: Have, Channel: Browser.
|
||||||
|
//
|
||||||
|
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
|
||||||
|
AMRShortMessageService = "sms"
|
||||||
|
)
|
||||||
|
|
|
@ -25,6 +25,7 @@ func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (st
|
||||||
AccessTokens: map[string]fosite.Requester{},
|
AccessTokens: map[string]fosite.Requester{},
|
||||||
RefreshTokens: map[string]storage.StoreRefreshToken{},
|
RefreshTokens: map[string]storage.StoreRefreshToken{},
|
||||||
PKCES: map[string]fosite.Requester{},
|
PKCES: map[string]fosite.Requester{},
|
||||||
|
BlacklistedJTIs: map[string]time.Time{},
|
||||||
AccessTokenRequestIDs: map[string]string{},
|
AccessTokenRequestIDs: map[string]string{},
|
||||||
RefreshTokenRequestIDs: map[string]string{},
|
RefreshTokenRequestIDs: map[string]string{},
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,7 +30,7 @@ func NewSession() (session *OpenIDSession) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSessionWithAuthorizeRequest uses details from an AuthorizeRequester to generate an OpenIDSession.
|
// NewSessionWithAuthorizeRequest uses details from an AuthorizeRequester to generate an OpenIDSession.
|
||||||
func NewSessionWithAuthorizeRequest(issuer, kid, subject, username string, extra map[string]interface{},
|
func NewSessionWithAuthorizeRequest(issuer, kid, subject, username string, amr []string, extra map[string]interface{},
|
||||||
authTime, requestedAt time.Time, requester fosite.AuthorizeRequester) (session *OpenIDSession) {
|
authTime, requestedAt time.Time, requester fosite.AuthorizeRequester) (session *OpenIDSession) {
|
||||||
if extra == nil {
|
if extra == nil {
|
||||||
extra = make(map[string]interface{})
|
extra = make(map[string]interface{})
|
||||||
|
@ -47,6 +47,8 @@ func NewSessionWithAuthorizeRequest(issuer, kid, subject, username string, extra
|
||||||
Nonce: requester.GetRequestForm().Get("nonce"),
|
Nonce: requester.GetRequestForm().Get("nonce"),
|
||||||
Audience: requester.GetGrantedAudience(),
|
Audience: requester.GetGrantedAudience(),
|
||||||
Extra: extra,
|
Extra: extra,
|
||||||
|
|
||||||
|
AuthenticationMethodsReferences: amr,
|
||||||
},
|
},
|
||||||
Headers: &jwt.Headers{
|
Headers: &jwt.Headers{
|
||||||
Extra: map[string]interface{}{
|
Extra: map[string]interface{}{
|
||||||
|
|
|
@ -49,8 +49,9 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
|
||||||
requested := time.Unix(1647332518, 0)
|
requested := time.Unix(1647332518, 0)
|
||||||
authAt := time.Unix(1647332500, 0)
|
authAt := time.Unix(1647332500, 0)
|
||||||
issuer := "https://example.com"
|
issuer := "https://example.com"
|
||||||
|
amr := []string{AMRPasswordBasedAuthentication}
|
||||||
|
|
||||||
session := NewSessionWithAuthorizeRequest(issuer, "primary", subject.String(), "john", extra, authAt, requested, request)
|
session := NewSessionWithAuthorizeRequest(issuer, "primary", subject.String(), "john", amr, extra, authAt, requested, request)
|
||||||
|
|
||||||
require.NotNil(t, session)
|
require.NotNil(t, session)
|
||||||
require.NotNil(t, session.Extra)
|
require.NotNil(t, session.Extra)
|
||||||
|
@ -58,24 +59,29 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
|
||||||
require.NotNil(t, session.Headers.Extra)
|
require.NotNil(t, session.Headers.Extra)
|
||||||
require.NotNil(t, session.Claims)
|
require.NotNil(t, session.Claims)
|
||||||
require.NotNil(t, session.Claims.Extra)
|
require.NotNil(t, session.Claims.Extra)
|
||||||
|
require.NotNil(t, session.Claims.AuthenticationMethodsReferences)
|
||||||
|
|
||||||
assert.Equal(t, "abc123xyzauthelia", session.Claims.Nonce)
|
|
||||||
assert.Equal(t, subject.String(), session.Claims.Subject)
|
|
||||||
assert.Equal(t, subject.String(), session.Subject)
|
assert.Equal(t, subject.String(), session.Subject)
|
||||||
assert.Equal(t, issuer, session.Claims.Issuer)
|
|
||||||
assert.Equal(t, "primary", session.Headers.Get("kid"))
|
|
||||||
assert.Equal(t, "example", session.ClientID)
|
assert.Equal(t, "example", session.ClientID)
|
||||||
assert.Equal(t, requested, session.Claims.RequestedAt)
|
|
||||||
assert.Equal(t, authAt, session.Claims.AuthTime)
|
|
||||||
assert.Greater(t, session.Claims.IssuedAt.Unix(), authAt.Unix())
|
assert.Greater(t, session.Claims.IssuedAt.Unix(), authAt.Unix())
|
||||||
assert.Equal(t, "john", session.Username)
|
assert.Equal(t, "john", session.Username)
|
||||||
|
|
||||||
require.Contains(t, session.Claims.Extra, "preferred_username")
|
assert.Equal(t, "abc123xyzauthelia", session.Claims.Nonce)
|
||||||
|
assert.Equal(t, subject.String(), session.Claims.Subject)
|
||||||
|
assert.Equal(t, amr, session.Claims.AuthenticationMethodsReferences)
|
||||||
|
assert.Equal(t, authAt, session.Claims.AuthTime)
|
||||||
|
assert.Equal(t, requested, session.Claims.RequestedAt)
|
||||||
|
assert.Equal(t, issuer, session.Claims.Issuer)
|
||||||
assert.Equal(t, "john", session.Claims.Extra["preferred_username"])
|
assert.Equal(t, "john", session.Claims.Extra["preferred_username"])
|
||||||
|
|
||||||
session = NewSessionWithAuthorizeRequest(issuer, "primary", subject.String(), "john", nil, authAt, requested, request)
|
assert.Equal(t, "primary", session.Headers.Get("kid"))
|
||||||
|
|
||||||
|
require.Contains(t, session.Claims.Extra, "preferred_username")
|
||||||
|
|
||||||
|
session = NewSessionWithAuthorizeRequest(issuer, "primary", subject.String(), "john", nil, nil, authAt, requested, request)
|
||||||
|
|
||||||
require.NotNil(t, session)
|
require.NotNil(t, session)
|
||||||
require.NotNil(t, session.Claims)
|
require.NotNil(t, session.Claims)
|
||||||
assert.NotNil(t, session.Claims.Extra)
|
assert.NotNil(t, session.Claims.Extra)
|
||||||
|
assert.Nil(t, session.Claims.AuthenticationMethodsReferences)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/authelia/authelia/v4/internal/authentication"
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
"github.com/authelia/authelia/v4/internal/authorization"
|
||||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||||
|
"github.com/authelia/authelia/v4/internal/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestShouldInitializerSession(t *testing.T) {
|
func TestShouldInitializerSession(t *testing.T) {
|
||||||
|
@ -93,9 +94,10 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
||||||
AuthenticationLevel: authentication.OneFactor,
|
AuthenticationLevel: authentication.OneFactor,
|
||||||
LastActivity: timeOneFactor.Unix(),
|
LastActivity: timeOneFactor.Unix(),
|
||||||
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
||||||
|
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true},
|
||||||
}, session)
|
}, session)
|
||||||
|
|
||||||
session.SetTwoFactor(timeTwoFactor)
|
session.SetTwoFactorDuo(timeTwoFactor)
|
||||||
|
|
||||||
err = provider.SaveSession(ctx, session)
|
err = provider.SaveSession(ctx, session)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -109,6 +111,7 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
||||||
LastActivity: timeTwoFactor.Unix(),
|
LastActivity: timeTwoFactor.Unix(),
|
||||||
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
||||||
SecondFactorAuthnTimestamp: timeTwoFactor.Unix(),
|
SecondFactorAuthnTimestamp: timeTwoFactor.Unix(),
|
||||||
|
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Duo: true},
|
||||||
}, session)
|
}, session)
|
||||||
|
|
||||||
authAt, err = session.AuthenticatedTime(authorization.OneFactor)
|
authAt, err = session.AuthenticatedTime(authorization.OneFactor)
|
||||||
|
@ -124,6 +127,169 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
|
||||||
assert.Equal(t, timeZeroFactor, authAt)
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
|
||||||
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
configuration := schema.SessionConfiguration{}
|
||||||
|
|
||||||
|
timeOneFactor := time.Unix(1625048140, 0)
|
||||||
|
timeTwoFactor := time.Unix(1625048150, 0)
|
||||||
|
timeZeroFactor := time.Unix(0, 0)
|
||||||
|
|
||||||
|
configuration.Domain = testDomain
|
||||||
|
configuration.Name = testName
|
||||||
|
configuration.Expiration = testExpiration
|
||||||
|
|
||||||
|
provider := NewProvider(configuration, nil)
|
||||||
|
session, _ := provider.GetSession(ctx)
|
||||||
|
|
||||||
|
session.SetOneFactor(timeOneFactor, &authentication.UserDetails{Username: testUsername}, false)
|
||||||
|
|
||||||
|
err := provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
authAt, err := session.AuthenticatedTime(authorization.OneFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeOneFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.Denied)
|
||||||
|
assert.EqualError(t, err, "invalid authorization level")
|
||||||
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
|
|
||||||
|
assert.Equal(t, UserSession{
|
||||||
|
Username: testUsername,
|
||||||
|
AuthenticationLevel: authentication.OneFactor,
|
||||||
|
LastActivity: timeOneFactor.Unix(),
|
||||||
|
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
|
||||||
|
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true},
|
||||||
|
}, session)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, session.AuthenticationMethodRefs)
|
||||||
|
assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication())
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.OneFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeOneFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.TwoFactor)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, timeTwoFactor, authAt)
|
||||||
|
|
||||||
|
authAt, err = session.AuthenticatedTime(authorization.Denied)
|
||||||
|
assert.EqualError(t, err, "invalid authorization level")
|
||||||
|
assert.Equal(t, timeZeroFactor, authAt)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, false, false)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, true, false)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, true, false)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, false, true)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorWebauthn(timeTwoFactor, false, true)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorTOTP(timeTwoFactor)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
|
||||||
|
session.SetTwoFactorTOTP(timeTwoFactor)
|
||||||
|
|
||||||
|
err = provider.SaveSession(ctx, session)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
session, err = provider.GetSession(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true},
|
||||||
|
session.AuthenticationMethodRefs)
|
||||||
|
}
|
||||||
|
|
||||||
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
|
func TestShouldDestroySessionAndWipeSessionData(t *testing.T) {
|
||||||
ctx := &fasthttp.RequestCtx{}
|
ctx := &fasthttp.RequestCtx{}
|
||||||
configuration := schema.SessionConfiguration{}
|
configuration := schema.SessionConfiguration{}
|
||||||
|
|
|
@ -10,8 +10,9 @@ import (
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/authentication"
|
"github.com/authelia/authelia/v4/internal/authentication"
|
||||||
"github.com/authelia/authelia/v4/internal/authorization"
|
|
||||||
"github.com/authelia/authelia/v4/internal/logging"
|
"github.com/authelia/authelia/v4/internal/logging"
|
||||||
|
"github.com/authelia/authelia/v4/internal/model"
|
||||||
|
"github.com/authelia/authelia/v4/internal/oidc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProviderConfig is the configuration used to create the session provider.
|
// ProviderConfig is the configuration used to create the session provider.
|
||||||
|
@ -37,11 +38,13 @@ type UserSession struct {
|
||||||
FirstFactorAuthnTimestamp int64
|
FirstFactorAuthnTimestamp int64
|
||||||
SecondFactorAuthnTimestamp int64
|
SecondFactorAuthnTimestamp int64
|
||||||
|
|
||||||
|
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
|
||||||
|
|
||||||
// Webauthn holds the session registration data for this session.
|
// Webauthn holds the session registration data for this session.
|
||||||
Webauthn *webauthn.SessionData
|
Webauthn *webauthn.SessionData
|
||||||
|
|
||||||
// Represent an OIDC workflow session initiated by the client if not null.
|
// Represent an OIDC workflow session initiated by the client if not null.
|
||||||
OIDCWorkflowSession *OIDCWorkflowSession
|
OIDCWorkflowSession *model.OIDCWorkflowSession
|
||||||
|
|
||||||
// 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.
|
||||||
|
@ -56,19 +59,6 @@ type Identity struct {
|
||||||
Email string
|
Email string
|
||||||
}
|
}
|
||||||
|
|
||||||
// OIDCWorkflowSession represent an OIDC workflow session.
|
|
||||||
type OIDCWorkflowSession struct {
|
|
||||||
ClientID string
|
|
||||||
RequestedScopes []string
|
|
||||||
GrantedScopes []string
|
|
||||||
RequestedAudience []string
|
|
||||||
GrantedAudience []string
|
|
||||||
TargetURI string
|
|
||||||
AuthURI string
|
|
||||||
RequiredAuthorizationLevel authorization.Level
|
|
||||||
CreatedTimestamp int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRedisLogger() *redisLogger {
|
func newRedisLogger() *redisLogger {
|
||||||
return &redisLogger{logger: logging.Logger()}
|
return &redisLogger{logger: logging.Logger()}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ func NewDefaultUserSession() UserSession {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetOneFactor sets the expected property values for one factor authentication.
|
// SetOneFactor sets the 1FA AMR's and expected property values for one factor authentication.
|
||||||
func (s *UserSession) SetOneFactor(now time.Time, details *authentication.UserDetails, keepMeLoggedIn bool) {
|
func (s *UserSession) SetOneFactor(now time.Time, details *authentication.UserDetails, keepMeLoggedIn bool) {
|
||||||
s.FirstFactorAuthnTimestamp = now.Unix()
|
s.FirstFactorAuthnTimestamp = now.Unix()
|
||||||
s.LastActivity = now.Unix()
|
s.LastActivity = now.Unix()
|
||||||
|
@ -29,15 +29,37 @@ func (s *UserSession) SetOneFactor(now time.Time, details *authentication.UserDe
|
||||||
s.DisplayName = details.DisplayName
|
s.DisplayName = details.DisplayName
|
||||||
s.Groups = details.Groups
|
s.Groups = details.Groups
|
||||||
s.Emails = details.Emails
|
s.Emails = details.Emails
|
||||||
|
|
||||||
|
s.AuthenticationMethodRefs.UsernameAndPassword = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTwoFactor sets the expected property values for two factor authentication.
|
func (s *UserSession) setTwoFactor(now time.Time) {
|
||||||
func (s *UserSession) SetTwoFactor(now time.Time) {
|
|
||||||
s.SecondFactorAuthnTimestamp = now.Unix()
|
s.SecondFactorAuthnTimestamp = now.Unix()
|
||||||
s.LastActivity = now.Unix()
|
s.LastActivity = now.Unix()
|
||||||
s.AuthenticationLevel = authentication.TwoFactor
|
s.AuthenticationLevel = authentication.TwoFactor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetTwoFactorTOTP sets the relevant TOTP AMR's and sets the factor to 2FA.
|
||||||
|
func (s *UserSession) SetTwoFactorTOTP(now time.Time) {
|
||||||
|
s.setTwoFactor(now)
|
||||||
|
s.AuthenticationMethodRefs.TOTP = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTwoFactorDuo sets the relevant Duo AMR's and sets the factor to 2FA.
|
||||||
|
func (s *UserSession) SetTwoFactorDuo(now time.Time) {
|
||||||
|
s.setTwoFactor(now)
|
||||||
|
s.AuthenticationMethodRefs.Duo = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTwoFactorWebauthn sets the relevant Webauthn AMR's and sets the factor to 2FA.
|
||||||
|
func (s *UserSession) SetTwoFactorWebauthn(now time.Time, userPresence, userVerified bool) {
|
||||||
|
s.setTwoFactor(now)
|
||||||
|
s.AuthenticationMethodRefs.Webauthn = true
|
||||||
|
s.AuthenticationMethodRefs.WebauthnUserPresence, s.AuthenticationMethodRefs.WebauthnUserVerified = userPresence, userVerified
|
||||||
|
|
||||||
|
s.Webauthn = nil
|
||||||
|
}
|
||||||
|
|
||||||
// AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level.
|
// AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level.
|
||||||
func (s UserSession) AuthenticatedTime(level authorization.Level) (authenticatedTime time.Time, err error) {
|
func (s UserSession) AuthenticatedTime(level authorization.Level) (authenticatedTime time.Time, err error) {
|
||||||
switch level {
|
switch level {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
version: '3'
|
version: '3'
|
||||||
services:
|
services:
|
||||||
oidc-client:
|
oidc-client:
|
||||||
image: ghcr.io/authelia/oidc-tester-app:master-2a82ab3
|
image: ghcr.io/authelia/oidc-tester-app:master-89622a8
|
||||||
command: /entrypoint.sh
|
command: /entrypoint.sh
|
||||||
depends_on:
|
depends_on:
|
||||||
- authelia-backend
|
- authelia-backend
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
while true;
|
while true;
|
||||||
do
|
do
|
||||||
oidc-tester-app --issuer https://login.example.com:8080 --id oidc-tester-app --secret foobar --scopes openid,profile,email --redirect-domain oidc.example.com
|
oidc-tester-app --issuer https://login.example.com:8080 --id oidc-tester-app --secret foobar --scopes openid,profile,email --public-url https://oidc.example.com:8080
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
Loading…
Reference in New Issue