mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:37:31 +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:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
@@ -16,90 +17,80 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
deviceAuthTable = table{
|
||||
name: projection.DeviceAuthProjectionTable,
|
||||
instanceIDCol: projection.DeviceAuthColumnInstanceID,
|
||||
deviceAuthRequestTable = table{
|
||||
name: projection.DeviceAuthRequestProjectionTable,
|
||||
instanceIDCol: projection.DeviceAuthRequestColumnInstanceID,
|
||||
}
|
||||
DeviceAuthColumnID = Column{
|
||||
name: projection.DeviceAuthColumnID,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnClientID = Column{
|
||||
name: projection.DeviceAuthRequestColumnClientID,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnClientID = Column{
|
||||
name: projection.DeviceAuthColumnClientID,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnDeviceCode = Column{
|
||||
name: projection.DeviceAuthRequestColumnDeviceCode,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnDeviceCode = Column{
|
||||
name: projection.DeviceAuthColumnDeviceCode,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnUserCode = Column{
|
||||
name: projection.DeviceAuthRequestColumnUserCode,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnUserCode = Column{
|
||||
name: projection.DeviceAuthColumnUserCode,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnScopes = Column{
|
||||
name: projection.DeviceAuthRequestColumnScopes,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnExpires = Column{
|
||||
name: projection.DeviceAuthColumnExpires,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnCreationDate = Column{
|
||||
name: projection.DeviceAuthRequestColumnCreationDate,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnScopes = Column{
|
||||
name: projection.DeviceAuthColumnScopes,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnChangeDate = Column{
|
||||
name: projection.DeviceAuthRequestColumnChangeDate,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnState = Column{
|
||||
name: projection.DeviceAuthColumnState,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnSequence = Column{
|
||||
name: projection.DeviceAuthRequestColumnSequence,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
DeviceAuthColumnSubject = Column{
|
||||
name: projection.DeviceAuthColumnSubject,
|
||||
table: deviceAuthTable,
|
||||
}
|
||||
DeviceAuthColumnCreationDate = Column{
|
||||
name: projection.DeviceAuthColumnCreationDate,
|
||||
table: deviceAuthTable,
|
||||
}
|
||||
DeviceAuthColumnChangeDate = Column{
|
||||
name: projection.DeviceAuthColumnChangeDate,
|
||||
table: deviceAuthTable,
|
||||
}
|
||||
DeviceAuthColumnSequence = Column{
|
||||
name: projection.DeviceAuthColumnSequence,
|
||||
table: deviceAuthTable,
|
||||
}
|
||||
DeviceAuthColumnInstanceID = Column{
|
||||
name: projection.DeviceAuthColumnInstanceID,
|
||||
table: deviceAuthTable,
|
||||
DeviceAuthRequestColumnInstanceID = Column{
|
||||
name: projection.DeviceAuthRequestColumnInstanceID,
|
||||
table: deviceAuthRequestTable,
|
||||
}
|
||||
)
|
||||
|
||||
func (q *Queries) DeviceAuthByDeviceCode(ctx context.Context, clientID, deviceCode string) (deviceAuth *domain.DeviceAuth, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
stmt, scan := prepareDeviceAuthQuery(ctx, q.client)
|
||||
eq := sq.Eq{
|
||||
DeviceAuthColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
DeviceAuthColumnClientID.identifier(): clientID,
|
||||
DeviceAuthColumnDeviceCode.identifier(): deviceCode,
|
||||
}
|
||||
query, args, err := stmt.Where(eq).ToSql()
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-uk1Oh", "Errors.Query.SQLStatement")
|
||||
}
|
||||
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
deviceAuth, err = scan(row)
|
||||
return err
|
||||
}, query, args...)
|
||||
return deviceAuth, err
|
||||
type DeviceAuth struct {
|
||||
ClientID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Expires time.Time
|
||||
Scopes []string
|
||||
State domain.DeviceAuthState
|
||||
Subject string
|
||||
UserAuthMethods []domain.UserAuthMethodType
|
||||
AuthTime time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) DeviceAuthByUserCode(ctx context.Context, userCode string) (deviceAuth *domain.DeviceAuth, err error) {
|
||||
// DeviceAuthByDeviceCode gets the current state of a Device Authorization directly from the eventstore.
|
||||
func (q *Queries) DeviceAuthByDeviceCode(ctx context.Context, deviceCode string) (deviceAuth *DeviceAuth, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
model := NewDeviceAuthReadModel(deviceCode, authz.GetInstance(ctx).InstanceID())
|
||||
if err := q.eventstore.FilterToQueryReducer(ctx, model); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !model.State.Exists() {
|
||||
return nil, zerrors.ThrowNotFound(nil, "QUERY-eeR0e", "Errors.DeviceAuth.NotExisting")
|
||||
}
|
||||
return &model.DeviceAuth, nil
|
||||
}
|
||||
|
||||
// DeviceAuthRequestByUserCode finds a Device Authorization request by User-Code from the `device_auth_requests` projection.
|
||||
func (q *Queries) DeviceAuthRequestByUserCode(ctx context.Context, userCode string) (authReq *domain.AuthRequestDevice, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
stmt, scan := prepareDeviceAuthQuery(ctx, q.client)
|
||||
eq := sq.Eq{
|
||||
DeviceAuthColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
DeviceAuthColumnUserCode.identifier(): userCode,
|
||||
DeviceAuthRequestColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
|
||||
DeviceAuthRequestColumnUserCode.identifier(): userCode,
|
||||
}
|
||||
query, args, err := stmt.Where(eq).ToSql()
|
||||
if err != nil {
|
||||
@@ -107,34 +98,32 @@ func (q *Queries) DeviceAuthByUserCode(ctx context.Context, userCode string) (de
|
||||
}
|
||||
|
||||
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
|
||||
deviceAuth, err = scan(row)
|
||||
authReq, err = scan(row)
|
||||
return err
|
||||
}, query, args...)
|
||||
return deviceAuth, err
|
||||
return authReq, err
|
||||
}
|
||||
|
||||
var deviceAuthSelectColumns = []string{
|
||||
DeviceAuthColumnID.identifier(),
|
||||
DeviceAuthColumnClientID.identifier(),
|
||||
DeviceAuthColumnScopes.identifier(),
|
||||
DeviceAuthColumnExpires.identifier(),
|
||||
DeviceAuthColumnState.identifier(),
|
||||
DeviceAuthColumnSubject.identifier(),
|
||||
DeviceAuthRequestColumnClientID.identifier(),
|
||||
DeviceAuthRequestColumnDeviceCode.identifier(),
|
||||
DeviceAuthRequestColumnUserCode.identifier(),
|
||||
DeviceAuthRequestColumnScopes.identifier(),
|
||||
}
|
||||
|
||||
func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.DeviceAuth, error)) {
|
||||
return sq.Select(deviceAuthSelectColumns...).From(deviceAuthTable.identifier()).PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*domain.DeviceAuth, error) {
|
||||
dst := new(domain.DeviceAuth)
|
||||
var scopes database.TextArray[string]
|
||||
func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.AuthRequestDevice, error)) {
|
||||
return sq.Select(deviceAuthSelectColumns...).From(deviceAuthRequestTable.identifier()).PlaceholderFormat(sq.Dollar),
|
||||
func(row *sql.Row) (*domain.AuthRequestDevice, error) {
|
||||
dst := new(domain.AuthRequestDevice)
|
||||
var (
|
||||
scopes database.TextArray[string]
|
||||
)
|
||||
|
||||
err := row.Scan(
|
||||
&dst.AggregateID,
|
||||
&dst.ClientID,
|
||||
&dst.DeviceCode,
|
||||
&dst.UserCode,
|
||||
&scopes,
|
||||
&dst.Expires,
|
||||
&dst.State,
|
||||
&dst.Subject,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, zerrors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotExisting")
|
||||
@@ -142,7 +131,6 @@ func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectB
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal")
|
||||
}
|
||||
|
||||
dst.Scopes = scopes
|
||||
return dst, nil
|
||||
}
|
||||
|
58
internal/query/device_auth_model.go
Normal file
58
internal/query/device_auth_model.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package query
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
)
|
||||
|
||||
type DeviceAuthReadModel struct {
|
||||
eventstore.ReadModel
|
||||
DeviceAuth
|
||||
}
|
||||
|
||||
func NewDeviceAuthReadModel(deviceCode, resourceOwner string) *DeviceAuthReadModel {
|
||||
return &DeviceAuthReadModel{
|
||||
ReadModel: eventstore.ReadModel{
|
||||
AggregateID: deviceCode,
|
||||
ResourceOwner: resourceOwner,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DeviceAuthReadModel) Reduce() error {
|
||||
for _, event := range m.Events {
|
||||
switch e := event.(type) {
|
||||
case *deviceauth.AddedEvent:
|
||||
m.ClientID = e.ClientID
|
||||
m.DeviceCode = e.DeviceCode
|
||||
m.UserCode = e.UserCode
|
||||
m.Expires = e.Expires
|
||||
m.Scopes = e.Scopes
|
||||
m.State = e.State
|
||||
case *deviceauth.ApprovedEvent:
|
||||
m.State = domain.DeviceAuthStateApproved
|
||||
m.Subject = e.Subject
|
||||
m.UserAuthMethods = e.UserAuthMethods
|
||||
m.AuthTime = e.AuthTime
|
||||
case *deviceauth.CanceledEvent:
|
||||
m.State = e.Reason.State()
|
||||
}
|
||||
}
|
||||
|
||||
return m.ReadModel.Reduce()
|
||||
}
|
||||
|
||||
func (m *DeviceAuthReadModel) Query() *eventstore.SearchQueryBuilder {
|
||||
return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent).
|
||||
ResourceOwner(m.ResourceOwner).
|
||||
AddQuery().
|
||||
AggregateTypes(deviceauth.AggregateType).
|
||||
AggregateIDs(m.AggregateID).
|
||||
EventTypes(
|
||||
deviceauth.AddedEventType,
|
||||
deviceauth.ApprovedEventType,
|
||||
deviceauth.CanceledEventType,
|
||||
).
|
||||
Builder()
|
||||
}
|
@@ -6,82 +6,188 @@ import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
const (
|
||||
expectedDeviceAuthQueryC = `SELECT` +
|
||||
` projections.device_authorizations.id,` +
|
||||
` projections.device_authorizations.client_id,` +
|
||||
` projections.device_authorizations.scopes,` +
|
||||
` projections.device_authorizations.expires,` +
|
||||
` projections.device_authorizations.state,` +
|
||||
` projections.device_authorizations.subject` +
|
||||
` FROM projections.device_authorizations`
|
||||
expectedDeviceAuthWhereDeviceCodeQueryC = expectedDeviceAuthQueryC +
|
||||
` WHERE projections.device_authorizations.client_id = $1` +
|
||||
` AND projections.device_authorizations.device_code = $2` +
|
||||
` AND projections.device_authorizations.instance_id = $3`
|
||||
expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC +
|
||||
` WHERE projections.device_authorizations.instance_id = $1` +
|
||||
` AND projections.device_authorizations.user_code = $2`
|
||||
)
|
||||
|
||||
var (
|
||||
expectedDeviceAuthQuery = regexp.QuoteMeta(expectedDeviceAuthQueryC)
|
||||
expectedDeviceAuthWhereDeviceCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereDeviceCodeQueryC)
|
||||
expectedDeviceAuthWhereUserCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereUserCodeQueryC)
|
||||
expectedDeviceAuthValues = []driver.Value{
|
||||
"primary-id",
|
||||
"client-id",
|
||||
database.TextArray[string]{"a", "b", "c"},
|
||||
testNow,
|
||||
domain.DeviceAuthStateApproved,
|
||||
"subject",
|
||||
}
|
||||
expectedDeviceAuth = &domain.DeviceAuth{
|
||||
ObjectRoot: models.ObjectRoot{
|
||||
AggregateID: "primary-id",
|
||||
},
|
||||
ClientID: "client-id",
|
||||
Scopes: []string{"a", "b", "c"},
|
||||
Expires: testNow,
|
||||
State: domain.DeviceAuthStateApproved,
|
||||
Subject: "subject",
|
||||
}
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestQueries_DeviceAuthByDeviceCode(t *testing.T) {
|
||||
client, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build mock client: %v", err)
|
||||
ctx := authz.NewMockContext("inst1", "org1", "user1")
|
||||
timestamp := time.Date(2015, 12, 15, 22, 13, 45, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
eventstore func(t *testing.T) *eventstore.Eventstore
|
||||
want *DeviceAuth
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "filter error",
|
||||
eventstore: expectEventstore(
|
||||
expectFilterError(io.ErrClosedPipe),
|
||||
),
|
||||
wantErr: io.ErrClosedPipe,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(),
|
||||
),
|
||||
wantErr: zerrors.ThrowNotFound(nil, "QUERY-eeR0e", "Errors.DeviceAuth.NotExisting"),
|
||||
},
|
||||
{
|
||||
name: "ok, initiated",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
"client1", "device1", "user-code", timestamp, []string{"foo", "bar"},
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &DeviceAuth{
|
||||
ClientID: "client1",
|
||||
DeviceCode: "device1",
|
||||
UserCode: "user-code",
|
||||
Expires: timestamp,
|
||||
Scopes: []string{"foo", "bar"},
|
||||
State: domain.DeviceAuthStateInitiated,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok, approved",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
"client1", "device1", "user-code", timestamp, []string{"foo", "bar"},
|
||||
)),
|
||||
eventFromEventPusher(deviceauth.NewApprovedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
"user1", []domain.UserAuthMethodType{domain.UserAuthMethodTypePasswordless},
|
||||
timestamp,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &DeviceAuth{
|
||||
ClientID: "client1",
|
||||
DeviceCode: "device1",
|
||||
UserCode: "user-code",
|
||||
Expires: timestamp,
|
||||
Scopes: []string{"foo", "bar"},
|
||||
State: domain.DeviceAuthStateApproved,
|
||||
Subject: "user1",
|
||||
UserAuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePasswordless},
|
||||
AuthTime: timestamp,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok, denied",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
"client1", "device1", "user-code", timestamp, []string{"foo", "bar"},
|
||||
)),
|
||||
eventFromEventPusher(deviceauth.NewCanceledEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
domain.DeviceAuthCanceledDenied,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &DeviceAuth{
|
||||
ClientID: "client1",
|
||||
DeviceCode: "device1",
|
||||
UserCode: "user-code",
|
||||
Expires: timestamp,
|
||||
Scopes: []string{"foo", "bar"},
|
||||
State: domain.DeviceAuthStateDenied,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ok, expired",
|
||||
eventstore: expectEventstore(
|
||||
expectFilter(
|
||||
eventFromEventPusher(deviceauth.NewAddedEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
"client1", "device1", "user-code", timestamp, []string{"foo", "bar"},
|
||||
)),
|
||||
eventFromEventPusher(deviceauth.NewCanceledEvent(
|
||||
ctx,
|
||||
deviceauth.NewAggregate("device1", "instance1"),
|
||||
domain.DeviceAuthCanceledExpired,
|
||||
)),
|
||||
),
|
||||
),
|
||||
want: &DeviceAuth{
|
||||
ClientID: "client1",
|
||||
DeviceCode: "device1",
|
||||
UserCode: "user-code",
|
||||
Expires: timestamp,
|
||||
Scopes: []string{"foo", "bar"},
|
||||
State: domain.DeviceAuthStateExpired,
|
||||
},
|
||||
},
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(expectedDeviceAuthWhereDeviceCodeQuery).WillReturnRows(
|
||||
sqlmock.NewRows(deviceAuthSelectColumns).AddRow(expectedDeviceAuthValues...),
|
||||
)
|
||||
mock.ExpectCommit()
|
||||
q := Queries{
|
||||
client: &database.DB{DB: client},
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
q := &Queries{
|
||||
eventstore: tt.eventstore(t),
|
||||
}
|
||||
got, err := q.DeviceAuthByDeviceCode(ctx, "device1")
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
got, err := q.DeviceAuthByDeviceCode(context.TODO(), "123", "456")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedDeviceAuth, got)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestQueries_DeviceAuthByUserCode(t *testing.T) {
|
||||
const (
|
||||
expectedDeviceAuthQueryC = `SELECT` +
|
||||
` projections.device_auth_requests.client_id,` +
|
||||
` projections.device_auth_requests.device_code,` +
|
||||
` projections.device_auth_requests.user_code,` +
|
||||
` projections.device_auth_requests.scopes` +
|
||||
` FROM projections.device_auth_requests`
|
||||
expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC +
|
||||
` WHERE projections.device_auth_requests.instance_id = $1` +
|
||||
` AND projections.device_auth_requests.user_code = $2`
|
||||
)
|
||||
|
||||
var (
|
||||
expectedDeviceAuthQuery = regexp.QuoteMeta(expectedDeviceAuthQueryC)
|
||||
expectedDeviceAuthWhereUserCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereUserCodeQueryC)
|
||||
expectedDeviceAuthValues = []driver.Value{
|
||||
"client-id",
|
||||
"device1",
|
||||
"user-code",
|
||||
database.TextArray[string]{"a", "b", "c"},
|
||||
}
|
||||
expectedDeviceAuth = &domain.AuthRequestDevice{
|
||||
ClientID: "client-id",
|
||||
DeviceCode: "device1",
|
||||
UserCode: "user-code",
|
||||
Scopes: []string{"a", "b", "c"},
|
||||
}
|
||||
)
|
||||
|
||||
func TestQueries_DeviceAuthRequestByUserCode(t *testing.T) {
|
||||
client, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build mock client: %v", err)
|
||||
@@ -96,7 +202,7 @@ func TestQueries_DeviceAuthByUserCode(t *testing.T) {
|
||||
q := Queries{
|
||||
client: &database.DB{DB: client},
|
||||
}
|
||||
got, err := q.DeviceAuthByUserCode(context.TODO(), "789")
|
||||
got, err := q.DeviceAuthRequestByUserCode(context.TODO(), "789")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedDeviceAuth, got)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
@@ -110,7 +216,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want want
|
||||
object any
|
||||
object *domain.AuthRequestDevice
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
@@ -137,7 +243,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) {
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*domain.DeviceAuth)(nil),
|
||||
object: nil,
|
||||
},
|
||||
{
|
||||
name: "other error",
|
||||
@@ -153,7 +259,7 @@ func Test_prepareDeviceAuthQuery(t *testing.T) {
|
||||
return nil, true
|
||||
},
|
||||
},
|
||||
object: (*domain.DeviceAuth)(nil),
|
||||
object: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
@@ -3,7 +3,6 @@ package projection
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_handler "github.com/zitadel/zitadel/internal/eventstore/handler"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
|
||||
@@ -12,57 +11,51 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DeviceAuthProjectionTable = "projections.device_authorizations"
|
||||
DeviceAuthRequestProjectionTable = "projections.device_auth_requests"
|
||||
|
||||
DeviceAuthColumnID = "id"
|
||||
DeviceAuthColumnClientID = "client_id"
|
||||
DeviceAuthColumnDeviceCode = "device_code"
|
||||
DeviceAuthColumnUserCode = "user_code"
|
||||
DeviceAuthColumnExpires = "expires"
|
||||
DeviceAuthColumnScopes = "scopes"
|
||||
DeviceAuthColumnState = "state"
|
||||
DeviceAuthColumnSubject = "subject"
|
||||
|
||||
DeviceAuthColumnCreationDate = "creation_date"
|
||||
DeviceAuthColumnChangeDate = "change_date"
|
||||
DeviceAuthColumnSequence = "sequence"
|
||||
DeviceAuthColumnInstanceID = "instance_id"
|
||||
DeviceAuthRequestColumnClientID = "client_id"
|
||||
DeviceAuthRequestColumnDeviceCode = "device_code"
|
||||
DeviceAuthRequestColumnUserCode = "user_code"
|
||||
DeviceAuthRequestColumnScopes = "scopes"
|
||||
DeviceAuthRequestColumnCreationDate = "creation_date"
|
||||
DeviceAuthRequestColumnChangeDate = "change_date"
|
||||
DeviceAuthRequestColumnSequence = "sequence"
|
||||
DeviceAuthRequestColumnInstanceID = "instance_id"
|
||||
)
|
||||
|
||||
type deviceAuthProjection struct{}
|
||||
// deviceAuthRequestProjection holds device authorization requests
|
||||
// and makes them search-able by User Code.
|
||||
// In principle the projected data is only needed during user login.
|
||||
// Device Token logic uses the eventstore directly.
|
||||
type deviceAuthRequestProjection struct{}
|
||||
|
||||
func newDeviceAuthProjection(ctx context.Context, config handler.Config) *handler.Handler {
|
||||
return handler.NewHandler(ctx, &config, new(deviceAuthProjection))
|
||||
return handler.NewHandler(ctx, &config, new(deviceAuthRequestProjection))
|
||||
}
|
||||
|
||||
func (*deviceAuthProjection) Name() string {
|
||||
return DeviceAuthProjectionTable
|
||||
func (*deviceAuthRequestProjection) Name() string {
|
||||
return DeviceAuthRequestProjectionTable
|
||||
}
|
||||
|
||||
func (*deviceAuthProjection) Init() *old_handler.Check {
|
||||
func (*deviceAuthRequestProjection) Init() *old_handler.Check {
|
||||
return handler.NewTableCheck(
|
||||
handler.NewTable([]*handler.InitColumn{
|
||||
handler.NewColumn(DeviceAuthColumnID, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthColumnClientID, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthColumnDeviceCode, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthColumnUserCode, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthColumnExpires, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(DeviceAuthColumnScopes, handler.ColumnTypeTextArray),
|
||||
handler.NewColumn(DeviceAuthColumnState, handler.ColumnTypeEnum, handler.Default(domain.DeviceAuthStateInitiated)),
|
||||
handler.NewColumn(DeviceAuthColumnSubject, handler.ColumnTypeText, handler.Default("")),
|
||||
handler.NewColumn(DeviceAuthColumnCreationDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(DeviceAuthColumnChangeDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(DeviceAuthColumnSequence, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(DeviceAuthColumnInstanceID, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthRequestColumnClientID, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthRequestColumnDeviceCode, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthRequestColumnUserCode, handler.ColumnTypeText),
|
||||
handler.NewColumn(DeviceAuthRequestColumnScopes, handler.ColumnTypeTextArray),
|
||||
handler.NewColumn(DeviceAuthRequestColumnCreationDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(DeviceAuthRequestColumnChangeDate, handler.ColumnTypeTimestamp),
|
||||
handler.NewColumn(DeviceAuthRequestColumnSequence, handler.ColumnTypeInt64),
|
||||
handler.NewColumn(DeviceAuthRequestColumnInstanceID, handler.ColumnTypeText),
|
||||
},
|
||||
handler.NewPrimaryKey(DeviceAuthColumnInstanceID, DeviceAuthColumnID),
|
||||
handler.WithIndex(handler.NewIndex("user_code", []string{DeviceAuthColumnInstanceID, DeviceAuthColumnUserCode})),
|
||||
handler.WithIndex(handler.NewIndex("device_code", []string{DeviceAuthColumnInstanceID, DeviceAuthColumnClientID, DeviceAuthColumnDeviceCode})),
|
||||
handler.NewPrimaryKey(DeviceAuthRequestColumnInstanceID, DeviceAuthRequestColumnDeviceCode),
|
||||
handler.WithIndex(handler.NewIndex("user_code", []string{DeviceAuthRequestColumnInstanceID, DeviceAuthRequestColumnUserCode})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (p *deviceAuthProjection) Reducers() []handler.AggregateReducer {
|
||||
func (p *deviceAuthRequestProjection) Reducers() []handler.AggregateReducer {
|
||||
return []handler.AggregateReducer{
|
||||
{
|
||||
Aggregate: deviceauth.AggregateType,
|
||||
@@ -73,22 +66,18 @@ func (p *deviceAuthProjection) Reducers() []handler.AggregateReducer {
|
||||
},
|
||||
{
|
||||
Event: deviceauth.ApprovedEventType,
|
||||
Reduce: p.reduceAppoved,
|
||||
Reduce: p.reduceDoneEvents,
|
||||
},
|
||||
{
|
||||
Event: deviceauth.CanceledEventType,
|
||||
Reduce: p.reduceCanceled,
|
||||
},
|
||||
{
|
||||
Event: deviceauth.RemovedEventType,
|
||||
Reduce: p.reduceRemoved,
|
||||
Reduce: p.reduceDoneEvents,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *deviceAuthProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
func (p *deviceAuthRequestProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*deviceauth.AddedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-chu6O", "reduce.wrong.event.type %T != %s", event, deviceauth.AddedEventType)
|
||||
@@ -96,66 +85,30 @@ func (p *deviceAuthProjection) reduceAdded(event eventstore.Event) (*handler.Sta
|
||||
return handler.NewCreateStatement(
|
||||
e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(DeviceAuthColumnID, e.Aggregate().ID),
|
||||
handler.NewCol(DeviceAuthColumnClientID, e.ClientID),
|
||||
handler.NewCol(DeviceAuthColumnDeviceCode, e.DeviceCode),
|
||||
handler.NewCol(DeviceAuthColumnUserCode, e.UserCode),
|
||||
handler.NewCol(DeviceAuthColumnExpires, e.Expires),
|
||||
handler.NewCol(DeviceAuthColumnScopes, e.Scopes),
|
||||
handler.NewCol(DeviceAuthColumnCreationDate, e.CreationDate()),
|
||||
handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(DeviceAuthColumnSequence, e.Sequence()),
|
||||
handler.NewCol(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCol(DeviceAuthRequestColumnClientID, e.ClientID),
|
||||
handler.NewCol(DeviceAuthRequestColumnDeviceCode, e.DeviceCode),
|
||||
handler.NewCol(DeviceAuthRequestColumnUserCode, e.UserCode),
|
||||
handler.NewCol(DeviceAuthRequestColumnScopes, e.Scopes),
|
||||
handler.NewCol(DeviceAuthRequestColumnCreationDate, e.CreationDate()),
|
||||
handler.NewCol(DeviceAuthRequestColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(DeviceAuthRequestColumnSequence, e.Sequence()),
|
||||
handler.NewCol(DeviceAuthRequestColumnInstanceID, e.Aggregate().InstanceID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *deviceAuthProjection) reduceAppoved(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*deviceauth.ApprovedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-kei0A", "reduce.wrong.event.type %T != %s", event, deviceauth.ApprovedEventType)
|
||||
}
|
||||
return handler.NewUpdateStatement(e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(DeviceAuthColumnState, domain.DeviceAuthStateApproved),
|
||||
handler.NewCol(DeviceAuthColumnSubject, e.Subject),
|
||||
handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(DeviceAuthColumnSequence, e.Sequence()),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
// reduceDoneEvents removes the device auth request from the projection.
|
||||
func (p *deviceAuthRequestProjection) reduceDoneEvents(event eventstore.Event) (*handler.Statement, error) {
|
||||
switch event.(type) {
|
||||
case *deviceauth.ApprovedEvent, *deviceauth.CanceledEvent:
|
||||
return handler.NewDeleteStatement(event,
|
||||
[]handler.Condition{
|
||||
handler.NewCond(DeviceAuthRequestColumnInstanceID, event.Aggregate().InstanceID),
|
||||
handler.NewCond(DeviceAuthRequestColumnDeviceCode, event.Aggregate().ID),
|
||||
},
|
||||
), nil
|
||||
|
||||
func (p *deviceAuthProjection) reduceCanceled(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*deviceauth.CanceledEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-eeS8d", "reduce.wrong.event.type %T != %s", event, deviceauth.CanceledEventType)
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-eeS8d", "reduce.wrong.event.type %T", event)
|
||||
}
|
||||
return handler.NewUpdateStatement(e,
|
||||
[]handler.Column{
|
||||
handler.NewCol(DeviceAuthColumnState, e.Reason.State()),
|
||||
handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()),
|
||||
handler.NewCol(DeviceAuthColumnSequence, e.Sequence()),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
||||
func (p *deviceAuthProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) {
|
||||
e, ok := event.(*deviceauth.RemovedEvent)
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "HANDL-AJi1u", "reduce.wrong.event.type %T != %s", event, deviceauth.RemovedEventType)
|
||||
}
|
||||
return handler.NewDeleteStatement(e,
|
||||
[]handler.Condition{
|
||||
handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID),
|
||||
handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID),
|
||||
},
|
||||
), nil
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
"github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/keypair"
|
||||
@@ -99,6 +100,7 @@ func StartQueries(
|
||||
quota.RegisterEventMappers(repo.eventstore)
|
||||
limits.RegisterEventMappers(repo.eventstore)
|
||||
restrictions.RegisterEventMappers(repo.eventstore)
|
||||
deviceauth.RegisterEventMappers(repo.eventstore)
|
||||
|
||||
repo.checkPermission = permissionCheck(repo)
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/repository/mock"
|
||||
action_repo "github.com/zitadel/zitadel/internal/repository/action"
|
||||
"github.com/zitadel/zitadel/internal/repository/authrequest"
|
||||
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
||||
"github.com/zitadel/zitadel/internal/repository/feature"
|
||||
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
||||
iam_repo "github.com/zitadel/zitadel/internal/repository/instance"
|
||||
@@ -54,6 +55,7 @@ func expectEventstore(expects ...expect) func(*testing.T) *eventstore.Eventstore
|
||||
quota_repo.RegisterEventMappers(es)
|
||||
limits.RegisterEventMappers(es)
|
||||
feature.RegisterEventMappers(es)
|
||||
deviceauth.RegisterEventMappers(es)
|
||||
return es
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user