zitadel/internal/command/device_auth.go
Livio Spring 5b40af79f0
fix: correctly check user state (#8631)
# Which Problems Are Solved

ZITADEL's user account deactivation mechanism did not work correctly
with service accounts. Deactivated service accounts retained the ability
to request tokens, which could lead to unauthorized access to
applications and resources.

# How the Problems Are Solved

Additionally to checking the user state on the session API and login UI,
the state is checked on all oidc session methods resulting in a new
token or when returning the user information (userinfo, introspection,
id_token / access_token and saml attributes)
2024-09-17 13:21:49 +00:00

181 lines
5.7 KiB
Go

package command
import (
"context"
"fmt"
"time"
"golang.org/x/text/language"
"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"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes, audience []string, needRefreshToken bool) (*domain.ObjectDetails, error) {
aggr := deviceauth.NewAggregate(deviceCode, authz.GetInstance(ctx).InstanceID())
model := NewDeviceAuthWriteModel(deviceCode, aggr.ResourceOwner)
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewAddedEvent(
ctx,
aggr,
clientID,
deviceCode,
userCode,
expires,
scopes,
audience,
needRefreshToken,
))
if err != nil {
return nil, err
}
err = AppendAndReduce(model, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&model.WriteModel), nil
}
func (c *Commands) ApproveDeviceAuth(
ctx context.Context,
deviceCode,
userID,
userOrgID string,
authMethods []domain.UserAuthMethodType,
authTime time.Time,
preferredLanguage *language.Tag,
userAgent *domain.UserAgent,
sessionID string,
) (*domain.ObjectDetails, error) {
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, deviceCode)
if err != nil {
return nil, err
}
if !model.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound")
}
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, model.aggregate, userID, userOrgID, authMethods, authTime, preferredLanguage, userAgent, sessionID))
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) {
model, err := c.getDeviceAuthWriteModelByDeviceCode(ctx, id)
if err != nil {
return nil, err
}
if !model.State.Exists() {
return nil, zerrors.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound")
}
pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewCanceledEvent(ctx, model.aggregate, reason))
if err != nil {
return nil, err
}
err = AppendAndReduce(model, pushedEvents...)
if err != nil {
return nil, err
}
return writeModelToObjectDetails(&model.WriteModel), nil
}
func (c *Commands) getDeviceAuthWriteModelByDeviceCode(ctx context.Context, deviceCode string) (*DeviceAuthWriteModel, error) {
model := &DeviceAuthWriteModel{
WriteModel: eventstore.WriteModel{AggregateID: deviceCode},
}
err := c.eventstore.FilterToQueryReducer(ctx, model)
if err != nil {
return nil, err
}
model.aggregate = deviceauth.NewAggregate(model.AggregateID, model.InstanceID)
return model, nil
}
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.UserID, deviceAuthModel.UserOrgID)
if err != nil {
return nil, err
}
cmd.AddSession(ctx,
deviceAuthModel.UserID,
deviceAuthModel.UserOrgID,
deviceAuthModel.SessionID,
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))
}