zitadel/internal/api/ui/login/custom_action.go
cpli 5a9609ef29
feat(actions): add "zitadel/uuid" module (#6135)
* feat: add "zitadel/uuid" module

* feat(actions/uuid): add v1, v3, and v4 UUIDs

* add namespaces and improve hash based functions

* add docs

---------

Co-authored-by: Florian Forster <florian@zitadel.com>
Co-authored-by: Livio Spring <livio.a@gmail.com>
2023-10-13 09:31:23 +02:00

437 lines
13 KiB
Go

package login
import (
"context"
"encoding/json"
"net/http"
"github.com/dop251/goja"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/actions"
"github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/query"
)
func (l *Login) runPostExternalAuthenticationActions(
user *domain.ExternalUser,
tokens *oidc.Tokens[*oidc.IDTokenClaims],
authRequest *domain.AuthRequest,
httpRequest *http.Request,
idpUser idp.User,
authenticationError error,
) (_ *domain.ExternalUser, userChanged bool, err error) {
ctx := httpRequest.Context()
// use the request org (scopes or domain discovery) as default
resourceOwner := authRequest.RequestedOrgID
// if the user was already linked to an IDP and redirected to that, the requested org might be empty
if resourceOwner == "" {
resourceOwner = authRequest.UserOrgID
}
// if we will have no org (e.g. user clicked directly on the IDP on the login page)
if resourceOwner == "" {
// in this case the user might nevertheless already be linked to an IDP,
// so let's do a workaround and resourceOwnerOfUserIDPLink if there would be a IDP link
resourceOwner, err = l.resourceOwnerOfUserIDPLink(ctx, authRequest.SelectedIDPConfigID, user.ExternalUserID)
logging.WithFields("authReq", authRequest.ID, "idpID", authRequest.SelectedIDPConfigID).OnError(err).
Warn("could not determine resource owner for runPostExternalAuthenticationActions, fall back to default org id")
}
// fallback to default org id
if resourceOwner == "" {
resourceOwner = authz.GetInstance(ctx).DefaultOrganisationID()
}
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner, false)
if err != nil {
return nil, false, err
}
metadataList := object.MetadataListFromDomain(user.Metadatas)
apiFields := actions.WithAPIFields(
actions.SetFields("setFirstName", func(firstName string) {
user.FirstName = firstName
userChanged = true
}),
actions.SetFields("setLastName", func(lastName string) {
user.LastName = lastName
userChanged = true
}),
actions.SetFields("setNickName", func(nickName string) {
user.NickName = nickName
userChanged = true
}),
actions.SetFields("setDisplayName", func(displayName string) {
user.DisplayName = displayName
userChanged = true
}),
actions.SetFields("setPreferredLanguage", func(preferredLanguage string) {
user.PreferredLanguage = language.Make(preferredLanguage)
userChanged = true
}),
actions.SetFields("setPreferredUsername", func(username string) {
user.PreferredUsername = username
userChanged = true
}),
actions.SetFields("setEmail", func(email domain.EmailAddress) {
user.Email = email
userChanged = true
}),
actions.SetFields("setEmailVerified", func(verified bool) {
user.IsEmailVerified = verified
userChanged = true
}),
actions.SetFields("setPhone", func(phone domain.PhoneNumber) {
user.Phone = phone
userChanged = true
}),
actions.SetFields("setPhoneVerified", func(verified bool) {
user.IsPhoneVerified = verified
userChanged = true
}),
actions.SetFields("metadata", func(c *actions.FieldConfig) interface{} {
return metadataList.MetadataListFromDomain(c.Runtime)
}),
actions.SetFields("v1",
actions.SetFields("user",
actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
authErrStr := "none"
if authenticationError != nil {
authErrStr = authenticationError.Error()
}
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
ctxFieldOptions := append(tokenCtxFields(tokens),
actions.SetFields("v1",
actions.SetFields("externalUser", func(c *actions.FieldConfig) interface{} {
return object.UserFromExternalUser(c, user)
}),
actions.SetFields("providerInfo", func(c *actions.FieldConfig) interface{} {
return c.Runtime.ToValue(idpUser)
}),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
actions.SetFields("httpRequest", object.HTTPRequestField(httpRequest)),
actions.SetFields("authError", authErrStr),
),
)
ctxFields := actions.SetContextFields(ctxFieldOptions...)
err = actions.Run(
actionCtx,
ctxFields,
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
return nil, false, err
}
}
user.Metadatas = object.MetadataListToDomain(metadataList)
return user, userChanged, err
}
type authMethod string
const (
authMethodPassword authMethod = "password"
authMethodOTP authMethod = "OTP"
authMethodOTPSMS authMethod = "OTP SMS"
authMethodOTPEmail authMethod = "OTP Email"
authMethodU2F authMethod = "U2F"
authMethodPasswordless authMethod = "passwordless"
)
func (l *Login) runPostInternalAuthenticationActions(
authRequest *domain.AuthRequest,
httpRequest *http.Request,
authMethod authMethod,
authenticationError error,
) ([]*domain.Metadata, error) {
ctx := httpRequest.Context()
resourceOwner := authRequest.RequestedOrgID
if resourceOwner == "" {
resourceOwner = authRequest.UserOrgID
}
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeInternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner, false)
if err != nil {
return nil, err
}
metadataList := object.MetadataListFromDomain(nil)
apiFields := actions.WithAPIFields(
actions.SetFields("metadata", func(c *actions.FieldConfig) interface{} {
return metadataList.MetadataListFromDomain(c.Runtime)
}),
actions.SetFields("v1",
actions.SetFields("user",
actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
authErrStr := "none"
if authenticationError != nil {
authErrStr = authenticationError.Error()
}
ctxFields := actions.SetContextFields(
actions.SetFields("v1",
actions.SetFields("authMethod", authMethod),
actions.SetFields("authError", authErrStr),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
actions.SetFields("httpRequest", object.HTTPRequestField(httpRequest)),
),
)
err = actions.Run(
actionCtx,
ctxFields,
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
return nil, err
}
}
return object.MetadataListToDomain(metadataList), err
}
func (l *Login) runPreCreationActions(
authRequest *domain.AuthRequest,
httpRequest *http.Request,
user *domain.Human,
metadata []*domain.Metadata,
resourceOwner string,
flowType domain.FlowType,
) (*domain.Human, []*domain.Metadata, error) {
ctx := httpRequest.Context()
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, flowType, domain.TriggerTypePreCreation, resourceOwner, false)
if err != nil {
return nil, nil, err
}
metadataList := object.MetadataListFromDomain(metadata)
apiFields := actions.WithAPIFields(
actions.SetFields("setFirstName", func(firstName string) {
user.FirstName = firstName
}),
actions.SetFields("setLastName", func(lastName string) {
user.LastName = lastName
}),
actions.SetFields("setNickName", func(nickName string) {
user.NickName = nickName
}),
actions.SetFields("setDisplayName", func(displayName string) {
user.DisplayName = displayName
}),
actions.SetFields("setPreferredLanguage", func(preferredLanguage string) {
user.PreferredLanguage = language.Make(preferredLanguage)
}),
actions.SetFields("setGender", func(gender domain.Gender) {
user.Gender = gender
}),
actions.SetFields("setUsername", func(username string) {
user.Username = username
}),
actions.SetFields("setEmail", func(email domain.EmailAddress) {
if user.Email == nil {
user.Email = &domain.Email{}
}
user.Email.EmailAddress = email
}),
actions.SetFields("setEmailVerified", func(verified bool) {
if user.Email == nil {
return
}
user.Email.IsEmailVerified = verified
}),
actions.SetFields("setPhone", func(phone domain.PhoneNumber) {
if user.Phone == nil {
user.Phone = &domain.Phone{}
}
user.Phone.PhoneNumber = phone
}),
actions.SetFields("setPhoneVerified", func(verified bool) {
if user.Phone == nil {
return
}
user.Phone.IsPhoneVerified = verified
}),
actions.SetFields("metadata", func(c *actions.FieldConfig) interface{} {
return metadataList.MetadataListFromDomain(c.Runtime)
}),
actions.SetFields("v1",
actions.SetFields("user",
actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
ctxOpts := actions.SetContextFields(
actions.SetFields("v1",
actions.SetFields("user", func(c *actions.FieldConfig) interface{} {
return object.UserFromHuman(c, user)
}),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
actions.SetFields("httpRequest", object.HTTPRequestField(httpRequest)),
),
)
err = actions.Run(
actionCtx,
ctxOpts,
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
return nil, nil, err
}
}
return user, object.MetadataListToDomain(metadataList), err
}
func (l *Login) runPostCreationActions(
userID string,
authRequest *domain.AuthRequest,
httpRequest *http.Request,
resourceOwner string,
flowType domain.FlowType,
) ([]*domain.UserGrant, error) {
ctx := httpRequest.Context()
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, flowType, domain.TriggerTypePostCreation, resourceOwner, false)
if err != nil {
return nil, err
}
mutableUserGrants := &object.UserGrants{UserGrants: make([]object.UserGrant, 0)}
apiFields := actions.WithAPIFields(
actions.SetFields("userGrants", &mutableUserGrants.UserGrants),
actions.SetFields("v1",
actions.SetFields("appendUserGrant", object.AppendGrantFunc(mutableUserGrants)),
),
)
for _, a := range triggerActions {
actionCtx, cancel := context.WithTimeout(ctx, a.Timeout())
ctxFields := actions.SetContextFields(
actions.SetFields("v1",
actions.SetFields("getUser", func(c *actions.FieldConfig) interface{} {
return func(call goja.FunctionCall) goja.Value {
user, err := l.query.GetUserByID(actionCtx, true, userID, false)
if err != nil {
panic(err)
}
return object.UserFromQuery(c, user)
}
}),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
actions.SetFields("httpRequest", object.HTTPRequestField(httpRequest)),
),
)
err = actions.Run(
actionCtx,
ctxFields,
apiFields,
a.Script,
a.Name,
append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithUUID(actionCtx))...,
)
cancel()
if err != nil {
return nil, err
}
}
return object.UserGrantsToDomain(userID, mutableUserGrants.UserGrants), err
}
func tokenCtxFields(tokens *oidc.Tokens[*oidc.IDTokenClaims]) []actions.FieldOption {
var accessToken, idToken string
getClaim := func(claim string) interface{} {
return nil
}
claimsJSON := func() (string, error) {
return "", nil
}
if tokens == nil {
return []actions.FieldOption{
actions.SetFields("accessToken", accessToken),
actions.SetFields("idToken", idToken),
actions.SetFields("getClaim", getClaim),
actions.SetFields("claimsJSON", claimsJSON),
}
}
accessToken = tokens.AccessToken
idToken = tokens.IDToken
if tokens.IDTokenClaims != nil {
getClaim = func(claim string) interface{} {
return tokens.IDTokenClaims.Claims[claim]
}
claimsJSON = func() (string, error) {
c, err := json.Marshal(tokens.IDTokenClaims)
if err != nil {
return "", err
}
return string(c), nil
}
}
return []actions.FieldOption{
actions.SetFields("accessToken", accessToken),
actions.SetFields("idToken", idToken),
actions.SetFields("getClaim", getClaim),
actions.SetFields("claimsJSON", claimsJSON),
}
}
func (l *Login) resourceOwnerOfUserIDPLink(ctx context.Context, idpConfigID string, externalUserID string) (string, error) {
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpConfigID)
if err != nil {
return "", err
}
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
if err != nil {
return "", err
}
queries := []query.SearchQuery{
idQuery, externalIDQuery,
}
links, err := l.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false)
if err != nil {
return "", err
}
if len(links.Links) != 1 {
return "", nil
}
return links.Links[0].ResourceOwner, nil
}