mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:47:32 +00:00
feat: device authorization RFC 8628 (#5646)
* device auth: implement the write events * add grant type device code * fix(init): check if default value implements stringer --------- Co-authored-by: adlerhurst <silvan.reusser@gmail.com>
This commit is contained in:
@@ -90,6 +90,7 @@ const (
|
||||
OIDCGrantTypeAuthorizationCode OIDCGrantType = iota
|
||||
OIDCGrantTypeImplicit
|
||||
OIDCGrantTypeRefreshToken
|
||||
OIDCGrantTypeDeviceCode
|
||||
)
|
||||
|
||||
type OIDCApplicationType int32
|
||||
|
@@ -122,6 +122,8 @@ func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) {
|
||||
return &AuthRequest{Request: &AuthRequestOIDC{}}, nil
|
||||
case AuthRequestTypeSAML:
|
||||
return &AuthRequest{Request: &AuthRequestSAML{}}, nil
|
||||
case AuthRequestTypeDevice:
|
||||
return &AuthRequest{Request: &AuthRequestDevice{}}, nil
|
||||
}
|
||||
return nil, errors.ThrowInvalidArgument(nil, "DOMAIN-ds2kl", "invalid request type")
|
||||
}
|
||||
@@ -184,3 +186,12 @@ func (a *AuthRequest) GetScopeOrgID() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *AuthRequest) Done() bool {
|
||||
for _, step := range a.PossibleSteps {
|
||||
if step.Type() == NextStepRedirectToCallback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
78
internal/domain/device_auth.go
Normal file
78
internal/domain/device_auth.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"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
|
||||
// log output.
|
||||
//
|
||||
//go:generate stringer -type=DeviceAuthState -linecomment
|
||||
type DeviceAuthState uint
|
||||
|
||||
const (
|
||||
DeviceAuthStateUndefined DeviceAuthState = iota // undefined
|
||||
DeviceAuthStateInitiated // initiated
|
||||
DeviceAuthStateApproved // approved
|
||||
DeviceAuthStateDenied // denied
|
||||
DeviceAuthStateExpired // expired
|
||||
DeviceAuthStateRemoved // removed
|
||||
)
|
||||
|
||||
// Exists returns true when not Undefined and
|
||||
// any status lower than Removed.
|
||||
func (s DeviceAuthState) Exists() bool {
|
||||
return s > DeviceAuthStateUndefined && s < DeviceAuthStateRemoved
|
||||
}
|
||||
|
||||
// Done returns true when DeviceAuthState is Approved.
|
||||
// This implements the OIDC interface requirement of "Done"
|
||||
func (s DeviceAuthState) Done() bool {
|
||||
return s == DeviceAuthStateApproved
|
||||
}
|
||||
|
||||
// Denied returns true when DeviceAuthState is Denied, Expired or Removed.
|
||||
// This implements the OIDC interface requirement of "Denied".
|
||||
func (s DeviceAuthState) Denied() bool {
|
||||
return s >= DeviceAuthStateDenied
|
||||
}
|
||||
|
||||
// DeviceAuthCanceled is a subset of DeviceAuthState, allowed to
|
||||
// be used in the deviceauth.CanceledEvent.
|
||||
// The string type is used to make the eventstore more readable
|
||||
// on the reason of cancelation.
|
||||
type DeviceAuthCanceled string
|
||||
|
||||
const (
|
||||
DeviceAuthCanceledDenied = "denied"
|
||||
DeviceAuthCanceledExpired = "expired"
|
||||
)
|
||||
|
||||
func (c DeviceAuthCanceled) State() DeviceAuthState {
|
||||
switch c {
|
||||
case DeviceAuthCanceledDenied:
|
||||
return DeviceAuthStateDenied
|
||||
case DeviceAuthCanceledExpired:
|
||||
return DeviceAuthStateExpired
|
||||
default:
|
||||
return DeviceAuthStateUndefined
|
||||
}
|
||||
}
|
158
internal/domain/device_auth_test.go
Normal file
158
internal/domain/device_auth_test.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeviceAuthState_Exists(t *testing.T) {
|
||||
tests := []struct {
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
if got := tt.s.Exists(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Exists() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthState_Done(t *testing.T) {
|
||||
tests := []struct {
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
if got := tt.s.Done(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Done() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthState_Denied(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateRemoved,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.s.Denied(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Denied() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthCanceled_State(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c DeviceAuthCanceled
|
||||
want DeviceAuthState
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
want: DeviceAuthStateUndefined,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
c: "foo",
|
||||
want: DeviceAuthStateUndefined,
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
c: DeviceAuthCanceledDenied,
|
||||
want: DeviceAuthStateDenied,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
c: DeviceAuthCanceledExpired,
|
||||
want: DeviceAuthStateExpired,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.c.State(); got != tt.want {
|
||||
t.Errorf("DeviceAuthCanceled.State() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
28
internal/domain/deviceauthstate_string.go
Normal file
28
internal/domain/deviceauthstate_string.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Code generated by "stringer -type=DeviceAuthState -linecomment"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[DeviceAuthStateUndefined-0]
|
||||
_ = x[DeviceAuthStateInitiated-1]
|
||||
_ = x[DeviceAuthStateApproved-2]
|
||||
_ = x[DeviceAuthStateDenied-3]
|
||||
_ = x[DeviceAuthStateExpired-4]
|
||||
_ = x[DeviceAuthStateRemoved-5]
|
||||
}
|
||||
|
||||
const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredremoved"
|
||||
|
||||
var _DeviceAuthState_index = [...]uint8{0, 9, 18, 26, 32, 39, 46}
|
||||
|
||||
func (i DeviceAuthState) String() string {
|
||||
if i >= DeviceAuthState(len(_DeviceAuthState_index)-1) {
|
||||
return "DeviceAuthState(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _DeviceAuthState_name[_DeviceAuthState_index[i]:_DeviceAuthState_index[i+1]]
|
||||
}
|
@@ -22,6 +22,7 @@ type AuthRequestType int32
|
||||
const (
|
||||
AuthRequestTypeOIDC AuthRequestType = iota
|
||||
AuthRequestTypeSAML
|
||||
AuthRequestTypeDevice
|
||||
)
|
||||
|
||||
type AuthRequestOIDC struct {
|
||||
@@ -56,3 +57,18 @@ func (a *AuthRequestSAML) Type() AuthRequestType {
|
||||
func (a *AuthRequestSAML) IsValid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type AuthRequestDevice struct {
|
||||
ID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scopes []string
|
||||
}
|
||||
|
||||
func (*AuthRequestDevice) Type() AuthRequestType {
|
||||
return AuthRequestTypeDevice
|
||||
}
|
||||
|
||||
func (a *AuthRequestDevice) IsValid() bool {
|
||||
return a.DeviceCode != "" && a.UserCode != "" && len(a.Scopes) > 0
|
||||
}
|
||||
|
Reference in New Issue
Block a user