Silvan 1ce9a4322e
test(load): machine jwt profile grant (#8482)
# Which Problems Are Solved

Currently there was no load test present for machine jwt profile grant.
This test is now added

# How the Problems Are Solved

K6 test implemented.

# Additional Context

- part of https://github.com/zitadel/zitadel/issues/8352
2024-08-27 13:06:03 +00:00

171 lines
4.8 KiB
TypeScript

import { JSONObject, check, fail } from 'k6';
import encoding from 'k6/encoding';
import http, { RequestBody } from 'k6/http';
import { Trend } from 'k6/metrics';
import url from './url';
import { Config } from './config';
// @ts-ignore Import module
import zitadel from 'k6/x/zitadel';
export class Tokens {
idToken?: string;
accessToken?: string;
info?: any;
constructor(res: JSONObject) {
this.idToken = res.id_token ? res.id_token!.toString() : undefined;
this.accessToken = res.access_token ? res.access_token!.toString() : undefined;
this.info = this.idToken
? JSON.parse(encoding.b64decode(this.idToken?.split('.')[1].toString(), 'rawstd', 's'))
: undefined;
}
}
let oidcConfig: any | undefined;
function configuration() {
if (oidcConfig !== undefined) {
return oidcConfig;
}
const res = http.get(url('/.well-known/openid-configuration'));
check(res, {
'openid configuration': (r) => r.status == 200 || fail('unable to load openid configuration'),
});
oidcConfig = res.json();
return oidcConfig;
}
const userinfoTrend = new Trend('oidc_user_info_duration', true);
export function userinfo(token: string) {
const userinfo = http.get(configuration().userinfo_endpoint, {
headers: {
authorization: 'Bearer ' + token,
'Content-Type': 'application/json',
},
});
check(userinfo, {
'userinfo status ok': (r) => r.status === 200,
});
userinfoTrend.add(userinfo.timings.duration);
}
const introspectTrend = new Trend('oidc_introspect_duration', true);
export function introspect(jwt: string, token: string) {
const res = http.post(
configuration().introspection_endpoint,
{
client_assertion: jwt,
token: token,
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
},
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
alg: 'RS256',
},
},
);
check(res, {
'introspect status ok': (r) => r.status === 200,
});
introspectTrend.add(res.timings.duration);
}
const clientCredentialsTrend = new Trend('oidc_client_credentials_duration', true);
export function clientCredentials(clientId: string, clientSecret: string): Promise<Tokens> {
return new Promise((resolve, reject) => {
const response = http.asyncRequest('POST', configuration().token_endpoint,
{
grant_type: "client_credentials",
scope: 'openid profile urn:zitadel:iam:org:project:id:zitadel:aud',
client_id: clientId,
client_secret: clientSecret,
},
{
headers: {
'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)
check(tokens, {
'client credentials token ok': (t) => t.accessToken !== undefined,
}) || reject(`client credentials access token missing (client id: ${clientId}`);
resolve(tokens)
});
});
}
export interface TokenRequest {
payload(): RequestBody;
headers(): { [name: string]: string; };
}
const privateKey = open('../.keys/key.pem');
export class JWTProfileRequest implements TokenRequest {
keyPayload!: {
userId: string;
expiration: number;
keyId: string;
};
constructor(userId: string, keyId: string) {
this.keyPayload = {
userId: userId,
// 1 minute
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
});
return {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
scope: 'openid',
assertion: `${assertion}`
};
};
public headers(): { [name: string]: string; } {
return {
'Content-Type': 'application/x-www-form-urlencoded'
};
};
}
const tokenDurationTrend = new Trend('oidc_token_duration', true);
export async function token(request: TokenRequest): Promise<Tokens> {
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')! != '',
});
return new Tokens(res.json() as JSONObject);
});
};