fix: store auth methods instead of AMR in auth request linking and OIDC Session (#6192)

This PR changes the information stored on the SessionLinkedEvent and (OIDC Session) AddedEvent from OIDC AMR strings to domain.UserAuthMethodTypes, so no information is lost in the process (e.g. authentication with an IDP)
This commit is contained in:
Livio Spring
2023-07-12 14:24:01 +02:00
committed by GitHub
parent a3a1e245ad
commit ee26f99ebf
15 changed files with 156 additions and 174 deletions

47
internal/api/oidc/amr.go Normal file
View File

@@ -0,0 +1,47 @@
package oidc
import "github.com/zitadel/zitadel/internal/domain"
const (
// Password states that the users password has been verified
// Deprecated: use `PWD` instead
Password = "password"
// PWD states that the users password has been verified
PWD = "pwd"
// MFA states that multiple factors have been verified (e.g. pwd and otp or passkey)
MFA = "mfa"
// OTP states that a one time password has been verified (e.g. TOTP)
OTP = "otp"
// UserPresence states that the end users presence has been verified (e.g. passkey and u2f)
UserPresence = "user"
)
// AuthMethodTypesToAMR maps zitadel auth method types to Authentication Method Reference Values
// as defined in [RFC 8176, section 2].
//
// [RFC 8176, section 2]: https://datatracker.ietf.org/doc/html/rfc8176#section-2
func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
amr := make([]string, 0, 4)
var mfa bool
for _, methodType := range methodTypes {
switch methodType {
case domain.UserAuthMethodTypePassword:
amr = append(amr, PWD)
case domain.UserAuthMethodTypePasswordless:
mfa = true
amr = append(amr, UserPresence)
case domain.UserAuthMethodTypeU2F:
amr = append(amr, UserPresence)
case domain.UserAuthMethodTypeOTP:
amr = append(amr, OTP)
case domain.UserAuthMethodTypeIDP:
// no AMR value according to specification
case domain.UserAuthMethodTypeUnspecified:
// ignore
}
}
if mfa || len(amr) >= 2 {
amr = append(amr, MFA)
}
return amr
}

View File

@@ -1,43 +0,0 @@
// Package amr maps zitadel session factors to Authentication Method Reference Values
// as defined in [RFC 8176, section 2].
//
// [RFC 8176, section 2]: https://datatracker.ietf.org/doc/html/rfc8176#section-2
package amr
const (
// Password states that the users password has been verified
// Deprecated: use `PWD` instead
Password = "password"
// PWD states that the users password has been verified
PWD = "pwd"
// MFA states that multiple factors have been verified (e.g. pwd and otp or passkey)
MFA = "mfa"
// OTP states that a one time password has been verified (e.g. TOTP)
OTP = "otp"
// UserPresence states that the end users presence has been verified (e.g. passkey and u2f)
UserPresence = "user"
)
type AuthenticationMethodReference interface {
IsPasswordChecked() bool
IsPasskeyChecked() bool
IsU2FChecked() bool
IsOTPChecked() bool
}
func List(model AuthenticationMethodReference) []string {
amr := make([]string, 0)
if model.IsPasswordChecked() {
amr = append(amr, PWD)
}
if model.IsPasskeyChecked() || model.IsU2FChecked() {
amr = append(amr, UserPresence)
}
if model.IsOTPChecked() {
amr = append(amr, OTP)
}
if model.IsPasskeyChecked() || len(amr) >= 2 {
amr = append(amr, MFA)
}
return amr
}

View File

