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:
Tim Möhlmann
2023-12-20 14:21:08 +02:00
committed by GitHub
parent e15f6229cd
commit e22689c125
25 changed files with 629 additions and 621 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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 != ""
}