diff --git a/.gitignore b/.gitignore
index 6169749e73..333eb0f8f1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ profile.cov
__debug_bin
debug
sandbox.go
+/cmd/dev/
# IDE
.idea
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3520156346..6713e31e00 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -259,14 +259,14 @@ npm run lint:fix
# Install npm dependencies
npm install
-# Run all tests in a headless browser
-npm run e2e:dev
+# Run all e2e tests
+npm run e2e:dev -- --headed
```
You can also open the test suite interactively for fast success feedback on specific tests.
```bash
-# Run all tests in a headless browser
+# Run tests interactively
npm run open:dev
```
diff --git a/console/src/app/app.component.ts b/console/src/app/app.component.ts
index 39e331c6bc..16dbe053a8 100644
--- a/console/src/app/app.component.ts
+++ b/console/src/app/app.component.ts
@@ -202,7 +202,6 @@ export class AppComponent implements OnDestroy {
.getActiveOrg()
.then(async (org) => {
this.org = org;
-
// TODO add when console storage is implemented
// this.startIntroWorkflow();
})
diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html
index cc99cf48b4..0eedef35e0 100644
--- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html
+++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-factor-dialog/auth-factor-dialog.component.html
@@ -6,7 +6,13 @@
{{ 'USER.MFA.DIALOG.ADD_MFA_DESCRIPTION' | translate }}
-
+
{{ 'USER.MFA.OTP' | translate }}
diff --git a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts
index 3baf2fc499..3007b957d5 100644
--- a/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts
+++ b/console/src/app/pages/users/user-detail/auth-user-detail/auth-user-mfa/auth-user-mfa.component.ts
@@ -37,7 +37,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public AuthFactorState: any = AuthFactorState;
public error: string = '';
- public otpAvailable: boolean = false;
+ public otpDisabled$ = new BehaviorSubject
(true);
constructor(private service: GrpcAuthService, private toast: ToastService, private dialog: MatDialog) {}
@@ -52,7 +52,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
public addAuthFactor(): void {
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
data: {
- otpDisabled: !this.otpAvailable,
+ otpDisabled$: this.otpDisabled$,
},
});
@@ -71,7 +71,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
const index = list.findIndex((mfa) => mfa.otp);
if (index === -1) {
- this.otpAvailable = true;
+ this.otpDisabled$.next(false);
}
})
.catch((error) => {
diff --git a/docs/docs/apis/actions/login-flow.md b/docs/docs/apis/actions/complement-token.md
similarity index 87%
rename from docs/docs/apis/actions/login-flow.md
rename to docs/docs/apis/actions/complement-token.md
index 97eda02c2b..60f83df36b 100644
--- a/docs/docs/apis/actions/login-flow.md
+++ b/docs/docs/apis/actions/complement-token.md
@@ -1,16 +1,14 @@
---
-title: Login flows
+title: Complement Token Flow
---
-## Complement Token
-
This flow is executed during the creation of tokens and token introspection.
-### Pre Userinfo creation
+## Pre Userinfo creation
This trigger is called before userinfo are set in the token or response.
-#### Parameters of Pre Userinfo creation
+### Parameters of Pre Userinfo creation
- `ctx`
The first parameter contains the following fields:
@@ -27,11 +25,11 @@ This trigger is called before userinfo are set in the token or response.
- `setMetadata(string, Any)`
Key of the metadata and any value
-### Pre access token creation
+## Pre access token creation
This trigger is called before the claims are set in the access token and the token type is `jwt`.
-#### Parameters of Pre access token creation
+### Parameters of Pre access token creation
- `ctx`
The first parameter contains the following fields:
diff --git a/docs/docs/apis/actions/register-flow.md b/docs/docs/apis/actions/external-authentication.md
similarity index 89%
rename from docs/docs/apis/actions/register-flow.md
rename to docs/docs/apis/actions/external-authentication.md
index 6a440b6825..204f9eba81 100644
--- a/docs/docs/apis/actions/register-flow.md
+++ b/docs/docs/apis/actions/external-authentication.md
@@ -1,16 +1,14 @@
---
-title: Register flows
+title: External Authentication Flow
---
-## External Authentication
-
This flow is executed if the user logs in using an [identity provider](../../guides/integrate/identity-brokering) or using a [jwt token](../../concepts/structure/jwt_idp).
-### Post Authentication
+## Post Authentication
A user has authenticated externally. ZITADEL retrieved and mapped the external information.
-#### Parameters of post authentication action
+### Parameters of Post Authentication Action
- `ctx`
The first parameter contains the following fields
@@ -24,6 +22,7 @@ The first parameter contains the following fields
The id token which will be returned to the user
- `v1`
- `externalUser()` [*externalUser*](./objects#external-user)
+ - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
- `api`
The second parameter contains the following fields
- `v1`
@@ -53,16 +52,17 @@ The first parameter contains the following fields
- `metadata`
Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
-### Pre Creation
+## Pre Creation
A user selected **Register** on the overview page after external authentication. ZITADEL did not create the user yet.
-#### Parameters of Pre Creation
+### Parameters of Pre Creation
- `ctx`
The first parameter contains the following fields
- `v1`
- `user` [*human*](./objects#human-user)
+ - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
- `api`
The second parameter contains the following fields
- `metadata`
@@ -95,16 +95,17 @@ A user selected **Register** on the overview page after external authentication.
- `appendMetadata(string, Any)`
The first parameter represents the key and the second a value which will be stored
-### Post Creation
+## Post Creation
A user selected **Register** on the overview page after external authentication and ZITADEL successfully created the user.
-#### Parameters of Post Creation
+### Parameters of Post Creation
- `ctx`
The first parameter contains the following fields
- `v1`
- `getUser()` [*user*](./objects#user)
+ - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
- `api`
The second parameter contains the following fields
- `userGrants` Array of [*userGrant*](./objects#user-grant)'s
diff --git a/docs/docs/apis/actions/internal-authentication.md b/docs/docs/apis/actions/internal-authentication.md
new file mode 100644
index 0000000000..61f96bc356
--- /dev/null
+++ b/docs/docs/apis/actions/internal-authentication.md
@@ -0,0 +1,90 @@
+---
+title: Internal Authentication Flow
+---
+
+## Post Authentication
+
+A user has authenticated directly at ZITADEL.
+ZITADEL validated the users inputs for password, one-time password, security key or passwordless factor.
+Each validation step triggers the action.
+
+### Parameters of Post Authentication Action
+
+- `ctx`
+ The first parameter contains the following fields
+ - `v1`
+ - `authMethod` *string*
+ This is one of "password", "OTP", "U2F" or "passwordless"
+ - `authError` *string*
+ This is a verification errors string representation. If the verification succeeds, this is "none"
+ - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
+- `api`
+ The second parameter contains the following fields
+ - `metadata`
+ Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
+ - `v1`
+ - `user`
+ - `appendMetadata(string, Any)`
+ The first parameter represents the key and the second a value which will be stored
+
+## Pre Creation
+
+A user registers directly at ZITADEL.
+ZITADEL did not create the user yet.
+
+### Parameters of Pre Creation
+
+- `ctx`
+ The first parameter contains the following fields
+ - `v1`
+ - `user` [*human*](./objects#human-user)
+ - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
+- `api`
+ The second parameter contains the following fields
+ - `metadata`
+ Array of [*metadata*](./objects#metadata-with-value-as-bytes). This function is deprecated, please use `api.v1.user.appendMetadata`
+ - `setFirstName(string)`
+ Sets the first name
+ - `setLastName(string)`
+ Sets the last name
+ - `setNickName(string)`
+ Sets the nick name
+ - `setDisplayName(string)`
+ Sets the display name
+ - `setPreferredLanguage(string)`
+ Sets the preferred language, the string has to be a valid language tag as defined in [RFC 5646](https://www.rfc-editor.org/rfc/rfc5646)
+ - `setGender(int)`
+ Sets the gender.
+ 0: unspecified 1: female 2: male 3: diverse
+ - `setUsername(string)`
+ Sets the username
+ - `setEmail(string)`
+ Sets the email
+ - `setEmailVerified(bool)`
+ If true the email set is verified without user interaction
+ - `setPhone(string)`
+ Sets the phone number
+ - `setPhoneVerified(bool)`
+ If true the phone number set is verified without user interaction
+ - `v1`
+ - `user`
+ - `appendMetadata(string, Any)`
+ The first parameter represents the key and the second a value which will be stored
+
+## Post Creation
+
+A user registers directly at ZITADEL.
+ZITADEL successfully created the user.
+
+### Parameters of Post Creation
+
+- `ctx`
+ The first parameter contains the following fields
+ - `v1`
+ - `getUser()` [*user*](./objects#user)
+ - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
+- `api`
+ The second parameter contains the following fields
+ - `userGrants` Array of [*userGrant*](./objects#user-grant)'s
+ - `v1`
+ - `appendUserGrant(`[`userGrant`](./objects#user-grant)`)`
diff --git a/docs/docs/apis/actions/introduction.md b/docs/docs/apis/actions/introduction.md
index 4d88c06111..6321414dbc 100644
--- a/docs/docs/apis/actions/introduction.md
+++ b/docs/docs/apis/actions/introduction.md
@@ -46,9 +46,10 @@ Trigger types define the point during execution of request. Each trigger defines
Currently ZITADEL provides the following flows:
-- [Login](./login-flow.md)
-- [Register](./register-flow.md)
+- [Internal Authentication](./internal-authentication.md)
+- [External Authentication](./external-authentication.md)
+- [Complement Token](./complement-token.md)
## Available Modules inside Javascript
-- [HTTP module](./modules#http) to call API's
\ No newline at end of file
+- [HTTP module](./modules#http) to call API's
diff --git a/docs/docs/apis/actions/objects.md b/docs/docs/apis/actions/objects.md
index 6a1f4d14ed..8b54e9a5cd 100644
--- a/docs/docs/apis/actions/objects.md
+++ b/docs/docs/apis/actions/objects.md
@@ -108,3 +108,48 @@ title: Objects
- `phone`
- `phone` *string*
- `isPhoneVerified` *boolean*
+
+## Auth Request
+
+This object contains context information about the request to the [authorization endpoint](/docs/apis/openidoauth/endpoints#authorization_endpoint).
+
+- `id` *string*
+- `agentId` *string*
+- `creationDate` *Date*
+- `changeDate` *Date*
+- `browserInfo` *browserInfo*
+ - `userAgent` *string*
+ - `acceptLanguage` *string*
+ - `remoteIp` *string*
+- `applicationId` *string*
+- `callbackUri` *string*
+- `transferState` *string*
+- `prompt` Array of *Number*
+ 0: not specified 1: none 2: login 3: consent 4: select_account 5: create
+- `uiLocales` Array of *string*
+- `loginHint` *string*
+- `maxAuthAge` *Number*
+ Duration in nanoseconds
+- `instanceId` *string*
+- `request`
+ - `oidc`
+ - `scopes` Array of *string*
+- `userId` *string*
+- `userName` *string*
+- `loginName` *string*
+- `displayName` *string*
+- `resourceOwner` *string*
+- `requestedOrgId` *string*
+- `requestedOrgName` *string*
+- `requestedPrimaryDomain` *string*
+- `requestedOrgDomain` *bool*
+- `applicationResourceOwner` *string*
+- `privateLabelingSetting` *Number*
+ 0: Unspecified 1: Enforce project resource owner policy 2: Allow login user resource owner policy
+- `selectedIdpConfigId` *string*
+- `linkingUsers` Array of [*ExternalUser*](#external-user)
+- `passwordVerified` *bool*
+- `mfasVerified` Array of *Number*
+ 0: OTP 1: U2F 2: U2F User verification
+- `audience` Array of *string*
+- `authTime` *Date*
diff --git a/docs/docs/guides/manage/customize/behavior.md b/docs/docs/guides/manage/customize/behavior.md
index 6b73990c93..865a58457a 100644
--- a/docs/docs/guides/manage/customize/behavior.md
+++ b/docs/docs/guides/manage/customize/behavior.md
@@ -35,7 +35,7 @@ https://github.com/zitadel/actions/blob/main/examples/add_user_grant.js
## Run the action when a user registers
-Now, make the action hook into the [external authentication flow](../../../apis/actions/register-flow#external-authentication).
+Now, make the action hook into the [external authentication flow](../../../apis/actions/external-authentication).
1. In the **Flows ** section, select the **+ New** button.
1. Select the **Flow Type** _External Authentication_.
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 710ebbd2c6..189c86b3ea 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -232,8 +232,9 @@ module.exports = {
items: [
"apis/actions/introduction",
"apis/actions/modules",
- "apis/actions/login-flow",
- "apis/actions/register-flow",
+ "apis/actions/internal-authentication",
+ "apis/actions/external-authentication",
+ "apis/actions/complement-token",
"apis/actions/objects",
]
},
diff --git a/e2e/.gitignore b/e2e/.gitignore
index 3c3629e647..ec4817a10c 100644
--- a/e2e/.gitignore
+++ b/e2e/.gitignore
@@ -1 +1,2 @@
node_modules
+.cypress-cache
diff --git a/e2e/.prettierignore b/e2e/.prettierignore
index d1eeddfaa3..8b33bc629c 100644
--- a/e2e/.prettierignore
+++ b/e2e/.prettierignore
@@ -7,3 +7,6 @@ node_modules
cypress/screenshots/
cypress/videos/
cypress/results/
+
+# not compiling code
+cypress/e2e/actions/e2eSetMetadata.js
\ No newline at end of file
diff --git a/e2e/.prettierrc.json b/e2e/.prettierrc.json
index 7a2a5d19e3..c961125f02 100644
--- a/e2e/.prettierrc.json
+++ b/e2e/.prettierrc.json
@@ -2,4 +2,4 @@
"printWidth": 125,
"singleQuote": true,
"trailingComma": "all"
-}
+}
\ No newline at end of file
diff --git a/e2e/cypress.config.ts b/e2e/cypress.config.ts
index 57ad7e226d..bcebebcf72 100644
--- a/e2e/cypress.config.ts
+++ b/e2e/cypress.config.ts
@@ -12,7 +12,6 @@ export default defineConfig({
json: true,
},
- chromeWebSecurity: false,
trashAssetsBeforeRuns: false,
defaultCommandTimeout: 10000,
diff --git a/internal/actions/object/auth_request.go b/internal/actions/object/auth_request.go
new file mode 100644
index 0000000000..4c4bc3e1e8
--- /dev/null
+++ b/internal/actions/object/auth_request.go
@@ -0,0 +1,122 @@
+package object
+
+import (
+ "net"
+ "time"
+
+ "github.com/dop251/goja"
+
+ "github.com/zitadel/zitadel/internal/actions"
+ "github.com/zitadel/zitadel/internal/domain"
+)
+
+// AuthRequestField accepts the domain.AuthRequest by value, so its not mutated
+func AuthRequestField(authRequest *domain.AuthRequest) func(c *actions.FieldConfig) interface{} {
+ return func(c *actions.FieldConfig) interface{} {
+ return AuthRequestFromDomain(c, authRequest)
+ }
+}
+
+func AuthRequestFromDomain(c *actions.FieldConfig, request *domain.AuthRequest) goja.Value {
+ return c.Runtime.ToValue(&authRequest{
+ Id: request.ID,
+ AgentId: request.AgentID,
+ CreationDate: request.CreationDate,
+ ChangeDate: request.ChangeDate,
+ BrowserInfo: &browserInfo{
+ UserAgent: request.BrowserInfo.UserAgent,
+ AcceptLanguage: request.BrowserInfo.AcceptLanguage,
+ RemoteIp: request.BrowserInfo.RemoteIP,
+ },
+ ApplicationId: request.ApplicationID,
+ CallbackUri: request.CallbackURI,
+ TransferState: request.TransferState,
+ Prompt: request.Prompt,
+ UiLocales: request.UiLocales,
+ LoginHint: request.LoginHint,
+ MaxAuthAge: request.MaxAuthAge,
+ InstanceId: request.InstanceID,
+ Request: requestFromDomain(request.Request),
+ UserId: request.UserID,
+ UserName: request.UserName,
+ LoginName: request.LoginName,
+ DisplayName: request.DisplayName,
+ ResourceOwner: request.UserOrgID,
+ RequestedOrgId: request.RequestedOrgID,
+ RequestedOrgName: request.RequestedOrgName,
+ RequestedPrimaryDomain: request.RequestedPrimaryDomain,
+ RequestedOrgDomain: request.RequestedOrgDomain,
+ ApplicationResourceOwner: request.ApplicationResourceOwner,
+ PrivateLabelingSetting: request.PrivateLabelingSetting,
+ SelectedIdpConfigId: request.SelectedIDPConfigID,
+ LinkingUsers: externalUsersFromDomain(request.LinkingUsers),
+ PasswordVerified: request.PasswordVerified,
+ MfasVerified: request.MFAsVerified,
+ Audience: request.Audience,
+ AuthTime: request.AuthTime,
+ })
+}
+
+type authRequest struct {
+ Id string
+ AgentId string
+ CreationDate time.Time
+ ChangeDate time.Time
+ BrowserInfo *browserInfo
+ ApplicationId string
+ CallbackUri string
+ TransferState string
+ Prompt []domain.Prompt
+ UiLocales []string
+ LoginHint string
+ MaxAuthAge *time.Duration
+ InstanceId string
+ Request *request
+ UserId string
+ UserName string
+ LoginName string
+ DisplayName string
+ // UserOrgID string
+ ResourceOwner string
+ // requested by scope
+ RequestedOrgId string
+ // requested by scope
+ RequestedOrgName string
+ // requested by scope
+ RequestedPrimaryDomain string
+ // requested by scope
+ RequestedOrgDomain bool
+ // client
+ ApplicationResourceOwner string
+ PrivateLabelingSetting domain.PrivateLabelingSetting
+ SelectedIdpConfigId string
+ LinkingUsers []*externalUser
+ PasswordVerified bool
+ MfasVerified []domain.MFAType
+ Audience []string
+ AuthTime time.Time
+}
+
+func requestFromDomain(req domain.Request) *request {
+ r := new(request)
+
+ if oidcRequest, ok := req.(*domain.AuthRequestOIDC); ok {
+ r.Oidc = OIDCRequest{Scopes: oidcRequest.Scopes}
+ }
+
+ return r
+}
+
+type request struct {
+ Oidc OIDCRequest
+}
+
+type OIDCRequest struct {
+ Scopes []string
+}
+
+type browserInfo struct {
+ UserAgent string
+ AcceptLanguage string
+ RemoteIp net.IP
+}
diff --git a/internal/actions/object/metadata.go b/internal/actions/object/metadata.go
index ae48a4a059..ba66604f15 100644
--- a/internal/actions/object/metadata.go
+++ b/internal/actions/object/metadata.go
@@ -8,6 +8,7 @@ import (
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/actions"
+ "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
@@ -54,3 +55,87 @@ type userMetadata struct {
Key string
Value goja.Value
}
+
+type MetadataList struct {
+ Metadata []*Metadata
+}
+
+type Metadata struct {
+ Key string
+ // Value is for exporting to javascript
+ Value goja.Value
+ // value is for mapping to [domain.Metadata]
+ value []byte
+}
+
+func (md *MetadataList) AppendMetadataFunc(call goja.FunctionCall) goja.Value {
+ if len(call.Arguments) != 2 {
+ panic("exactly 2 (key, value) arguments expected")
+ }
+
+ value, err := json.Marshal(call.Arguments[1].Export())
+ if err != nil {
+ logging.WithError(err).Debug("unable to marshal")
+ panic(err)
+ }
+
+ md.Metadata = append(md.Metadata,
+ &Metadata{
+ Key: call.Arguments[0].Export().(string),
+ Value: call.Arguments[1],
+ value: value,
+ })
+ return nil
+}
+
+func MetadataListToDomain(metadataList *MetadataList) []*domain.Metadata {
+ if metadataList == nil {
+ return nil
+ }
+
+ list := make([]*domain.Metadata, len(metadataList.Metadata))
+ for i, metadata := range metadataList.Metadata {
+ list[i] = &domain.Metadata{
+ Key: metadata.Key,
+ Value: metadata.value,
+ }
+ }
+
+ return list
+}
+
+func MetadataField(metadata *MetadataList) func(c *actions.FieldConfig) interface{} {
+ return func(c *actions.FieldConfig) interface{} {
+ for _, md := range metadata.Metadata {
+ if json.Valid(md.value) {
+ err := json.Unmarshal(md.value, &md.Value)
+ if err != nil {
+ panic(err)
+ }
+ }
+ }
+
+ return metadata.Metadata
+ }
+}
+
+func MetadataListFromDomain(metadata []*domain.Metadata) *MetadataList {
+ list := &MetadataList{Metadata: make([]*Metadata, len(metadata))}
+
+ for i, md := range metadata {
+ var val interface{}
+ if json.Valid(md.Value) {
+ err := json.Unmarshal(md.Value, &val)
+ if err != nil {
+ panic(err)
+ }
+ }
+
+ list.Metadata[i] = &Metadata{
+ Key: md.Key,
+ value: md.Value,
+ }
+ }
+
+ return list
+}
diff --git a/internal/actions/object/object.go b/internal/actions/object/object.go
new file mode 100644
index 0000000000..c51236c8da
--- /dev/null
+++ b/internal/actions/object/object.go
@@ -0,0 +1,14 @@
+package object
+
+import "github.com/dop251/goja"
+
+func objectFromFirstArgument(call goja.FunctionCall, runtime *goja.Runtime) *goja.Object {
+ if len(call.Arguments) != 1 {
+ panic("exactly one argument expected")
+ }
+ object := call.Arguments[0].ToObject(runtime)
+ if object == nil {
+ panic("unable to unmarshal arg")
+ }
+ return object
+}
diff --git a/internal/actions/object/user.go b/internal/actions/object/user.go
index 043ab36e3e..83fa873b85 100644
--- a/internal/actions/object/user.go
+++ b/internal/actions/object/user.go
@@ -11,7 +11,21 @@ import (
)
func UserFromExternalUser(c *actions.FieldConfig, user *domain.ExternalUser) goja.Value {
- return c.Runtime.ToValue(&externalUser{
+ return c.Runtime.ToValue(externalUserFromDomain(user))
+}
+
+func externalUsersFromDomain(users []*domain.ExternalUser) []*externalUser {
+ externalUsers := make([]*externalUser, len(users))
+
+ for i, user := range users {
+ externalUsers[i] = externalUserFromDomain(user)
+ }
+
+ return externalUsers
+}
+
+func externalUserFromDomain(user *domain.ExternalUser) *externalUser {
+ return &externalUser{
ExternalId: user.ExternalUserID,
ExternalIdpId: user.ExternalUserID,
Human: human{
@@ -25,7 +39,7 @@ func UserFromExternalUser(c *actions.FieldConfig, user *domain.ExternalUser) goj
Phone: user.Phone,
IsPhoneVerified: user.IsPhoneVerified,
},
- })
+ }
}
func UserFromHuman(c *actions.FieldConfig, user *domain.Human) goja.Value {
@@ -95,6 +109,7 @@ func humanFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
},
})
}
+
func machineFromQuery(c *actions.FieldConfig, user *query.User) goja.Value {
return c.Runtime.ToValue(&machineUser{
Id: user.ID,
diff --git a/internal/actions/object/user_grant.go b/internal/actions/object/user_grant.go
new file mode 100644
index 0000000000..d95e7d574c
--- /dev/null
+++ b/internal/actions/object/user_grant.go
@@ -0,0 +1,68 @@
+package object
+
+import (
+ "github.com/dop251/goja"
+
+ "github.com/zitadel/zitadel/internal/actions"
+ "github.com/zitadel/zitadel/internal/domain"
+)
+
+type UserGrants struct {
+ UserGrants []UserGrant
+}
+
+type UserGrant struct {
+ ProjectID string
+ ProjectGrantID string
+ Roles []string
+}
+
+func AppendGrantFunc(userGrants *UserGrants) func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
+ return func(c *actions.FieldConfig) func(call goja.FunctionCall) goja.Value {
+ return func(call goja.FunctionCall) goja.Value {
+ firstArg := objectFromFirstArgument(call, c.Runtime)
+ grant := UserGrant{}
+ mapObjectToGrant(firstArg, &grant)
+ userGrants.UserGrants = append(userGrants.UserGrants, grant)
+ return nil
+ }
+ }
+}
+
+func mapObjectToGrant(object *goja.Object, grant *UserGrant) {
+ for _, key := range object.Keys() {
+ switch key {
+ case "projectId":
+ grant.ProjectID = object.Get(key).String()
+ case "projectGrantId":
+ grant.ProjectGrantID = object.Get(key).String()
+ case "roles":
+ if roles, ok := object.Get(key).Export().([]interface{}); ok {
+ for _, role := range roles {
+ if r, ok := role.(string); ok {
+ grant.Roles = append(grant.Roles, r)
+ }
+ }
+ }
+ }
+ }
+ if grant.ProjectID == "" {
+ panic("projectId not set")
+ }
+}
+
+func UserGrantsToDomain(userID string, actionUserGrants []UserGrant) []*domain.UserGrant {
+ if actionUserGrants == nil {
+ return nil
+ }
+ userGrants := make([]*domain.UserGrant, len(actionUserGrants))
+ for i, grant := range actionUserGrants {
+ userGrants[i] = &domain.UserGrant{
+ UserID: userID,
+ ProjectID: grant.ProjectID,
+ ProjectGrantID: grant.ProjectGrantID,
+ RoleKeys: grant.Roles,
+ }
+ }
+ return userGrants
+}
diff --git a/internal/actions/provided.go b/internal/actions/provided.go
deleted file mode 100644
index 635323fc1a..0000000000
--- a/internal/actions/provided.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package actions
-
-type UserGrant struct {
- ProjectID string
- ProjectGrantID string
- Roles []string
-}
diff --git a/internal/api/grpc/action/action.go b/internal/api/grpc/action/action.go
index ffc0cb5cc7..bc29f6e119 100644
--- a/internal/api/grpc/action/action.go
+++ b/internal/api/grpc/action/action.go
@@ -17,6 +17,8 @@ func FlowTypeToDomain(flowType string) domain.FlowType {
return domain.FlowTypeExternalAuthentication
case domain.FlowTypeCustomiseToken.ID():
return domain.FlowTypeCustomiseToken
+ case domain.FlowTypeInternalAuthentication.ID():
+ return domain.FlowTypeInternalAuthentication
default:
return domain.FlowTypeUnspecified
}
diff --git a/internal/api/grpc/admin/export.go b/internal/api/grpc/admin/export.go
index 70ec2ca578..15cf7ad313 100644
--- a/internal/api/grpc/admin/export.go
+++ b/internal/api/grpc/admin/export.go
@@ -659,7 +659,7 @@ func (s *Server) getUsers(ctx context.Context, org string, withPasswords bool, w
func (s *Server) getTriggerActions(ctx context.Context, org string, processedActions []string) (_ []*management_pb.SetTriggerActionsRequest, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
- flowTypes := []domain.FlowType{domain.FlowTypeExternalAuthentication}
+ flowTypes := []domain.FlowType{domain.FlowTypeExternalAuthentication, domain.FlowTypeInternalAuthentication}
triggerActions := make([]*management_pb.SetTriggerActionsRequest, 0)
for _, flowType := range flowTypes {
diff --git a/internal/api/grpc/management/flow.go b/internal/api/grpc/management/flow.go
index c8692bd813..3e3ed25f81 100644
--- a/internal/api/grpc/management/flow.go
+++ b/internal/api/grpc/management/flow.go
@@ -17,6 +17,7 @@ func (s *Server) ListFlowTypes(ctx context.Context, _ *mgmt_pb.ListFlowTypesRequ
Result: []*action_pb.FlowType{
action_grpc.FlowTypeToPb(domain.FlowTypeExternalAuthentication),
action_grpc.FlowTypeToPb(domain.FlowTypeCustomiseToken),
+ action_grpc.FlowTypeToPb(domain.FlowTypeInternalAuthentication),
},
}, nil
}
diff --git a/internal/api/ui/login/custom_action.go b/internal/api/ui/login/custom_action.go
index f63d3b8487..30f6225329 100644
--- a/internal/api/ui/login/custom_action.go
+++ b/internal/api/ui/login/custom_action.go
@@ -5,7 +5,6 @@ import (
"encoding/json"
"github.com/dop251/goja"
- "github.com/zitadel/logging"
"github.com/zitadel/oidc/v2/pkg/oidc"
"golang.org/x/text/language"
@@ -30,25 +29,7 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
return nil, err
}
- ctxFields := actions.SetContextFields(
- actions.SetFields("accessToken", tokens.AccessToken),
- actions.SetFields("idToken", tokens.IDToken),
- actions.SetFields("getClaim", func(claim string) interface{} {
- return tokens.IDTokenClaims.GetClaim(claim)
- }),
- actions.SetFields("claimsJSON", func() (string, error) {
- c, err := json.Marshal(tokens.IDTokenClaims)
- if err != nil {
- return "", err
- }
- return string(c), nil
- }),
- actions.SetFields("v1",
- actions.SetFields("externalUser", func(c *actions.FieldConfig) interface{} {
- return object.UserFromExternalUser(c, user)
- }),
- ),
- )
+ metadataList := object.MetadataListFromDomain(user.Metadatas)
apiFields := actions.WithAPIFields(
actions.SetFields("setFirstName", func(firstName string) {
user.FirstName = firstName
@@ -80,35 +61,28 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
actions.SetFields("setPhoneVerified", func(verified bool) {
user.IsPhoneVerified = verified
}),
- actions.SetFields("metadata", &user.Metadatas),
+ actions.SetFields("metadata", &metadataList.Metadata),
actions.SetFields("v1",
actions.SetFields("user",
- actions.SetFields("appendMetadata", func(call goja.FunctionCall) goja.Value {
- if len(call.Arguments) != 2 {
- panic("exactly 2 (key, value) arguments expected")
- }
- key := call.Arguments[0].Export().(string)
- val := call.Arguments[1].Export()
-
- value, err := json.Marshal(val)
- if err != nil {
- logging.WithError(err).Debug("unable to marshal")
- panic(err)
- }
-
- user.Metadatas = append(user.Metadatas,
- &domain.Metadata{
- Key: key,
- Value: value,
- })
- return nil
- }),
+ actions.SetFields("appendMetadata", metadataList.AppendMetadataFunc),
),
),
)
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("authRequest", object.AuthRequestField(req)),
+ ),
+ )
+
+ ctxFields := actions.SetContextFields(ctxFieldOptions...)
+
err = actions.Run(
actionCtx,
ctxFields,
@@ -122,22 +96,78 @@ func (l *Login) customExternalUserMapping(ctx context.Context, user *domain.Exte
return nil, err
}
}
+ user.Metadatas = object.MetadataListToDomain(metadataList)
return user, err
}
-func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *domain.Human, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, metadata []*domain.Metadata, resourceOwner string) (*domain.Human, []*domain.Metadata, error) {
- triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePreCreation, resourceOwner, false)
+type authMethod string
+
+const (
+ authMethodPassword authMethod = "password"
+ authMethodOTP authMethod = "OTP"
+ authMethodU2F authMethod = "U2F"
+ authMethodPasswordless authMethod = "passwordless"
+)
+
+func (l *Login) triggerPostLocalAuthentication(ctx context.Context, req *domain.AuthRequest, authMethod authMethod, authenticationError error) ([]*domain.Metadata, error) {
+ resourceOwner := req.RequestedOrgID
+ if resourceOwner == "" {
+ resourceOwner = req.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", &metadataList.Metadata),
+ 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(req)),
+ ),
+ )
+
+ err = actions.Run(
+ actionCtx,
+ ctxFields,
+ apiFields,
+ a.Script,
+ a.Name,
+ append(actions.ActionToOptions(a), actions.WithHTTP(actionCtx), actions.WithLogger(actions.ServerLog))...,
+ )
+ cancel()
+ if err != nil {
+ return nil, err
+ }
+ }
+ return object.MetadataListToDomain(metadataList), err
+}
+
+func (l *Login) customUserToLoginUserMapping(ctx context.Context, authRequest *domain.AuthRequest, user *domain.Human, metadata []*domain.Metadata, resourceOwner string, flowType domain.FlowType) (*domain.Human, []*domain.Metadata, error) {
+ triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, flowType, domain.TriggerTypePreCreation, resourceOwner, false)
if err != nil {
return nil, nil, err
}
- ctxOpts := actions.SetContextFields(
- actions.SetFields("v1",
- actions.SetFields("user", func(c *actions.FieldConfig) interface{} {
- return object.UserFromHuman(c, user)
- }),
- ),
- )
+ metadataList := object.MetadataListFromDomain(metadata)
apiFields := actions.WithAPIFields(
actions.SetFields("setFirstName", func(firstName string) {
user.FirstName = firstName
@@ -184,35 +214,26 @@ func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *
}
user.Phone.IsPhoneVerified = verified
}),
- actions.SetFields("metadata", metadata),
+ actions.SetFields("metadata", &metadataList.Metadata),
actions.SetFields("v1",
actions.SetFields("user",
- actions.SetFields("appendMetadata", func(call goja.FunctionCall) goja.Value {
- if len(call.Arguments) != 2 {
- panic("exactly 2 (key, value) arguments expected")
- }
- key := call.Arguments[0].Export().(string)
- val := call.Arguments[1].Export()
-
- value, err := json.Marshal(val)
- if err != nil {
- logging.WithError(err).Debug("unable to marshal")
- panic(err)
- }
-
- metadata = append(metadata,
- &domain.Metadata{
- Key: key,
- Value: value,
- })
- return nil
- }),
+ 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)),
+ ),
+ )
+
err = actions.Run(
actionCtx,
ctxOpts,
@@ -226,57 +247,21 @@ func (l *Login) customExternalUserToLoginUserMapping(ctx context.Context, user *
return nil, nil, err
}
}
- return user, metadata, err
+ return user, object.MetadataListToDomain(metadataList), err
}
-func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.Tokens, req *domain.AuthRequest, config *iam_model.IDPConfigView, resourceOwner string) ([]*domain.UserGrant, error) {
- triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostCreation, resourceOwner, false)
+func (l *Login) customGrants(ctx context.Context, userID string, authRequest *domain.AuthRequest, resourceOwner string, flowType domain.FlowType) ([]*domain.UserGrant, error) {
+ triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, flowType, domain.TriggerTypePostCreation, resourceOwner, false)
if err != nil {
return nil, err
}
- actionUserGrants := make([]actions.UserGrant, 0)
+ mutableUserGrants := &object.UserGrants{UserGrants: make([]object.UserGrant, 0)}
apiFields := actions.WithAPIFields(
- actions.SetFields("userGrants", &actionUserGrants),
+ actions.SetFields("userGrants", &mutableUserGrants.UserGrants),
actions.SetFields("v1",
- actions.SetFields("appendUserGrant", func(c *actions.FieldConfig) interface{} {
- return func(call goja.FunctionCall) goja.Value {
- if len(call.Arguments) != 1 {
- panic("exactly one argument expected")
- }
- object := call.Arguments[0].ToObject(c.Runtime)
- if object == nil {
- panic("unable to unmarshal arg")
- }
- grant := actions.UserGrant{}
-
- for _, key := range object.Keys() {
- switch key {
- case "projectId":
- grant.ProjectID = object.Get(key).String()
- case "projectGrantId":
- grant.ProjectGrantID = object.Get(key).String()
- case "roles":
- if roles, ok := object.Get(key).Export().([]interface{}); ok {
- for _, role := range roles {
- if r, ok := role.(string); ok {
- grant.Roles = append(grant.Roles, r)
- }
- }
- }
- }
- }
-
- if grant.ProjectID == "" {
- panic("projectId not set")
- }
-
- actionUserGrants = append(actionUserGrants, grant)
-
- return nil
- }
- }),
+ actions.SetFields("appendUserGrant", object.AppendGrantFunc(mutableUserGrants)),
),
)
@@ -287,13 +272,14 @@ func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.To
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)
+ user, err := l.query.GetUserByID(actionCtx, true, authRequest.UserID, false)
if err != nil {
panic(err)
}
return object.UserFromQuery(c, user)
}
}),
+ actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
),
)
@@ -310,21 +296,22 @@ func (l *Login) customGrants(ctx context.Context, userID string, tokens *oidc.To
return nil, err
}
}
- return actionUserGrantsToDomain(userID, actionUserGrants), err
+ return object.UserGrantsToDomain(userID, mutableUserGrants.UserGrants), err
}
-func actionUserGrantsToDomain(userID string, actionUserGrants []actions.UserGrant) []*domain.UserGrant {
- if actionUserGrants == nil {
- return nil
+func tokenCtxFields(tokens *oidc.Tokens) []actions.FieldOption {
+ return []actions.FieldOption{
+ actions.SetFields("accessToken", tokens.AccessToken),
+ actions.SetFields("idToken", tokens.IDToken),
+ actions.SetFields("getClaim", func(claim string) interface{} {
+ return tokens.IDTokenClaims.GetClaim(claim)
+ }),
+ actions.SetFields("claimsJSON", func() (string, error) {
+ c, err := json.Marshal(tokens.IDTokenClaims)
+ if err != nil {
+ return "", err
+ }
+ return string(c), nil
+ }),
}
- userGrants := make([]*domain.UserGrant, len(actionUserGrants))
- for i, grant := range actionUserGrants {
- userGrants[i] = &domain.UserGrant{
- UserID: userID,
- ProjectID: grant.ProjectID,
- ProjectGrantID: grant.ProjectGrantID,
- RoleKeys: grant.Roles,
- }
- }
- return userGrants
}
diff --git a/internal/api/ui/login/external_login_handler.go b/internal/api/ui/login/external_login_handler.go
index ecd7259483..a51d23f91c 100644
--- a/internal/api/ui/login/external_login_handler.go
+++ b/internal/api/ui/login/external_login_handler.go
@@ -383,7 +383,7 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, linkingUser, idpConfig)
- user, metadata, err = l.customExternalUserToLoginUserMapping(r.Context(), user, nil, authReq, idpConfig, metadata, resourceOwner)
+ user, metadata, err = l.customUserToLoginUserMapping(r.Context(), authReq, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
if err != nil {
l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err)
return
@@ -398,7 +398,7 @@ func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authR
l.renderError(w, r, authReq, err)
return
}
- userGrants, err := l.customGrants(r.Context(), authReq.UserID, nil, authReq, idpConfig, resourceOwner)
+ userGrants, err := l.customGrants(r.Context(), authReq.UserID, authReq, resourceOwner, domain.FlowTypeExternalAuthentication)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/external_register_handler.go b/internal/api/ui/login/external_register_handler.go
index 72acbc0b44..092750e409 100644
--- a/internal/api/ui/login/external_register_handler.go
+++ b/internal/api/ui/login/external_register_handler.go
@@ -161,7 +161,7 @@ func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, aut
return
}
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, externalUser, idpConfig)
- user, metadata, err = l.customExternalUserToLoginUserMapping(r.Context(), user, nil, authReq, idpConfig, metadata, resourceOwner)
+ user, metadata, err = l.customUserToLoginUserMapping(r.Context(), authReq, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
if err != nil {
l.renderRegisterOption(w, r, authReq, err)
return
@@ -177,7 +177,7 @@ func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, aut
l.renderError(w, r, authReq, err)
return
}
- userGrants, err := l.customGrants(r.Context(), authReq.UserID, nil, authReq, idpConfig, resourceOwner)
+ userGrants, err := l.customGrants(r.Context(), authReq.UserID, authReq, resourceOwner, domain.FlowTypeExternalAuthentication)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/jwt_handler.go b/internal/api/ui/login/jwt_handler.go
index 69593f6d24..9fa96b912a 100644
--- a/internal/api/ui/login/jwt_handler.go
+++ b/internal/api/ui/login/jwt_handler.go
@@ -129,7 +129,7 @@ func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request
}
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
- user, metadata, err = l.customExternalUserToLoginUserMapping(r.Context(), user, tokens, authReq, idpConfig, metadata, resourceOwner)
+ user, metadata, err = l.customUserToLoginUserMapping(r.Context(), authReq, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
if err != nil {
l.renderError(w, r, authReq, err)
return
@@ -144,7 +144,7 @@ func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request
l.renderError(w, r, authReq, err)
return
}
- userGrants, err := l.customGrants(r.Context(), authReq.UserID, tokens, authReq, idpConfig, resourceOwner)
+ userGrants, err := l.customGrants(r.Context(), authReq.UserID, authReq, resourceOwner, domain.FlowTypeExternalAuthentication)
if err != nil {
l.renderError(w, r, authReq, err)
return
diff --git a/internal/api/ui/login/login.go b/internal/api/ui/login/login.go
index d8e02a7892..f28c0072c0 100644
--- a/internal/api/ui/login/login.go
+++ b/internal/api/ui/login/login.go
@@ -73,7 +73,6 @@ func CreateLogin(config Config,
idpConfigAlg crypto.EncryptionAlgorithm,
csrfCookieKey []byte,
) (*Login, error) {
-
login := &Login{
oidcAuthCallbackURL: oidcAuthCallbackURL,
samlAuthCallbackURL: samlAuthCallbackURL,
diff --git a/internal/api/ui/login/mfa_verify_handler.go b/internal/api/ui/login/mfa_verify_handler.go
index 7640b9cade..afc1bb7433 100644
--- a/internal/api/ui/login/mfa_verify_handler.go
+++ b/internal/api/ui/login/mfa_verify_handler.go
@@ -36,6 +36,14 @@ func (l *Login) handleMFAVerify(w http.ResponseWriter, r *http.Request) {
if data.MFAType == domain.MFATypeOTP {
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyMFAOTP(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Code, userAgentID, domain.BrowserInfoFromRequest(r))
+
+ metadata, actionErr := l.triggerPostLocalAuthentication(r.Context(), authReq, authMethodOTP, err)
+ if err == nil && actionErr == nil && len(metadata) > 0 {
+ _, err = l.command.BulkSetUserMetadata(r.Context(), authReq.UserID, authReq.UserOrgID, metadata...)
+ } else if actionErr != nil && err == nil {
+ err = actionErr
+ }
+
if err != nil {
l.renderMFAVerifySelected(w, r, authReq, step, domain.MFATypeOTP, err)
return
diff --git a/internal/api/ui/login/mfa_verify_u2f_handler.go b/internal/api/ui/login/mfa_verify_u2f_handler.go
index eb5f4a9e77..d489845bc4 100644
--- a/internal/api/ui/login/mfa_verify_u2f_handler.go
+++ b/internal/api/ui/login/mfa_verify_u2f_handler.go
@@ -71,6 +71,14 @@ func (l *Login) handleU2FVerification(w http.ResponseWriter, r *http.Request) {
}
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
err = l.authRepo.VerifyMFAU2F(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, userAgentID, credData, domain.BrowserInfoFromRequest(r))
+
+ metadata, actionErr := l.triggerPostLocalAuthentication(r.Context(), authReq, authMethodU2F, err)
+ if err == nil && actionErr == nil && len(metadata) > 0 {
+ _, err = l.command.BulkSetUserMetadata(r.Context(), authReq.UserID, authReq.UserOrgID, metadata...)
+ } else if actionErr != nil && err == nil {
+ err = actionErr
+ }
+
if err != nil {
l.renderU2FVerification(w, r, authReq, step.MFAProviders, err)
return
diff --git a/internal/api/ui/login/password_handler.go b/internal/api/ui/login/password_handler.go
index fa9a884443..c6fb9bf7f7 100644
--- a/internal/api/ui/login/password_handler.go
+++ b/internal/api/ui/login/password_handler.go
@@ -39,6 +39,14 @@ func (l *Login) handlePasswordCheck(w http.ResponseWriter, r *http.Request) {
return
}
err = l.authRepo.VerifyPassword(setContext(r.Context(), authReq.UserOrgID), authReq.ID, authReq.UserID, authReq.UserOrgID, data.Password, authReq.AgentID, domain.BrowserInfoFromRequest(r))
+
+ metadata, actionErr := l.triggerPostLocalAuthentication(r.Context(), authReq, authMethodPassword, err)
+ if err == nil && actionErr == nil && len(metadata) > 0 {
+ _, err = l.command.BulkSetUserMetadata(r.Context(), authReq.UserID, authReq.UserOrgID, metadata...)
+ } else if actionErr != nil && err == nil {
+ err = actionErr
+ }
+
if err != nil {
if authReq.LoginPolicy.IgnoreUnknownUsernames {
l.renderLogin(w, r, authReq, err)
diff --git a/internal/api/ui/login/passwordless_login_handler.go b/internal/api/ui/login/passwordless_login_handler.go
index d199faa2ab..c3ad7b3cf6 100644
--- a/internal/api/ui/login/passwordless_login_handler.go
+++ b/internal/api/ui/login/passwordless_login_handler.go
@@ -63,6 +63,14 @@ func (l *Login) handlePasswordlessVerification(w http.ResponseWriter, r *http.Re
return
}
err = l.authRepo.VerifyPasswordless(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, authReq.ID, authReq.AgentID, credData, domain.BrowserInfoFromRequest(r))
+
+ metadata, actionErr := l.triggerPostLocalAuthentication(r.Context(), authReq, authMethodPasswordless, err)
+ if err == nil && actionErr == nil && len(metadata) > 0 {
+ _, err = l.command.BulkSetUserMetadata(r.Context(), authReq.UserID, authReq.UserOrgID, metadata...)
+ } else if actionErr != nil && err == nil {
+ err = actionErr
+ }
+
if err != nil {
l.renderPasswordlessVerification(w, r, authReq, formData.PasswordLogin, err)
return
diff --git a/internal/api/ui/login/register_handler.go b/internal/api/ui/login/register_handler.go
index 273f618133..1e8fe6327a 100644
--- a/internal/api/ui/login/register_handler.go
+++ b/internal/api/ui/login/register_handler.go
@@ -82,11 +82,48 @@ func (l *Login) handleRegisterCheck(w http.ResponseWriter, r *http.Request) {
l.renderRegister(w, r, authRequest, data, err)
return
}
- user, err := l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, data.toHumanDomain(), nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
+
+ // For consistency with the external authentication flow,
+ // the setMetadata() function is provided on the pre creation hook, for now,
+ // like for the ExternalAuthentication flow.
+ // If there is a need for additional context after registration,
+ // we could provide that method in the PostCreation trigger too,
+ // without breaking existing actions.
+ // Also, if that field is needed, we probably also should provide it
+ // for ExternalAuthentication.
+ user, metadatas, err := l.customUserToLoginUserMapping(r.Context(), authRequest, data.toHumanDomain(), make([]*domain.Metadata, 0), resourceOwner, domain.FlowTypeInternalAuthentication)
if err != nil {
l.renderRegister(w, r, authRequest, data, err)
return
}
+
+ user, err = l.command.RegisterHuman(setContext(r.Context(), resourceOwner), resourceOwner, user, nil, nil, initCodeGenerator, emailCodeGenerator, phoneCodeGenerator)
+ if err != nil {
+ l.renderRegister(w, r, authRequest, data, err)
+ return
+ }
+
+ if len(metadatas) > 0 {
+ _, err = l.command.BulkSetUserMetadata(r.Context(), user.AggregateID, resourceOwner, metadatas...)
+ if err != nil {
+ // TODO: What if action is configured to be allowed to fail? Same question for external registration.
+ l.renderRegister(w, r, authRequest, data, err)
+ return
+ }
+ }
+
+ userGrants, err := l.customGrants(r.Context(), user.AggregateID, authRequest, resourceOwner, domain.FlowTypeInternalAuthentication)
+ if err != nil {
+ l.renderError(w, r, authRequest, err)
+ return
+ }
+
+ err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
+ if err != nil {
+ l.renderError(w, r, authRequest, err)
+ return
+ }
+
if authRequest == nil {
l.defaultRedirect(w, r)
return
diff --git a/internal/command/instance_member.go b/internal/command/instance_member.go
index e7d70f8dce..b7d78a939a 100644
--- a/internal/command/instance_member.go
+++ b/internal/command/instance_member.go
@@ -126,7 +126,8 @@ func (c *Commands) RemoveInstanceMember(ctx context.Context, userID string) (*do
return nil, err
}
if errors.IsNotFound(err) {
- return nil, nil
+ // empty response because we have no data that match the request
+ return &domain.ObjectDetails{}, nil
}
instanceAgg := InstanceAggregateFromWriteModel(&memberWriteModel.MemberWriteModel.WriteModel)
diff --git a/internal/command/instance_member_test.go b/internal/command/instance_member_test.go
index 1aede93722..5307cbd39e 100644
--- a/internal/command/instance_member_test.go
+++ b/internal/command/instance_member_test.go
@@ -480,7 +480,7 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
},
},
{
- name: "member not existing, nil result",
+ name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
@@ -492,7 +492,7 @@ func TestCommandSide_RemoveIAMMember(t *testing.T) {
userID: "user1",
},
res: res{
- want: nil,
+ want: &domain.ObjectDetails{},
},
},
{
diff --git a/internal/command/org_member.go b/internal/command/org_member.go
index 68e3e5cace..3ead7a0685 100644
--- a/internal/command/org_member.go
+++ b/internal/command/org_member.go
@@ -145,7 +145,8 @@ func (c *Commands) RemoveOrgMember(ctx context.Context, orgID, userID string) (*
return nil, err
}
if errors.IsNotFound(err) {
- return nil, nil
+ // empty response because we have no data that match the request
+ return &domain.ObjectDetails{}, nil
}
orgAgg := OrgAggregateFromWriteModel(&m.MemberWriteModel.WriteModel)
diff --git a/internal/command/org_member_test.go b/internal/command/org_member_test.go
index 8a63bb5b40..e056d7bc16 100644
--- a/internal/command/org_member_test.go
+++ b/internal/command/org_member_test.go
@@ -784,7 +784,7 @@ func TestCommandSide_RemoveOrgMember(t *testing.T) {
},
},
{
- name: "member not existing, nil result",
+ name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
@@ -798,7 +798,7 @@ func TestCommandSide_RemoveOrgMember(t *testing.T) {
resourceOwner: "org1",
},
res: res{
- want: nil,
+ want: &domain.ObjectDetails{},
},
},
{
diff --git a/internal/command/project_member.go b/internal/command/project_member.go
index 4bed40030c..ed9f35146f 100644
--- a/internal/command/project_member.go
+++ b/internal/command/project_member.go
@@ -94,7 +94,8 @@ func (c *Commands) RemoveProjectMember(ctx context.Context, projectID, userID, r
return nil, err
}
if errors.IsNotFound(err) {
- return nil, nil
+ // empty response because we have no data that match the request
+ return &domain.ObjectDetails{}, nil
}
projectAgg := ProjectAggregateFromWriteModel(&m.MemberWriteModel.WriteModel)
diff --git a/internal/command/project_member_test.go b/internal/command/project_member_test.go
index 89ee1301fa..db46c7a7b9 100644
--- a/internal/command/project_member_test.go
+++ b/internal/command/project_member_test.go
@@ -551,7 +551,7 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
},
},
{
- name: "member not existing, nil result",
+ name: "member not existing, empty object details result",
fields: fields{
eventstore: eventstoreExpect(
t,
@@ -565,7 +565,7 @@ func TestCommandSide_RemoveProjectMember(t *testing.T) {
resourceOwner: "org1",
},
res: res{
- want: nil,
+ want: &domain.ObjectDetails{},
},
},
{
diff --git a/internal/domain/flow.go b/internal/domain/flow.go
index 396977e60f..c54436af1a 100644
--- a/internal/domain/flow.go
+++ b/internal/domain/flow.go
@@ -20,6 +20,7 @@ const (
FlowTypeUnspecified FlowType = iota
FlowTypeExternalAuthentication
FlowTypeCustomiseToken
+ FlowTypeInternalAuthentication
flowTypeCount
)
@@ -49,6 +50,12 @@ func (s FlowType) TriggerTypes() []TriggerType {
TriggerTypePreUserinfoCreation,
TriggerTypePreAccessTokenCreation,
}
+ case FlowTypeInternalAuthentication:
+ return []TriggerType{
+ TriggerTypePostAuthentication,
+ TriggerTypePreCreation,
+ TriggerTypePostCreation,
+ }
default:
return nil
}
@@ -71,6 +78,8 @@ func (s FlowType) LocalizationKey() string {
return "Action.Flow.Type.ExternalAuthentication"
case FlowTypeCustomiseToken:
return "Action.Flow.Type.CustomiseToken"
+ case FlowTypeInternalAuthentication:
+ return "Action.Flow.Type.InternalAuthentication"
default:
return "Action.Flow.Type.Unspecified"
}
diff --git a/internal/domain/request.go b/internal/domain/request.go
index 21806a881e..d76ab2cf4f 100644
--- a/internal/domain/request.go
+++ b/internal/domain/request.go
@@ -42,7 +42,6 @@ func (a *AuthRequestOIDC) IsValid() bool {
type AuthRequestSAML struct {
ID string
- RequestID string
BindingType string
Code string
Issuer string
diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml
index 66792b2aae..c3d36778e9 100644
--- a/internal/static/i18n/de.yaml
+++ b/internal/static/i18n/de.yaml
@@ -1117,6 +1117,7 @@ Action:
Unspecified: Unspezifiziert
ExternalAuthentication: Externe Authentifizierung
CustomiseToken: Token ergänzen
+ InternalAuthentication: Interne Authentifizierung
TriggerType:
Unspecified: Unspezifiziert
PostAuthentication: Nach Authentifizierung
diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml
index 7b1e91349c..5999049c68 100644
--- a/internal/static/i18n/en.yaml
+++ b/internal/static/i18n/en.yaml
@@ -1117,6 +1117,7 @@ Action:
Unspecified: Unspecified
ExternalAuthentication: External Authentication
CustomiseToken: Complement Token
+ InternalAuthentication: Internal Authentication
TriggerType:
Unspecified: Unspecified
PostAuthentication: Post Authentication
diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml
index 5f6686d431..2b93209a0d 100644
--- a/internal/static/i18n/fr.yaml
+++ b/internal/static/i18n/fr.yaml
@@ -946,6 +946,7 @@ Action:
Unspecified: Non spécifié
ExternalAuthentication: Authentification externe
CustomiseToken: Compléter Token
+ InternalAuthentication: Authentification interne
TriggerType:
Unspecified: Non spécifié
PostAuthentication: Authentification postérieure
diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml
index 5ef51aac93..ba5f65278a 100644
--- a/internal/static/i18n/it.yaml
+++ b/internal/static/i18n/it.yaml
@@ -946,6 +946,7 @@ Action:
Unspecified: Non specificato
ExternalAuthentication: Autenticazione esterna
CustomiseToken: Completare Token
+ InternalAuthentication: Autenticazione interna
TriggerType:
Unspecified: Non specificato
PostAuthentication: Post-autenticazione
diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml
index cfa6495861..3130be3639 100644
--- a/internal/static/i18n/zh.yaml
+++ b/internal/static/i18n/zh.yaml
@@ -936,6 +936,7 @@ Action:
Unspecified: 未指定的
ExternalAuthentication: 外部认证
CustomiseToken: 自定义令牌
+ InternalAuthentication: 内部认证
TriggerType:
Unspecified: 未指定的
PostAuthentication: 后期认证