@@ -1,14 +1,16 @@
package amr
package oidc
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/zitadel/zitadel/internal/domain"
)
func TestAMR(t *testing.T) {
type args struct {
model AuthenticationMethodReference
methodTypes []domain.UserAuthMethodType
}
tests := []struct {
name string
@@ -18,76 +20,50 @@ func TestAMR(t *testing.T) {
{
"no checks, empty",
args{
new(test),
nil,
},
[]string{},
},
{
"pw checked",
args{
&test{pwChecked: true},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword},
},
[]string{PWD},
},
{
"passkey checked",
args{
&test{passkeyChecked: true},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePasswordless},
},
[]string{UserPresence, MFA},
},
{
"u2f checked",
args{
&test{u2fChecked: true},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeU2F},
},
[]string{UserPresence},
},
{
"otp checked",
args{
&test{otpChecked: true},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypeOTP},
},
[]string{OTP},
},
{
"multiple checked",
args{
&test{
pwChecked: true,
u2fChecked: true,
},
[]domain.UserAuthMethodType{domain.UserAuthMethodTypePassword, domain.UserAuthMethodTypeU2F},
},
[]string{PWD, UserPresence, MFA},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := List(tt.args.model)
got := AuthMethodTypesToAMR(tt.args.methodTypes)
assert.Equal(t, tt.want, got)
})
}
}
type test struct {
pwChecked bool
passkeyChecked bool
u2fChecked bool
otpChecked bool
}
func (t test) IsPasswordChecked() bool {
return t.pwChecked
}
func (t test) IsPasskeyChecked() bool {
return t.passkeyChecked
}
func (t test) IsU2FChecked() bool {
return t.u2fChecked
}
func (t test) IsOTPChecked() bool {
return t.otpChecked
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
http_utils "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/oidc/amr"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/user/model"
@@ -34,10 +33,10 @@ func (a *AuthRequest) GetACR() string {
func (a *AuthRequest) GetAMR() []string {
list := make([]string, 0)
if a.PasswordVerified {
list = append(list, amr.Password, amr.PWD)
list = append(list, Password, PWD)
}
if len(a.MFAsVerified) > 0 {
list = append(list, amr.MFA)
list = append(list, MFA)
for _, mfa := range a.MFAsVerified {
if amrMFA := AMRFromMFAType(mfa); amrMFA != "" {
list = append(list, amrMFA)
@@ -263,10 +262,10 @@ func CodeChallengeToOIDC(challenge *domain.OIDCCodeChallenge) *oidc.CodeChalleng
func AMRFromMFAType(mfaType domain.MFAType) string {
switch mfaType {
case domain.MFATypeOTP:
return amr.OTP
return OTP
case domain.MFATypeU2F,
domain.MFATypeU2FUserVerification:
return amr.UserPresence
return UserPresence
default:
return ""
}

View File

@@ -21,7 +21,7 @@ func (a *AuthRequestV2) GetACR() string {
}
func (a *AuthRequestV2) GetAMR() []string {
return a.AMR
return AuthMethodTypesToAMR(a.AuthMethods)
}
func (a *AuthRequestV2) GetAudience() []string {
@@ -78,7 +78,7 @@ type RefreshTokenRequestV2 struct {
}
func (r *RefreshTokenRequestV2) GetAMR() []string {
return r.AuthMethodsReferences
return AuthMethodTypesToAMR(r.AuthMethods)
}
func (r *RefreshTokenRequestV2) GetAudience() []string {

View File

@@ -15,7 +15,7 @@ import (
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/oauth2"
"github.com/zitadel/zitadel/internal/api/oidc/amr"
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/integration"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2alpha"
@@ -270,6 +270,6 @@ func assertTokens(t *testing.T, tokens *oidc.Tokens[*oidc.IDTokenClaims], requir
func assertTokenClaims(t *testing.T, claims *oidc.IDTokenClaims, sessionStart, sessionChange time.Time) {
assert.Equal(t, User.GetUserId(), claims.Subject)
assert.Equal(t, []string{amr.UserPresence, amr.MFA}, claims.AuthenticationMethodsReferences)
assert.Equal(t, []string{oidc_api.UserPresence, oidc_api.MFA}, claims.AuthenticationMethodsReferences)
assert.WithinRange(t, claims.AuthTime.AsTime().UTC(), sessionStart.Add(-1*time.Second), sessionChange.Add(1*time.Second))
}