mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 03:47:33 +00:00
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:
47
internal/api/oidc/amr.go
Normal file
47
internal/api/oidc/amr.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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
|
||||
}
|
@@ -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 ""
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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))
|
||||
}
|
||||
|
Reference in New Issue
Block a user