zitadel/internal/repository/user/user.go

546 lines
14 KiB
Go
Raw Normal View History

package user
import (
"context"
"time"
"github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
UniqueUsername = "usernames"
userEventTypePrefix = eventstore.EventType("user.")
UserLockedType = userEventTypePrefix + "locked"
UserUnlockedType = userEventTypePrefix + "unlocked"
UserDeactivatedType = userEventTypePrefix + "deactivated"
UserReactivatedType = userEventTypePrefix + "reactivated"
UserRemovedType = userEventTypePrefix + "removed"
UserTokenAddedType = userEventTypePrefix + "token.added"
perf(oidc): optimize token creation (#7822) * implement code exchange * port tokenexchange to v2 tokens * implement refresh token * implement client credentials * implement jwt profile * implement device token * cleanup unused code * fix current unit tests * add user agent unit test * unit test domain package * need refresh token as argument * test commands create oidc session * test commands device auth * fix device auth build error * implicit for oidc session API * implement authorize callback handler for legacy implicit mode * upgrade oidc module to working draft * add missing auth methods and time * handle all errors in defer * do not fail auth request on error the oauth2 Go client automagically retries on any error. If we fail the auth request on the first error, the next attempt will always fail with the Errors.AuthRequest.NoCode, because the auth request state is already set to failed. The original error is then already lost and the oauth2 library does not return the original error. Therefore we should not fail the auth request. Might be worth discussing and perhaps send a bug report to Oauth2? * fix code flow tests by explicitly setting code exchanged * fix unit tests in command package * return allowed scope from client credential client * add device auth done reducer * carry nonce thru session into ID token * fix token exchange integration tests * allow project role scope prefix in client credentials client * gci formatting * do not return refresh token in client credentials and jwt profile * check org scope * solve linting issue on authorize callback error * end session based on v2 session ID * use preferred language and user agent ID for v2 access tokens * pin oidc v3.23.2 * add integration test for jwt profile and client credentials with org scopes * refresh token v1 to v2 * add user token v2 audit event * add activity trigger * cleanup and set panics for unused methods * use the encrypted code for v1 auth request get by code * add missing event translation * fix pipeline errors (hopefully) * fix another test * revert pointer usage of preferred language * solve browser info panic in device auth * remove duplicate entries in AMRToAuthMethodTypes to prevent future `mfa` claim * revoke v1 refresh token to prevent reuse * fix terminate oidc session * always return a new refresh toke in refresh token grant --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-05-16 05:07:56 +00:00
UserTokenV2AddedType = userEventTypePrefix + "token.v2.added"
UserTokenRemovedType = userEventTypePrefix + "token.removed"
UserImpersonatedType = userEventTypePrefix + "impersonated"
UserDomainClaimedType = userEventTypePrefix + "domain.claimed"
UserDomainClaimedSentType = userEventTypePrefix + "domain.claimed.sent"
UserUserNameChangedType = userEventTypePrefix + "username.changed"
)
func NewAddUsernameUniqueConstraint(userName, resourceOwner string, userLoginMustBeDomain bool) *eventstore.UniqueConstraint {
uniqueUserName := userName
if userLoginMustBeDomain {
uniqueUserName = userName + resourceOwner
}
return eventstore.NewAddEventUniqueConstraint(
UniqueUsername,
uniqueUserName,
"Errors.User.AlreadyExists")
}
func NewRemoveUsernameUniqueConstraint(userName, resourceOwner string, userLoginMustBeDomain bool) *eventstore.UniqueConstraint {
uniqueUserName := userName
if userLoginMustBeDomain {
uniqueUserName = userName + resourceOwner
}
return eventstore.NewRemoveUniqueConstraint(
UniqueUsername,
uniqueUserName)
}
type UserLockedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *UserLockedEvent) Payload() interface{} {
return nil
}
func (e *UserLockedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserLockedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserLockedEvent {
return &UserLockedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserLockedType,
),
}
}
func UserLockedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &UserLockedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type UserUnlockedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *UserUnlockedEvent) Payload() interface{} {
return nil
}
func (e *UserUnlockedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserUnlockedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserUnlockedEvent {
return &UserUnlockedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserUnlockedType,
),
}
}
func UserUnlockedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &UserUnlockedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type UserDeactivatedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *UserDeactivatedEvent) Payload() interface{} {
return nil
}
func (e *UserDeactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserDeactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserDeactivatedEvent {
return &UserDeactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserDeactivatedType,
),
}
}
func UserDeactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &UserDeactivatedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type UserReactivatedEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *UserReactivatedEvent) Payload() interface{} {
return nil
}
func (e *UserReactivatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserReactivatedEvent(ctx context.Context, aggregate *eventstore.Aggregate) *UserReactivatedEvent {
return &UserReactivatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserReactivatedType,
),
}
}
func UserReactivatedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &UserReactivatedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type UserRemovedEvent struct {
eventstore.BaseEvent `json:"-"`
userName string
externalIDPs []*domain.UserIDPLink
loginMustBeDomain bool
}
func (e *UserRemovedEvent) Payload() interface{} {
return nil
}
func (e *UserRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
events := make([]*eventstore.UniqueConstraint, 0)
if e.userName != "" {
events = append(events, NewRemoveUsernameUniqueConstraint(e.userName, e.Aggregate().ResourceOwner, e.loginMustBeDomain))
}
for _, idp := range e.externalIDPs {
events = append(events, NewRemoveUserIDPLinkUniqueConstraint(idp.IDPConfigID, idp.ExternalUserID))
}
return events
}
func NewUserRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
userName string,
externalIDPs []*domain.UserIDPLink,
userLoginMustBeDomain bool,
) *UserRemovedEvent {
return &UserRemovedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserRemovedType,
),
userName: userName,
externalIDPs: externalIDPs,
loginMustBeDomain: userLoginMustBeDomain,
}
}
func UserRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &UserRemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type UserTokenAddedEvent struct {
eventstore.BaseEvent `json:"-"`
TokenID string `json:"tokenId,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
UserAgentID string `json:"userAgentId,omitempty"`
RefreshTokenID string `json:"refreshTokenID,omitempty"`
Audience []string `json:"audience,omitempty"`
Scopes []string `json:"scopes,omitempty"`
AuthMethodsReferences []string `json:"authMethodsReferences,omitempty"`
AuthTime time.Time `json:"authTime,omitempty"`
Expiration time.Time `json:"expiration,omitempty"`
PreferredLanguage string `json:"preferredLanguage,omitempty"`
Reason domain.TokenReason `json:"reason,omitempty"`
Actor *domain.TokenActor `json:"actor,omitempty"`
}
func (e *UserTokenAddedEvent) Payload() interface{} {
return e
}
func (e *UserTokenAddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserTokenAddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
tokenID,
applicationID,
userAgentID,
preferredLanguage,
refreshTokenID string,
audience,
scopes,
authMethodsReferences []string,
authTime,
expiration time.Time,
reason domain.TokenReason,
actor *domain.TokenActor,
) *UserTokenAddedEvent {
return &UserTokenAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserTokenAddedType,
),
TokenID: tokenID,
ApplicationID: applicationID,
UserAgentID: userAgentID,
RefreshTokenID: refreshTokenID,
Audience: audience,
Scopes: scopes,
Expiration: expiration,
PreferredLanguage: preferredLanguage,
Reason: reason,
Actor: actor,
}
}
func UserTokenAddedEventMapper(event eventstore.Event) (eventstore.Event, error) {
tokenAdded := &UserTokenAddedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(tokenAdded)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-7M9sd", "unable to unmarshal token added")
}
return tokenAdded, nil
}
perf(oidc): optimize token creation (#7822) * implement code exchange * port tokenexchange to v2 tokens * implement refresh token * implement client credentials * implement jwt profile * implement device token * cleanup unused code * fix current unit tests * add user agent unit test * unit test domain package * need refresh token as argument * test commands create oidc session * test commands device auth * fix device auth build error * implicit for oidc session API * implement authorize callback handler for legacy implicit mode * upgrade oidc module to working draft * add missing auth methods and time * handle all errors in defer * do not fail auth request on error the oauth2 Go client automagically retries on any error. If we fail the auth request on the first error, the next attempt will always fail with the Errors.AuthRequest.NoCode, because the auth request state is already set to failed. The original error is then already lost and the oauth2 library does not return the original error. Therefore we should not fail the auth request. Might be worth discussing and perhaps send a bug report to Oauth2? * fix code flow tests by explicitly setting code exchanged * fix unit tests in command package * return allowed scope from client credential client * add device auth done reducer * carry nonce thru session into ID token * fix token exchange integration tests * allow project role scope prefix in client credentials client * gci formatting * do not return refresh token in client credentials and jwt profile * check org scope * solve linting issue on authorize callback error * end session based on v2 session ID * use preferred language and user agent ID for v2 access tokens * pin oidc v3.23.2 * add integration test for jwt profile and client credentials with org scopes * refresh token v1 to v2 * add user token v2 audit event * add activity trigger * cleanup and set panics for unused methods * use the encrypted code for v1 auth request get by code * add missing event translation * fix pipeline errors (hopefully) * fix another test * revert pointer usage of preferred language * solve browser info panic in device auth * remove duplicate entries in AMRToAuthMethodTypes to prevent future `mfa` claim * revoke v1 refresh token to prevent reuse * fix terminate oidc session * always return a new refresh toke in refresh token grant --------- Co-authored-by: Livio Spring <livio.a@gmail.com>
2024-05-16 05:07:56 +00:00
type UserTokenV2AddedEvent struct {
*eventstore.BaseEvent `json:"-"`
TokenID string `json:"tokenId,omitempty"`
}
func (e *UserTokenV2AddedEvent) Payload() interface{} {
return e
}
func (e *UserTokenV2AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) {
e.BaseEvent = b
}
func (e *UserTokenV2AddedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserTokenV2AddedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
tokenID string,
) *UserTokenV2AddedEvent {
return &UserTokenV2AddedEvent{
BaseEvent: eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserTokenV2AddedType,
),
TokenID: tokenID,
}
}
type UserImpersonatedEvent struct {
eventstore.BaseEvent `json:"-"`
ApplicationID string `json:"applicationId,omitempty"`
Actor *domain.TokenActor `json:"actor,omitempty"`
}
func (e *UserImpersonatedEvent) Payload() interface{} {
return e
}
func (e *UserImpersonatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *UserImpersonatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = *base
}
func NewUserImpersonatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
applicationID string,
actor *domain.TokenActor,
) *UserImpersonatedEvent {
return &UserImpersonatedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserImpersonatedType,
),
ApplicationID: applicationID,
Actor: actor,
}
}
type UserTokenRemovedEvent struct {
eventstore.BaseEvent `json:"-"`
TokenID string `json:"tokenId"`
}
func (e *UserTokenRemovedEvent) Payload() interface{} {
return e
}
func (e *UserTokenRemovedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewUserTokenRemovedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
tokenID string,
) *UserTokenRemovedEvent {
return &UserTokenRemovedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserTokenRemovedType,
),
TokenID: tokenID,
}
}
func UserTokenRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
tokenRemoved := &UserTokenRemovedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(tokenRemoved)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-7M9sd", "unable to unmarshal token added")
}
return tokenRemoved, nil
}
type DomainClaimedEvent struct {
eventstore.BaseEvent `json:"-"`
UserName string `json:"userName"`
TriggeredAtOrigin string `json:"triggerOrigin,omitempty"`
oldUserName string
userLoginMustBeDomain bool
}
func (e *DomainClaimedEvent) Payload() interface{} {
return e
}
func (e *DomainClaimedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{
NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain),
NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain),
}
}
func (e *DomainClaimedEvent) TriggerOrigin() string {
return e.TriggeredAtOrigin
}
func NewDomainClaimedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
userName,
oldUserName string,
userLoginMustBeDomain bool,
) *DomainClaimedEvent {
return &DomainClaimedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserDomainClaimedType,
),
UserName: userName,
oldUserName: oldUserName,
userLoginMustBeDomain: userLoginMustBeDomain,
feat: trusted (instance) domains (#8369) # Which Problems Are Solved ZITADEL currently selects the instance context based on a HTTP header (see https://github.com/zitadel/zitadel/issues/8279#issue-2399959845 and checks it against the list of instance domains. Let's call it instance or API domain. For any context based URL (e.g. OAuth, OIDC, SAML endpoints, links in emails, ...) the requested domain (instance domain) will be used. Let's call it the public domain. In cases of proxied setups, all exposed domains (public domains) require the domain to be managed as instance domain. This can either be done using the "ExternalDomain" in the runtime config or via system API, which requires a validation through CustomerPortal on zitadel.cloud. # How the Problems Are Solved - Two new headers / header list are added: - `InstanceHostHeaders`: an ordered list (first sent wins), which will be used to match the instance. (For backward compatibility: the `HTTP1HostHeader`, `HTTP2HostHeader` and `forwarded`, `x-forwarded-for`, `x-forwarded-host` are checked afterwards as well) - `PublicHostHeaders`: an ordered list (first sent wins), which will be used as public host / domain. This will be checked against a list of trusted domains on the instance. - The middleware intercepts all requests to the API and passes a `DomainCtx` object with the hosts and protocol into the context (previously only a computed `origin` was passed) - HTTP / GRPC server do not longer try to match the headers to instances themself, but use the passed `http.DomainContext` in their interceptors. - The `RequestedHost` and `RequestedDomain` from authz.Instance are removed in favor of the `http.DomainContext` - When authenticating to or signing out from Console UI, the current `http.DomainContext(ctx).Origin` (already checked by instance interceptor for validity) is used to compute and dynamically add a `redirect_uri` and `post_logout_redirect_uri`. - Gateway passes all configured host headers (previously only did `x-zitadel-*`) - Admin API allows to manage trusted domain # Additional Changes None # Additional Context - part of #8279 - open topics: - "single-instance" mode - Console UI
2024-07-31 15:00:38 +00:00
TriggeredAtOrigin: http.DomainContext(ctx).Origin(),
}
}
func DomainClaimedEventMapper(event eventstore.Event) (eventstore.Event, error) {
domainClaimed := &DomainClaimedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(domainClaimed)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-aR8jc", "unable to unmarshal domain claimed")
}
return domainClaimed, nil
}
type DomainClaimedSentEvent struct {
eventstore.BaseEvent `json:"-"`
}
func (e *DomainClaimedSentEvent) Payload() interface{} {
return nil
}
func (e *DomainClaimedSentEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func NewDomainClaimedSentEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
) *DomainClaimedSentEvent {
return &DomainClaimedSentEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserDomainClaimedSentType,
),
}
}
func DomainClaimedSentEventMapper(event eventstore.Event) (eventstore.Event, error) {
return &DomainClaimedSentEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}, nil
}
type UsernameChangedEvent struct {
eventstore.BaseEvent `json:"-"`
UserName string `json:"userName"`
oldUserName string
userLoginMustBeDomain bool
oldUserLoginMustBeDomain bool
}
func (e *UsernameChangedEvent) Payload() interface{} {
return e
}
func (e *UsernameChangedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return []*eventstore.UniqueConstraint{
NewRemoveUsernameUniqueConstraint(e.oldUserName, e.Aggregate().ResourceOwner, e.oldUserLoginMustBeDomain),
NewAddUsernameUniqueConstraint(e.UserName, e.Aggregate().ResourceOwner, e.userLoginMustBeDomain),
}
}
func NewUsernameChangedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
oldUserName,
newUserName string,
userLoginMustBeDomain bool,
opts ...UsernameChangedEventOption,
) *UsernameChangedEvent {
event := &UsernameChangedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserUserNameChangedType,
),
UserName: newUserName,
oldUserName: oldUserName,
userLoginMustBeDomain: userLoginMustBeDomain,
oldUserLoginMustBeDomain: userLoginMustBeDomain,
}
for _, opt := range opts {
opt(event)
}
return event
}
type UsernameChangedEventOption func(*UsernameChangedEvent)
// UsernameChangedEventWithPolicyChange signals that the change occurs because of / during a domain policy change
// (will ensure the unique constraint change is handled correctly)
func UsernameChangedEventWithPolicyChange() UsernameChangedEventOption {
return func(e *UsernameChangedEvent) {
e.oldUserLoginMustBeDomain = !e.userLoginMustBeDomain
}
}
func UsernameChangedEventMapper(event eventstore.Event) (eventstore.Event, error) {
domainClaimed := &UsernameChangedEvent{
BaseEvent: *eventstore.BaseEventFromRepo(event),
}
err := event.Unmarshal(domainClaimed)
if err != nil {
return nil, zerrors.ThrowInternal(err, "USER-4Bm9s", "unable to unmarshal username changed")
}
return domainClaimed, nil
}