diff --git a/internal/query/app.go b/internal/query/app.go index adf676bc38..b826bb52b8 100644 --- a/internal/query/app.go +++ b/internal/query/app.go @@ -481,7 +481,7 @@ func (q *Queries) SearchApps(ctx context.Context, queries *AppSearchQueries, wit return err }, stmt, args...) if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") + return nil, zerrors.ThrowInternal(err, "QUERY-h9TeF", "Errors.Internal") } apps.State, err = q.latestState(ctx, appsTable) return apps, err diff --git a/internal/query/sms.go b/internal/query/sms.go index ef4d1cfca2..310d3d0f14 100644 --- a/internal/query/sms.go +++ b/internal/query/sms.go @@ -207,7 +207,7 @@ func (q *Queries) SearchSMSConfigs(ctx context.Context, queries *SMSConfigsSearc return err }, stmt, args...) if err != nil { - return nil, zerrors.ThrowInternal(err, "QUERY-aJnZL", "Errors.Internal") + return nil, zerrors.ThrowInternal(err, "QUERY-l4bxm", "Errors.Internal") } configs.State, err = q.latestState(ctx, smsConfigsTable) return configs, err diff --git a/load-test/Makefile b/load-test/Makefile index 3fece26aa3..5c515c70b3 100644 --- a/load-test/Makefile +++ b/load-test/Makefile @@ -33,9 +33,21 @@ introspect: ensure_modules bundle cd ../../xk6-modules && xk6 build --with xk6-zitadel=. ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/introspection.js --vus ${VUS} --duration ${DURATION} --out csv=output/introspect_${DATE}.csv +.PHONY: oidc_session +oidc_session: ensure_key_pair ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/oidc_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/oidc_session_${DATE}.csv + +.PHONY: otp_session +otp_session: ensure_key_pair ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/otp_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/otp_session_${DATE}.csv + +.PHONY: password_session +password_session: ensure_key_pair ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/password_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/otp_session_${DATE}.csv + .PHONY: add_session -add_session: bundle - ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/session.js --vus ${VUS} --duration ${DURATION} --out csv=output/add_session_${DATE}.csv +add_session: ensure_modules bundle + ${K6} run --summary-trend-stats "min,avg,max,p(50),p(95),p(99)" dist/add_session.js --vus ${VUS} --duration ${DURATION} --out csv=output/add_session_${DATE}.csv .PHONY: machine_jwt_profile_grant machine_jwt_profile_grant: ensure_modules ensure_key_pair bundle diff --git a/load-test/README.md b/load-test/README.md index b4b2a6cae9..f161fac6de 100644 --- a/load-test/README.md +++ b/load-test/README.md @@ -20,6 +20,8 @@ The use cases under tests are defined in `src/use_cases`. The implementation of - `VUS`: Amount of parallel processes execute the test (default is 20) - `DURATION`: Defines how long the tests are executed (default is `200s`) - `ZITADEL_HOST`: URL of ZITADEL (default is `http://localhost:8080`) +- `ADMIN_LOGIN_NAME`: Loginanme of a human user with `IAM_OWNER`-role +- `ADMIN_PASSWORD`: password of the human user To setup the tests we use the credentials of console and log in using an admin. The user must be able to create organizations and all resources inside organizations. @@ -50,6 +52,15 @@ Before you run the tests you need an initialized user. The tests don't implement * `make add_session` setup: creates human users test: creates new sessions with user id check +* `make oidc_session` + setup: creates a machine user to create the auth request and session. + test: creates an auth request, a session and links the session to the auth request. Implementation of [this flow](https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard). +* `make otp_session` + setup: creates 1 human user for each VU and adds email OTP to it + test: creates a session based on the login name of the user, sets the email OTP challenge to the session and afterwards checks the OTP code +* `make password_session` + setup: creates 1 human user for each VU and adds email OTP to it + test: creates a session based on the login name of the user and checks for the password on a second step * `make machine_jwt_profile_grant` setup: generates private/public key, creates machine users, adds a key test: creates a token and calls user info diff --git a/load-test/package.json b/load-test/package.json index 1ed3c494bb..73bf8fd449 100644 --- a/load-test/package.json +++ b/load-test/package.json @@ -27,7 +27,7 @@ }, "scripts": { "bundle": "webpack", - "lint": "prettier --check src", + "lint": "prettier --check src/**", "lint:fix": "prettier --write src" } } diff --git a/load-test/src/login_ui.ts b/load-test/src/login_ui.ts index 5ea4afcdf5..3265dd1bf5 100644 --- a/load-test/src/login_ui.ts +++ b/load-test/src/login_ui.ts @@ -21,10 +21,27 @@ export function loginByUsernamePassword(user: User) { } const initLoginTrend = new Trend('login_ui_init_login_duration', true); -function initLogin(): Response { - const response = http.get(url('/oauth/v2/authorize', { searchParams: Client() })); +export function initLogin(clientId?: string): Response { + let params = {}; + let expectedStatus = 200; + if (clientId) { + params = { + headers: { + 'x-zitadel-login-client': clientId, + }, + redirects: 0, + }; + expectedStatus = 302; + } + + const response = http.get( + url('/oauth/v2/authorize', { + searchParams: Client(), + }), + params, + ); check(response, { - 'authorize status ok': (r) => r.status == 200 || fail(`init login failed: ${r}`), + 'authorize status ok': (r) => r.status == expectedStatus || fail(`init login failed: ${JSON.stringify(r)}`), }); initLoginTrend.add(response.timings.duration); return response; diff --git a/load-test/src/membership.ts b/load-test/src/membership.ts new file mode 100644 index 0000000000..874d7dfa2d --- /dev/null +++ b/load-test/src/membership.ts @@ -0,0 +1,25 @@ +import http from 'k6/http'; +import { Trend } from 'k6/metrics'; +import url from './url'; +import { check, fail } from 'k6'; + +const addIAMMemberTrend = new Trend('membership_iam_member', true); +export async function addIAMMember(userId: string, roles: string[], accessToken: string): Promise { + const res = await http.post( + url('/admin/v1/members'), + JSON.stringify({ + userId: userId, + roles: roles, + }), + { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }, + ); + check(res, { + 'member added successful': (r) => r.status == 200 || fail(`unable add member: ${JSON.stringify(res)}`), + }); + addIAMMemberTrend.add(res.timings.duration); +} diff --git a/load-test/src/oidc.ts b/load-test/src/oidc.ts index a7ebce7dc3..edd451ebd6 100644 --- a/load-test/src/oidc.ts +++ b/load-test/src/oidc.ts @@ -1,9 +1,9 @@ import { JSONObject, check, fail } from 'k6'; import encoding from 'k6/encoding'; -import http, { RequestBody } from 'k6/http'; +import http, { RequestBody, Response } from 'k6/http'; import { Trend } from 'k6/metrics'; import url from './url'; -import { Config } from './config'; +import { Client, Config } from './config'; // @ts-ignore Import module import zitadel from 'k6/x/zitadel'; @@ -79,9 +79,11 @@ export function introspect(jwt: string, token: string) { const clientCredentialsTrend = new Trend('oidc_client_credentials_duration', true); export function clientCredentials(clientId: string, clientSecret: string): Promise { return new Promise((resolve, reject) => { - const response = http.asyncRequest('POST', configuration().token_endpoint, + const response = http.asyncRequest( + 'POST', + configuration().token_endpoint, { - grant_type: "client_credentials", + grant_type: 'client_credentials', scope: 'openid profile urn:zitadel:iam:org:project:id:zitadel:aud', client_id: clientId, client_secret: clientSecret, @@ -91,26 +93,26 @@ export function clientCredentials(clientId: string, clientSecret: string): Promi 'Content-Type': 'application/x-www-form-urlencoded', }, }, - ); + ); response.then((res) => { check(res, { 'client credentials status ok': (r) => r.status === 200, }) || reject(`client credentials request failed (client id: ${clientId}) status: ${res.status} body: ${res.body}`); clientCredentialsTrend.add(res.timings.duration); - const tokens = new Tokens(res.json() as JSONObject) + const tokens = new Tokens(res.json() as JSONObject); check(tokens, { 'client credentials token ok': (t) => t.accessToken !== undefined, }) || reject(`client credentials access token missing (client id: ${clientId}`); - resolve(tokens) + resolve(tokens); }); }); } export interface TokenRequest { payload(): RequestBody; - headers(): { [name: string]: string; }; + headers(): { [name: string]: string }; } const privateKey = open('../.keys/key.pem'); @@ -126,46 +128,83 @@ export class JWTProfileRequest implements TokenRequest { this.keyPayload = { userId: userId, // 1 minute - expiration: 60*1_000_000_000, + expiration: 60 * 1_000_000_000, keyId: keyId, }; } - payload(): RequestBody{ - const assertion = zitadel.signJWTProfileAssertion( - this.keyPayload.userId, - this.keyPayload.keyId, - { - audience: [Config.host], - expiration: this.keyPayload.expiration, - key: privateKey - }); + payload(): RequestBody { + const assertion = zitadel.signJWTProfileAssertion(this.keyPayload.userId, this.keyPayload.keyId, { + audience: [Config.host], + expiration: this.keyPayload.expiration, + key: privateKey, + }); return { - 'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer', - scope: 'openid', - assertion: `${assertion}` + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + scope: 'openid urn:zitadel:iam:org:project:id:zitadel:aud', + assertion: `${assertion}`, }; - }; - public headers(): { [name: string]: string; } { + } + public headers(): { [name: string]: string } { return { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', }; - }; + } } const tokenDurationTrend = new Trend('oidc_token_duration', true); export async function token(request: TokenRequest): Promise { - return http.asyncRequest('POST', configuration().token_endpoint, - request.payload(), - { + return http + .asyncRequest('POST', configuration().token_endpoint, request.payload(), { headers: request.headers(), - }, - ).then((res) => { - tokenDurationTrend.add(res.timings.duration); - check(res, { - 'token status ok': (r) => r.status === 200, - 'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '', + }) + .then((res) => { + tokenDurationTrend.add(res.timings.duration); + check(res, { + 'token status ok': (r) => r.status === 200, + 'access token returned': (r) => r.json('access_token')! != undefined && r.json('access_token')! != '', + }); + return new Tokens(res.json() as JSONObject); }); - return new Tokens(res.json() as JSONObject); +} + +const authRequestBiIDTrend = new Trend('oidc_auth_request_by_id_duration', true); +export async function authRequestByID(id: string, tokens: any): Promise { + const response = http.get(url(`/v2/oidc/auth_requests/${id}`), { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, }); -}; \ No newline at end of file + check(response, { + 'authorize status ok': (r) => r.status == 200 || fail(`auth request by failed: ${JSON.stringify(r)}`), + }); + authRequestBiIDTrend.add(response.timings.duration); + return response; +} + +const finalizeAuthRequestTrend = new Trend('oidc_auth_requst_by_id_duration', true); +export async function finalizeAuthRequest(id: string, session: any, tokens: any): Promise { + const res = await http.post( + url(`/v2/oidc/auth_requests/${id}`), + JSON.stringify({ + session: { + sessionId: session.sessionId, + sessionToken: session.sessionToken, + }, + }), + { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + 'Content-Type': 'application/json', + // 'Accept': 'application/json', + 'x-zitadel-login-client': tokens.info.client_id, + }, + }, + ); + check(res, { + 'finalize auth request status ok': (r) => r.status == 200 || fail(`finalize auth request failed: ${JSON.stringify(r)}`), + }); + finalizeAuthRequestTrend.add(res.timings.duration); + + return res; +} diff --git a/load-test/src/session.ts b/load-test/src/session.ts index 28a994c975..213cddabf0 100644 --- a/load-test/src/session.ts +++ b/load-test/src/session.ts @@ -3,33 +3,23 @@ import { Org } from './org'; import http from 'k6/http'; import url from './url'; import { check } from 'k6'; -import { User } from './user'; export type Session = { + challenges: any; id: string; + token: string; }; const addSessionTrend = new Trend('session_add_session_duration', true); -export function createSession(user: User, org: Org, accessToken: string): Promise { +export function createSession(org: Org, accessToken: string, checks?: any): Promise { return new Promise((resolve, reject) => { - let response = http.asyncRequest( - 'POST', - url('/v2beta/sessions'), - JSON.stringify({ - checks: { - user: { - userId: user.userId, - } - } - }), - { - headers: { - authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'x-zitadel-orgid': org.organizationId, - }, + let response = http.asyncRequest('POST', url('/v2/sessions'), checks ? JSON.stringify({ checks: checks }) : null, { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, }, - ); + }); response.then((res) => { check(res, { 'add Session status ok': (r) => r.status === 201, @@ -40,3 +30,29 @@ export function createSession(user: User, org: Org, accessToken: string): Promis }); }); } + +const setSessionTrend = new Trend('session_set_session_duration', true); +export function setSession(id: string, session: any, accessToken: string, challenges?: any, checks?: any): Promise { + const body = { + sessionToken: session.sessionToken, + checks: checks ? checks : null, + challenges: challenges ? challenges : null, + }; + return new Promise((resolve, reject) => { + let response = http.asyncRequest('PATCH', url(`/v2/sessions/${id}`), JSON.stringify(body), { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + // 'x-zitadel-orgid': org.organizationId, + }, + }); + response.then((res) => { + check(res, { + 'set Session status ok': (r) => r.status === 200, + }) || reject(`unable to set Session status: ${res.status} body: ${res.body}`); + + setSessionTrend.add(res.timings.duration); + resolve(res.json() as Session); + }); + }); +} diff --git a/load-test/src/use_cases/machine_client_credentials_login.ts b/load-test/src/use_cases/machine_client_credentials_login.ts index a2cbcca8da..e3cb8e2464 100644 --- a/load-test/src/use_cases/machine_client_credentials_login.ts +++ b/load-test/src/use_cases/machine_client_credentials_login.ts @@ -1,7 +1,7 @@ import { loginByUsernamePassword } from '../login_ui'; import { createOrg, removeOrg } from '../org'; -import {createMachine, User, addMachineSecret} from '../user'; -import {clientCredentials, userinfo} from '../oidc'; +import { createMachine, User, addMachineSecret } from '../user'; +import { clientCredentials, userinfo } from '../oidc'; import { Config, MaxVUs } from '../config'; export async function setup() { @@ -37,10 +37,9 @@ export async function setup() { } export default function (data: any) { - clientCredentials(data.machines[__VU - 1].loginName, data.machines[__VU - 1].password) - .then((token) => { - userinfo(token.accessToken!) - }) + clientCredentials(data.machines[__VU - 1].loginName, data.machines[__VU - 1].password).then((token) => { + userinfo(token.accessToken!); + }); } export function teardown(data: any) { diff --git a/load-test/src/use_cases/machine_jwt_profile_grant.ts b/load-test/src/use_cases/machine_jwt_profile_grant.ts index 2511f9e2a5..b623b60721 100644 --- a/load-test/src/use_cases/machine_jwt_profile_grant.ts +++ b/load-test/src/use_cases/machine_jwt_profile_grant.ts @@ -1,7 +1,7 @@ import { loginByUsernamePassword } from '../login_ui'; import { createOrg, removeOrg } from '../org'; -import {createMachine, User, addMachineKey} from '../user'; -import {JWTProfileRequest, token, userinfo} from '../oidc'; +import { createMachine, User, addMachineKey } from '../user'; +import { JWTProfileRequest, token, userinfo } from '../oidc'; import { Config, MaxVUs } from '../config'; import encoding from 'k6/encoding'; @@ -10,10 +10,10 @@ const publicKey = encoding.b64encode(open('../.keys/key.pem.pub')); export async function setup() { const tokens = loginByUsernamePassword(Config.admin as User); console.info('setup: admin signed in'); - + const org = await createOrg(tokens.accessToken!); console.info(`setup: org (${org.organizationId}) created`); - + let machines = ( await Promise.all( Array.from({ length: MaxVUs() }, (_, i) => { @@ -24,16 +24,11 @@ export async function setup() { return { userId: machine.userId, loginName: machine.loginNames[0] }; }); console.info(`setup: ${machines.length} machines created`); - + let keys = ( await Promise.all( machines.map((machine) => { - return addMachineKey( - machine.userId, - org, - tokens.accessToken!, - publicKey, - ); + return addMachineKey(machine.userId, org, tokens.accessToken!, publicKey); }), ) ).map((key, i) => { @@ -45,7 +40,7 @@ export async function setup() { } export default function (data: any) { - token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId)) + token(new JWTProfileRequest(data.machines[__VU - 1].userId, data.machines[__VU - 1].keyId)); } export function teardown(data: any) { diff --git a/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts b/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts index 95437b0c97..61f6ecb238 100644 --- a/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts +++ b/load-test/src/use_cases/machine_jwt_profile_grant_single_user.ts @@ -1,7 +1,7 @@ import { loginByUsernamePassword } from '../login_ui'; import { createOrg, removeOrg } from '../org'; -import {createMachine, User, addMachineKey} from '../user'; -import {JWTProfileRequest, token, userinfo} from '../oidc'; +import { createMachine, User, addMachineKey } from '../user'; +import { JWTProfileRequest, token, userinfo } from '../oidc'; import { Config } from '../config'; import encoding from 'k6/encoding'; @@ -10,20 +10,20 @@ const publicKey = encoding.b64encode(open('../.keys/key.pem.pub')); export async function setup() { const tokens = loginByUsernamePassword(Config.admin as User); console.info('setup: admin signed in'); - + const org = await createOrg(tokens.accessToken!); console.info(`setup: org (${org.organizationId}) created`); - + const machine = await createMachine(`zitachine`, org, tokens.accessToken!); console.info(`setup: machine ${machine.userId} created`); const key = await addMachineKey(machine.userId, org, tokens.accessToken!, publicKey); console.info(`setup: key ${key.keyId} added`); - return { tokens, machine: {userId: machine.userId, keyId: key.keyId}, org }; + return { tokens, machine: { userId: machine.userId, keyId: key.keyId }, org }; } export default function (data: any) { - token(new JWTProfileRequest(data.machine.userId, data.machine.keyId)) + token(new JWTProfileRequest(data.machine.userId, data.machine.keyId)); } export function teardown(data: any) { diff --git a/load-test/src/use_cases/session.ts b/load-test/src/use_cases/session/add_session.ts similarity index 70% rename from load-test/src/use_cases/session.ts rename to load-test/src/use_cases/session/add_session.ts index d9cdfbd3b7..808bbf03cd 100644 --- a/load-test/src/use_cases/session.ts +++ b/load-test/src/use_cases/session/add_session.ts @@ -1,9 +1,9 @@ -import { loginByUsernamePassword } from '../login_ui'; -import { createOrg, removeOrg } from '../org'; -import { User, createHuman } from '../user'; +import { loginByUsernamePassword } from '../../login_ui'; +import { createOrg, removeOrg } from '../../org'; +import { User, createHuman } from '../../user'; import { Trend } from 'k6/metrics'; -import { Config, MaxVUs } from '../config'; -import { createSession } from '../session'; +import { Config, MaxVUs } from '../../config'; +import { createSession } from '../../session'; import { check } from 'k6'; export async function setup() { @@ -27,12 +27,16 @@ export async function setup() { const addSessionTrend = new Trend('add_session_duration', true); export default async function (data: any) { const start = new Date(); - const session = await createSession(data.users[__VU - 1], data.org, data.tokens.accessToken); + const session = await createSession(data.org, data.tokens.accessToken, { + user: { + userId: data.users[__VU - 1].userId, + }, + }); check(session, { - 'add session is status ok': (s) => s.id !== "", + 'add session is status ok': (s) => s.id !== '', }); - + addSessionTrend.add(new Date().getTime() - start.getTime()); } diff --git a/load-test/src/use_cases/session/oidc_session.ts b/load-test/src/use_cases/session/oidc_session.ts new file mode 100644 index 0000000000..9d3087fdef --- /dev/null +++ b/load-test/src/use_cases/session/oidc_session.ts @@ -0,0 +1,53 @@ +import { loginByUsernamePassword, initLogin } from '../../login_ui'; +import { createOrg, removeOrg } from '../../org'; +import { User, addMachineKey, createMachine } from '../../user'; +import { Trend } from 'k6/metrics'; +import { Config } from '../../config'; +import { check } from 'k6'; +import { finalizeAuthRequest, JWTProfileRequest, token } from '../../oidc'; +import { createSession } from '../../session'; +import encoding from 'k6/encoding'; +import { addIAMMember } from '../../membership'; + +const publicKey = encoding.b64encode(open('../.keys/key.pem.pub')); + +export async function setup() { + const adminTokens = loginByUsernamePassword(Config.admin as User); + console.log('setup: admin signed in'); + + const org = await createOrg(adminTokens.accessToken!); + console.log(`setup: org (${org.organizationId}) created`); + + const loginUser = await createMachine('load-test', org, adminTokens.accessToken!); + const loginUserKey = await addMachineKey(loginUser.userId, org, adminTokens.accessToken!, publicKey); + await addIAMMember(loginUser.userId, ['IAM_LOGIN_CLIENT'], adminTokens.accessToken!); + const tokens = await token(new JWTProfileRequest(loginUser.userId, loginUserKey.keyId)); + + return { tokens, user: loginUser, key: loginUserKey, org, adminTokens }; +} + +// implements the flow described in +// https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard +const addSessionTrend = new Trend('oidc_session_duration', true); +export default async function (data: any) { + const start = new Date(); + const authorizeResponse = initLogin(data.tokens.info.client_id); + + const authRequestId = new URLSearchParams(authorizeResponse.headers['Location']).values().next().value; + check(authRequestId, { + 'auth request id returned': (s) => s !== '', + }); + + const session = await createSession(data.org, data.tokens.accessToken, { + user: { + userId: data.user.userId, + }, + }); + await finalizeAuthRequest(authRequestId!, session, data.tokens!); + + addSessionTrend.add(new Date().getTime() - start.getTime()); +} + +export function teardown(data: any) { + removeOrg(data.org, data.adminTokens.accessToken); +} diff --git a/load-test/src/use_cases/session/otp_session.ts b/load-test/src/use_cases/session/otp_session.ts new file mode 100644 index 0000000000..df4ed8e321 --- /dev/null +++ b/load-test/src/use_cases/session/otp_session.ts @@ -0,0 +1,57 @@ +import { loginByUsernamePassword } from '../../login_ui'; +import { createOrg, removeOrg } from '../../org'; +import { createHuman, setEmailOTPOnHuman, User } from '../../user'; +import { Trend } from 'k6/metrics'; +import { Config, MaxVUs } from '../../config'; +import { createSession, setSession } from '../../session'; + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.log('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.log(`setup: org (${org.organizationId}) created`); + + const humanPromises = Array.from({ length: MaxVUs() }, (_, i) => { + return createHuman(`zitizen-${i}`, org, tokens.accessToken!); + }); + + const humans = (await Promise.all(humanPromises)).map((user) => { + setEmailOTPOnHuman(user, org, tokens.accessToken!); + return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' }; + }); + console.log(`setup: ${humans.length} users created`); + + return { tokens, org, users: humans }; +} + +// implements the flow described in +// https://zitadel.com/docs/guides/integrate/login-ui/oidc-standard +const otpSessionTrend = new Trend('otp_session_duration', true); +export default async function (data: any) { + const start = new Date(); + let session = await createSession(data.org, data.tokens.accessToken, { + user: { + loginName: data.users[__VU - 1].loginName, + }, + }); + const sessionId = (session as any).sessionId; + + session = await setSession(sessionId, session, data.tokens.accessToken, { + otpEmail: { + return_code: {}, + }, + }); + + session = await setSession(sessionId, session, data.tokens.accessToken, null, { + otpEmail: { + code: session.challenges.otpEmail, + }, + }); + + otpSessionTrend.add(new Date().getTime() - start.getTime()); +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); +} diff --git a/load-test/src/use_cases/session/password_session.ts b/load-test/src/use_cases/session/password_session.ts new file mode 100644 index 0000000000..2fa5261b0f --- /dev/null +++ b/load-test/src/use_cases/session/password_session.ts @@ -0,0 +1,49 @@ +import { loginByUsernamePassword } from '../../login_ui'; +import { createOrg, removeOrg } from '../../org'; +import { createHuman, setEmailOTPOnHuman, User } from '../../user'; +import { Trend } from 'k6/metrics'; +import { Config, MaxVUs } from '../../config'; +import { createSession, setSession } from '../../session'; + +export async function setup() { + const tokens = loginByUsernamePassword(Config.admin as User); + console.log('setup: admin signed in'); + + const org = await createOrg(tokens.accessToken!); + console.log(`setup: org (${org.organizationId}) created`); + + const humanPromises = Array.from({ length: MaxVUs() }, (_, i) => { + return createHuman(`zitizen-${i}`, org, tokens.accessToken!); + }); + + const humans = (await Promise.all(humanPromises)).map((user) => { + setEmailOTPOnHuman(user, org, tokens.accessToken!); + return { userId: user.userId, loginName: user.loginNames[0], password: 'Password1!' }; + }); + console.log(`setup: ${humans.length} users created`); + + return { tokens, org, users: humans }; +} + +const passwordSessionTrend = new Trend('password_session_duration', true); +export default async function (data: any) { + const start = new Date(); + let session = await createSession(data.org, data.tokens.accessToken, { + user: { + loginName: data.users[__VU - 1].loginName, + }, + }); + const sessionId = (session as any).sessionId; + + session = await setSession(sessionId, session, data.tokens.accessToken, null, { + password: { + password: data.users[__VU - 1].password, + }, + }); + + passwordSessionTrend.add(new Date().getTime() - start.getTime()); +} + +export function teardown(data: any) { + removeOrg(data.org, data.tokens.accessToken); +} diff --git a/load-test/src/user.ts b/load-test/src/user.ts index 6402be2034..86ce71fd9b 100644 --- a/load-test/src/user.ts +++ b/load-test/src/user.ts @@ -19,7 +19,7 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr return new Promise((resolve, reject) => { let response = http.asyncRequest( 'POST', - url('/v2beta/users/human'), + url('/v2/users/human'), JSON.stringify({ username: username, organization: { @@ -69,6 +69,23 @@ export function createHuman(username: string, org: Org, accessToken: string): Pr }); } +const setEmailOTPOnHumanTrend = new Trend('set_human_email_otp_duration', true); +export async function setEmailOTPOnHuman(user: User, org: Org, accessToken: string): Promise { + const response = await http.asyncRequest('POST', url(`/v2/users/${user.userId}/otp_email`), null, { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, + }, + }); + check(response, { + 'set email otp status ok': (r) => r.status === 200, + }); + setEmailOTPOnHumanTrend.add(response.timings.duration); + + return; +} + const updateHumanTrend = new Trend('update_human_duration', true); export function updateHuman( payload: any = {}, @@ -172,8 +189,8 @@ export function addMachinePat(userId: string, org: Org, accessToken: string): Pr } export type MachineSecret = { - clientId: string; - clientSecret: string; + clientId: string; + clientSecret: string; }; const addMachineSecretTrend = new Trend('user_add_machine_secret_duration', true); @@ -204,20 +221,23 @@ export type MachineKey = { const addMachineKeyTrend = new Trend('user_add_machine_key_duration', true); export function addMachineKey(userId: string, org: Org, accessToken: string, publicKey?: string): Promise { return new Promise((resolve, reject) => { - let response = http.asyncRequest('POST', url(`/management/v1/users/${userId}/keys`), - JSON.stringify({ - type: 'KEY_TYPE_JSON', - userId: userId, - // base64 encoded public key - publicKey: publicKey - }), - { - headers: { - authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'x-zitadel-orgid': org.organizationId, + let response = http.asyncRequest( + 'POST', + url(`/management/v1/users/${userId}/keys`), + JSON.stringify({ + type: 'KEY_TYPE_JSON', + userId: userId, + // base64 encoded public key + publicKey: publicKey, + }), + { + headers: { + authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-zitadel-orgid': org.organizationId, + }, }, - }); + ); response.then((res) => { check(res, { 'generate machine key status ok': (r) => r.status === 200, diff --git a/load-test/webpack.config.js b/load-test/webpack.config.js index 580e891425..dbb76d0399 100644 --- a/load-test/webpack.config.js +++ b/load-test/webpack.config.js @@ -5,7 +5,7 @@ const GlobEntries = require('webpack-glob-entries'); module.exports = { mode: 'production', - entry: GlobEntries('./src/use_cases/*.ts'), // Generates multiple entry for each test + entry: GlobEntries('./src/use_cases/**/*.ts'), // Generates multiple entry for each test output: { path: path.join(__dirname, 'dist'), libraryTarget: 'commonjs',