2023-04-19 08:46:02 +00:00
|
|
|
package command
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2024-05-16 05:07:56 +00:00
|
|
|
"fmt"
|
2023-04-19 08:46:02 +00:00
|
|
|
"time"
|
|
|
|
|
2024-05-16 05:07:56 +00:00
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
2023-04-19 08:46:02 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
|
|
"github.com/zitadel/zitadel/internal/repository/deviceauth"
|
2024-05-16 05:07:56 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
2023-12-08 14:30:55 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
2023-04-19 08:46:02 +00:00
|
|
|
)
|
|
|
|
|
2024-05-16 05:07:56 +00:00
|
|
|
func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes, audience []string, needRefreshToken bool) (*domain.ObjectDetails, error) {
|
2023-12-20 12:21:08 +00:00
|
|
|
aggr := deviceauth.NewAggregate(deviceCode, authz.GetInstance(ctx).InstanceID())
|
|
|
|
model := NewDeviceAuthWriteModel(deviceCode, aggr.ResourceOwner)
|
2023-04-19 08:46:02 +00:00
|
|
|
|
|
|
|
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewAddedEvent(
|
|
|
|
ctx,
|
|
|
|
aggr,
|
|
|
|
clientID,
|
|
|
|
deviceCode,
|
|
|
|
userCode,
|
|
|
|
expires,
|
|
|
|
scopes,
|
2024-04-03 06:06:21 +00:00
|
|
|
audience,
|
2024-05-16 05:07:56 +00:00
|
|
|
needRefreshToken,
|
2023-04-19 08:46:02 +00:00
|
|
|
))
|
|
|
|
if err != nil {
|
2023-12-20 12:21:08 +00:00
|
|
|
return nil, err
|
2023-04-19 08:46:02 +00:00
|
|
|
}
|
|
|
|
err = AppendAndReduce(model, pushedEvents...)
|
|
|
|
if err != nil {
|
2023-12-20 12:21:08 +00:00
|
|
|
return nil, err
|
2023-04-19 08:46:02 +00:00
|
|
|
}
|
|
|
|
|
2023-12-20 12:21:08 +00:00
|
|
|
return writeModelToObjectDetails(&model.WriteModel), nil
|
2023-04-19 08:46:02 +00:00
|
|
|
}
|
|
|
|
|
2024-05-16 05:07:56 +00:00
|
|
|
func (c *Commands) ApproveDeviceAuth(
|
|
|
|
ctx context.Context,
|
|
|
|
deviceCode,
|
|
|
|
userID,
|
|
|
|
userOrgID string,
|
|
|
|
authMethods []domain.UserAuthMethodType,
|
|
|
|
authTime time.Time,
|
|
|
|
preferredLanguage *language.Tag,
|
|
|
|
userAgent *domain.UserAgent,
|
|
|
|
) (*domain.ObjectDetails, error) {
|
2023-12-20 12:21:08 +00:00
|
|
|
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
|
2023-04-19 08:46:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !model.State.Exists() {
|
2023-12-08 14:30:55 +00:00
|
|
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
|
2023-04-19 08:46:02 +00:00
|
|
|
}
|
2024-05-16 05:07:56 +00:00
|
|
|
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent))
|
2023-04-19 08:46:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = AppendAndReduce(model, pushedEvents...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return writeModelToObjectDetails(&model.WriteModel), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) {
|
2023-12-20 12:21:08 +00:00
|
|
|
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id)
|
2023-04-19 08:46:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if !model.State.Exists() {
|
2023-12-08 14:30:55 +00:00
|
|
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound")
|
2023-04-19 08:46:02 +00:00
|
|
|
}
|
2024-05-16 05:07:56 +00:00
|
|
|
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewCanceledEvent(ctx, model.aggregate, reason))
|
2023-04-19 08:46:02 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = AppendAndReduce(model, pushedEvents...)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return writeModelToObjectDetails(&model.WriteModel), nil
|
|
|
|
}
|
|
|
|
|
2023-12-20 12:21:08 +00:00
|
|
|
func (c *Commands) getDeviceAuthWriteModelByDeviceCode(ctx context.Context, deviceCode string) (*DeviceAuthWriteModel, error) {
|
2024-05-16 05:07:56 +00:00
|
|
|
model := &DeviceAuthWriteModel{
|
|
|
|
WriteModel: eventstore.WriteModel{AggregateID: deviceCode},
|
|
|
|
}
|
2023-04-19 08:46:02 +00:00
|
|
|
err := c.eventstore.FilterToQueryReducer(ctx, model)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-05-16 05:07:56 +00:00
|
|
|
model.aggregate = deviceauth.NewAggregate(model.AggregateID, model.InstanceID)
|
2023-04-19 08:46:02 +00:00
|
|
|
return model, nil
|
|
|
|
}
|
2024-05-16 05:07:56 +00:00
|
|
|
|
|
|
|
type DeviceAuthStateError domain.DeviceAuthState
|
|
|
|
|
|
|
|
func (e DeviceAuthStateError) Error() string {
|
|
|
|
return fmt.Sprintf("device auth state not approved: %s", domain.DeviceAuthState(e).String())
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateOIDCSessionFromDeviceAuth creates a new OIDC session if the device authorization
|
|
|
|
// flow is completed (user logged in).
|
|
|
|
// A [DeviceAuthStateError] is returned if the device authorization was not approved,
|
|
|
|
// containing a [domain.DeviceAuthState] which can be used to inform the client about the state.
|
|
|
|
//
|
|
|
|
// As devices can poll at various intervals, an explicit state takes precedence over expiry.
|
|
|
|
// This is to prevent cases where users might approve or deny the authorization on time, but the next poll
|
|
|
|
// happens after expiry.
|
|
|
|
func (c *Commands) CreateOIDCSessionFromDeviceAuth(ctx context.Context, deviceCode string) (_ *OIDCSession, err error) {
|
|
|
|
ctx, span := tracing.NewSpan(ctx)
|
|
|
|
defer func() { span.EndWithError(err) }()
|
|
|
|
|
|
|
|
deviceAuthModel, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch deviceAuthModel.State {
|
|
|
|
case domain.DeviceAuthStateApproved:
|
|
|
|
break
|
|
|
|
case domain.DeviceAuthStateUndefined:
|
|
|
|
return nil, zerrors.ThrowNotFound(nil, "COMMAND-ua1Vo", "Errors.DeviceAuth.NotFound")
|
|
|
|
|
|
|
|
case domain.DeviceAuthStateInitiated:
|
|
|
|
if deviceAuthModel.Expires.Before(time.Now()) {
|
|
|
|
c.asyncPush(ctx, deviceauth.NewCanceledEvent(ctx, deviceAuthModel.aggregate, domain.DeviceAuthCanceledExpired))
|
|
|
|
return nil, DeviceAuthStateError(domain.DeviceAuthStateExpired)
|
|
|
|
}
|
|
|
|
fallthrough
|
|
|
|
case domain.DeviceAuthStateDenied, domain.DeviceAuthStateExpired, domain.DeviceAuthStateDone:
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
return nil, DeviceAuthStateError(deviceAuthModel.State)
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd, err := c.newOIDCSessionAddEvents(ctx, deviceAuthModel.UserOrgID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd.AddSession(ctx,
|
|
|
|
deviceAuthModel.UserID,
|
|
|
|
deviceAuthModel.UserOrgID,
|
|
|
|
"",
|
|
|
|
deviceAuthModel.ClientID,
|
|
|
|
deviceAuthModel.Audience,
|
|
|
|
deviceAuthModel.Scopes,
|
|
|
|
deviceAuthModel.UserAuthMethods,
|
|
|
|
deviceAuthModel.AuthTime,
|
|
|
|
"",
|
|
|
|
deviceAuthModel.PreferredLanguage,
|
|
|
|
deviceAuthModel.UserAgent,
|
|
|
|
)
|
|
|
|
if err = cmd.AddAccessToken(ctx, deviceAuthModel.Scopes, deviceAuthModel.UserID, deviceAuthModel.UserOrgID, domain.TokenReasonAuthRequest, nil); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if deviceAuthModel.NeedRefreshToken {
|
|
|
|
if err = cmd.AddRefreshToken(ctx, deviceAuthModel.UserID); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
cmd.DeviceAuthRequestDone(ctx, deviceAuthModel.aggregate)
|
|
|
|
return cmd.PushEvents(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cmd *OIDCSessionEvents) DeviceAuthRequestDone(ctx context.Context, deviceAuthAggregate *eventstore.Aggregate) {
|
|
|
|
cmd.events = append(cmd.events, deviceauth.NewDoneEvent(ctx, deviceAuthAggregate))
|
|
|
|
}
|