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
James Elliott 2022-04-01 22:18:58 +11:00 committed by GitHub
parent b2d35d88ec
commit 0116506330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 663 additions and 68 deletions

View File

@ -421,19 +421,20 @@ does.
_**Important Note:** The claim `sub` is planned to be changed in the future to a randomly unique value to identify the
individual user. Please use the claim `preferred_username` instead._
| Claim | JWT Type | Authelia Attribute | Description |
|:------------------:|:-------------:|:------------------:|:---------------------------------------------:|
| sub | string | username | The username the user used to login with |
| scope | string | scopes | Granted scopes (space delimited) |
| scp | array[string] | scopes | Granted scopes |
| iss | string | hostname | The issuer name, determined by URL |
| at_hash | string | _N/A_ | Access Token Hash |
| aud | array[string] | _N/A_ | Audience |
| exp | number | _N/A_ | Expires |
| auth_time | number | _N/A_ | The time the user authenticated with Authelia |
| rat | number | _N/A_ | The time when the token was requested |
| iat | number | _N/A_ | The time when the token was issued |
| jti | string(uuid) | _N/A_ | JWT Identifier |
| Claim | JWT Type | Authelia Attribute | Description |
|:---------:|:-------------:|:------------------:|:-----------------------------------------------------------:|
| sub | string | username | A unique value linked to the user who logged in |
| scope | string | scopes | Granted scopes (space delimited) |
| scp | array[string] | scopes | Granted scopes |
| iss | string | hostname | The issuer name, determined by URL |
| at_hash | string | _N/A_ | Access Token Hash |
| aud | array[string] | _N/A_ | Audience |
| exp | number | _N/A_ | Expires |
| auth_time | number | _N/A_ | The time the user authenticated with Authelia |
| rat | number | _N/A_ | The time when the token was requested |
| iat | number | _N/A_ | The time when the token was issued |
| jti | string(uuid) | _N/A_ | JWT Identifier |
| amr | array[string] | _N/A_ | An [RFC8176] list of authentication method reference values |
### 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 |
| 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
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/
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176

View File

@ -9,7 +9,9 @@ import (
"github.com/ory/fosite"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/session"
)
@ -97,7 +99,7 @@ func oidcAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *
subject := userSession.Username
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",
requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims)
@ -126,14 +128,14 @@ func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
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(), "', '"))
userSession.OIDCWorkflowSession = &session.OIDCWorkflowSession{
ClientID: client.GetID(),
RequestedScopes: requester.GetRequestedScopes(),
RequestedAudience: requester.GetRequestedAudience(),
AuthURI: redirectURL,
TargetURI: requester.GetRedirectURI().String(),
RequiredAuthorizationLevel: client.Policy,
CreatedTimestamp: time.Now().Unix(),
userSession.OIDCWorkflowSession = &model.OIDCWorkflowSession{
ClientID: client.GetID(),
RequestedScopes: requester.GetRequestedScopes(),
RequestedAudience: requester.GetRequestedAudience(),
AuthURI: redirectURL,
TargetURI: requester.GetRedirectURI().String(),
Require2FA: client.Policy == authorization.TwoFactor,
CreatedTimestamp: time.Now().Unix(),
}
if err := ctx.SaveSession(userSession); err != nil {

View File

@ -28,7 +28,7 @@ func oidcConsent(ctx *middlewares.AutheliaCtx) {
}
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()
return
@ -59,7 +59,7 @@ func oidcConsentPOST(ctx *middlewares.AutheliaCtx) {
}
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()
return

View File

@ -255,7 +255,7 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
return
}
userSession.SetTwoFactor(ctx.Clock.Now())
userSession.SetTwoFactorDuo(ctx.Clock.Now())
err = ctx.SaveSession(userSession)
if err != nil {

View File

@ -68,7 +68,7 @@ func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
return
}
userSession.SetTwoFactor(ctx.Clock.Now())
userSession.SetTwoFactorTOTP(ctx.Clock.Now())
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "authentication time", regulation.AuthTypeTOTP, userSession.Username, err)

View File

@ -185,8 +185,9 @@ func SecondFactorWebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return
}
userSession.SetTwoFactor(ctx.Clock.Now())
userSession.Webauthn = nil
userSession.SetTwoFactorWebauthn(ctx.Clock.Now(),
assertionResponse.Response.AuthenticatorData.Flags.UserPresent(),
assertionResponse.Response.AuthenticatorData.Flags.UserVerified())
if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err)

View File

