mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
feat(oidc): id token for device authorization (#7088)
* cleanup todo * pass id token details to oidc * feat(oidc): id token for device authorization This changes updates to the newest oidc version, so the Device Authorization grant can return ID tokens when the scope `openid` is set. There is also some refactoring done, so that the eventstore can be queried directly when polling for state. The projection is cleaned up to a minimum with only data required for the login UI. * try to be explicit wit hthe timezone to fix github * pin oidc v3.8.0 * remove TBD entry
This commit is contained in:
@@ -110,6 +110,23 @@ const (
|
||||
MFATypeOTPEmail
|
||||
)
|
||||
|
||||
func (m MFAType) UserAuthMethodType() UserAuthMethodType {
|
||||
switch m {
|
||||
case MFATypeTOTP:
|
||||
return UserAuthMethodTypeTOTP
|
||||
case MFATypeU2F:
|
||||
return UserAuthMethodTypeU2F
|
||||
case MFATypeU2FUserVerification:
|
||||
return UserAuthMethodTypePasswordless
|
||||
case MFATypeOTPSMS:
|
||||
return UserAuthMethodTypeOTPSMS
|
||||
case MFATypeOTPEmail:
|
||||
return UserAuthMethodTypeOTPEmail
|
||||
default:
|
||||
return UserAuthMethodTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
type MFALevel int
|
||||
|
||||
const (
|
||||
@@ -223,3 +240,14 @@ func (a *AuthRequest) PrivateLabelingOrgID(defaultID string) string {
|
||||
}
|
||||
return defaultID
|
||||
}
|
||||
|
||||
func (a *AuthRequest) UserAuthMethodTypes() []UserAuthMethodType {
|
||||
list := make([]UserAuthMethodType, 0, len(a.MFAsVerified)+1)
|
||||
if a.PasswordVerified {
|
||||
list = append(list, UserAuthMethodTypePassword)
|
||||
}
|
||||
for _, mfa := range a.MFAsVerified {
|
||||
list = append(list, mfa.UserAuthMethodType())
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
108
internal/domain/auth_request_test.go
Normal file
108
internal/domain/auth_request_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMFAType_UserAuthMethodType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
m MFAType
|
||||
want UserAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "totp",
|
||||
m: MFATypeTOTP,
|
||||
want: UserAuthMethodTypeTOTP,
|
||||
},
|
||||
{
|
||||
name: "u2f",
|
||||
m: MFATypeU2F,
|
||||
want: UserAuthMethodTypeU2F,
|
||||
},
|
||||
{
|
||||
name: "passwordless",
|
||||
m: MFATypeU2FUserVerification,
|
||||
want: UserAuthMethodTypePasswordless,
|
||||
},
|
||||
{
|
||||
name: "otp sms",
|
||||
m: MFATypeOTPSMS,
|
||||
want: UserAuthMethodTypeOTPSMS,
|
||||
},
|
||||
{
|
||||
name: "otp email",
|
||||
m: MFATypeOTPEmail,
|
||||
want: UserAuthMethodTypeOTPEmail,
|
||||
},
|
||||
{
|
||||
name: "unspecified",
|
||||
m: 99,
|
||||
want: UserAuthMethodTypeUnspecified,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.m.UserAuthMethodType()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthRequest_UserAuthMethodTypes(t *testing.T) {
|
||||
type fields struct {
|
||||
PasswordVerified bool
|
||||
MFAsVerified []MFAType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []UserAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "no auth methods",
|
||||
fields: fields{
|
||||
PasswordVerified: false,
|
||||
MFAsVerified: nil,
|
||||
},
|
||||
want: []UserAuthMethodType{},
|
||||
},
|
||||
{
|
||||
name: "only password",
|
||||
fields: fields{
|
||||
PasswordVerified: true,
|
||||
MFAsVerified: nil,
|
||||
},
|
||||
want: []UserAuthMethodType{
|
||||
UserAuthMethodTypePassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "password, with mfa",
|
||||
fields: fields{
|
||||
PasswordVerified: true,
|
||||
MFAsVerified: []MFAType{
|
||||
MFATypeTOTP,
|
||||
MFATypeU2F,
|
||||
},
|
||||
},
|
||||
want: []UserAuthMethodType{
|
||||
UserAuthMethodTypePassword,
|
||||
UserAuthMethodTypeTOTP,
|
||||
UserAuthMethodTypeU2F,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &AuthRequest{
|
||||
PasswordVerified: tt.fields.PasswordVerified,
|
||||
MFAsVerified: tt.fields.MFAsVerified,
|
||||
}
|
||||
got := a.UserAuthMethodTypes()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
@@ -2,28 +2,11 @@ package domain
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
// DeviceAuth describes a Device Authorization request.
|
||||
// It is used as input and output model in the command and query packages.
|
||||
type DeviceAuth struct {
|
||||
models.ObjectRoot
|
||||
|
||||
ClientID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Expires time.Time
|
||||
Scopes []string
|
||||
Subject string
|
||||
State DeviceAuthState
|
||||
}
|
||||
|
||||
// DeviceAuthState describes the step the
|
||||
// the device authorization process is in.
|
||||
// We generate the Stringer implemntation for pretier
|
||||
// We generate the Stringer implementation for prettier
|
||||
// log output.
|
||||
//
|
||||
//go:generate stringer -type=DeviceAuthState -linecomment
|
||||
@@ -35,13 +18,14 @@ const (
|
||||
DeviceAuthStateApproved // approved
|
||||
DeviceAuthStateDenied // denied
|
||||
DeviceAuthStateExpired // expired
|
||||
DeviceAuthStateRemoved // removed
|
||||
|
||||
deviceAuthStateCount // invalid
|
||||
)
|
||||
|
||||
// Exists returns true when not Undefined and
|
||||
// any status lower than Removed.
|
||||
// any status lower than deviceAuthStateCount.
|
||||
func (s DeviceAuthState) Exists() bool {
|
||||
return s > DeviceAuthStateUndefined && s < DeviceAuthStateRemoved
|
||||
return s > DeviceAuthStateUndefined && s < deviceAuthStateCount
|
||||
}
|
||||
|
||||
// Done returns true when DeviceAuthState is Approved.
|
||||
|
@@ -30,7 +30,7 @@ func TestDeviceAuthState_Exists(t *testing.T) {
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
s: deviceAuthStateCount,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
@@ -68,10 +68,6 @@ func TestDeviceAuthState_Done(t *testing.T) {
|
||||
s: DeviceAuthStateExpired,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
@@ -108,10 +104,6 @@ func TestDeviceAuthState_Denied(t *testing.T) {
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@@ -13,10 +13,10 @@ func _() {
|
||||
_ = x[DeviceAuthStateApproved-2]
|
||||
_ = x[DeviceAuthStateDenied-3]
|
||||
_ = x[DeviceAuthStateExpired-4]
|
||||
_ = x[DeviceAuthStateRemoved-5]
|
||||
_ = x[deviceAuthStateCount-5]
|
||||
}
|
||||
|
||||
const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredremoved"
|
||||
const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredinvalid"
|
||||
|
||||
var _DeviceAuthState_index = [...]uint8{0, 9, 18, 26, 32, 39, 46}
|
||||
|
||||
|
@@ -59,7 +59,7 @@ func (a *AuthRequestSAML) IsValid() bool {
|
||||
}
|
||||
|
||||
type AuthRequestDevice struct {
|
||||
ID string
|
||||
ClientID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scopes []string
|
||||
@@ -70,5 +70,5 @@ func (*AuthRequestDevice) Type() AuthRequestType {
|
||||
}
|
||||
|
||||
func (a *AuthRequestDevice) IsValid() bool {
|
||||
return a.DeviceCode != "" && a.UserCode != "" && len(a.Scopes) > 0
|
||||
return a.DeviceCode != "" && a.UserCode != ""
|
||||
}
|
||||
|
Reference in New Issue
Block a user