@ -3,6 +3,7 @@ package handlers
import (
"github.com/ory/fosite"
"github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/utils"
@ -10,7 +11,7 @@ import (
// 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.
func isConsentMissing(workflow *session.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
func isConsentMissing(workflow *model.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
if workflow == nil {
return true
}

View File

@ -6,19 +6,20 @@ import (
"github.com/stretchr/testify/assert"
"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/session"
)
func TestShouldDetectIfConsentIsMissing(t *testing.T) {
var workflow *session.OIDCWorkflowSession
var workflow *model.OIDCWorkflowSession
requestedScopes := []string{"openid", "profile"}
requestedAudience := []string{"https://authelia.com"}
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
workflow = &session.OIDCWorkflowSession{
workflow = &model.OIDCWorkflowSession{
GrantedScopes: []string{"openid", "profile"},
GrantedAudience: []string{"https://authelia.com"},
}

View File

@ -7,6 +7,7 @@ import (
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils"
@ -16,7 +17,7 @@ import (
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
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.ReplyOK()

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
})
}
})
}
}

View File

@ -6,7 +6,7 @@ import (
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"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.
@ -46,8 +46,8 @@ func (c InternalClient) GetID() string {
return c.ID
}
// GetConsentResponseBody returns the proper consent response body for this session.OIDCWorkflowSession.
func (c InternalClient) GetConsentResponseBody(session *session.OIDCWorkflowSession) ConsentGetResponseBody {
// GetConsentResponseBody returns the proper consent response body for this model.OIDCWorkflowSession.
func (c InternalClient) GetConsentResponseBody(session *model.OIDCWorkflowSession) ConsentGetResponseBody {
body := ConsentGetResponseBody{
ClientID: c.ID,
ClientDescription: c.Description,

View File

@ -10,7 +10,7 @@ import (
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"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) {
@ -79,7 +79,7 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
c.ID = "myclient"
c.Description = "My Client"
workflow := &session.OIDCWorkflowSession{
workflow := &model.OIDCWorkflowSession{
RequestedAudience: []string{"https://example.com"},
RequestedScopes: []string{"openid", "groups"},
}

View File

@ -31,3 +31,97 @@ const (
RevocationPath = "/api/oidc/revocation"
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"
)

View File

@ -25,6 +25,7 @@ func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (st
AccessTokens: map[string]fosite.Requester{},
RefreshTokens: map[string]storage.StoreRefreshToken{},
PKCES: map[string]fosite.Requester{},
BlacklistedJTIs: map[string]time.Time{},
AccessTokenRequestIDs: map[string]string{},
RefreshTokenRequestIDs: map[string]string{},
},

View File

@ -30,7 +30,7 @@ func NewSession() (session *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) {
if extra == nil {
extra = make(map[string]interface{})
@ -47,6 +47,8 @@ func NewSessionWithAuthorizeRequest(issuer, kid, subject, username string, extra
Nonce: requester.GetRequestForm().Get("nonce"),
Audience: requester.GetGrantedAudience(),
Extra: extra,
AuthenticationMethodsReferences: amr,
},
Headers: &jwt.Headers{
Extra: map[string]interface{}{

View File

@ -49,8 +49,9 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
requested := time.Unix(1647332518, 0)
authAt := time.Unix(1647332500, 0)
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.Extra)
@ -58,24 +59,29 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
require.NotNil(t, session.Headers.Extra)
require.NotNil(t, session.Claims)
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, issuer, session.Claims.Issuer)
assert.Equal(t, "primary", session.Headers.Get("kid"))
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.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"])
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.Claims)
assert.NotNil(t, session.Claims.Extra)
assert.Nil(t, session.Claims.AuthenticationMethodsReferences)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc"
)
func TestShouldInitializerSession(t *testing.T) {
@ -93,9 +94,10 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
AuthenticationLevel: authentication.OneFactor,
LastActivity: timeOneFactor.Unix(),
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true},
}, session)
session.SetTwoFactor(timeTwoFactor)
session.SetTwoFactorDuo(timeTwoFactor)
err = provider.SaveSession(ctx, session)
require.NoError(t, err)
@ -109,6 +111,7 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
LastActivity: timeTwoFactor.Unix(),
FirstFactorAuthnTimestamp: timeOneFactor.Unix(),
SecondFactorAuthnTimestamp: timeTwoFactor.Unix(),
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Duo: true},
}, session)
authAt, err = session.AuthenticatedTime(authorization.OneFactor)
@ -124,6 +127,169 @@ func TestShouldSetSessionAuthenticationLevels(t *testing.T) {
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) {
ctx := &fasthttp.RequestCtx{}
configuration := schema.SessionConfiguration{}

View File

@ -10,8 +10,9 @@ import (
"github.com/sirupsen/logrus"
"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/model"
"github.com/authelia/authelia/v4/internal/oidc"
)
// ProviderConfig is the configuration used to create the session provider.
@ -37,11 +38,13 @@ type UserSession struct {
FirstFactorAuthnTimestamp int64
SecondFactorAuthnTimestamp int64
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
// Webauthn holds the session registration data for this session.
Webauthn *webauthn.SessionData
// 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
// while doing the query actually updating the password.
@ -56,19 +59,6 @@ type Identity struct {
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 {
return &redisLogger{logger: logging.Logger()}
}

View File

@ -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) {
s.FirstFactorAuthnTimestamp = 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.Groups = details.Groups
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.LastActivity = now.Unix()
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.
func (s UserSession) AuthenticatedTime(level authorization.Level) (authenticatedTime time.Time, err error) {
switch level {

View File

@ -2,7 +2,7 @@
version: '3'
services:
oidc-client:
image: ghcr.io/authelia/oidc-tester-app:master-2a82ab3
image: ghcr.io/authelia/oidc-tester-app:master-89622a8
command: /entrypoint.sh
depends_on:
- authelia-backend

View File

@ -2,6 +2,6 @@
while true;
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
done