add auth provider factory

add oidc cache and respect token lifetime
simplify account retrieval
This commit is contained in:
Florian Schade
2020-11-30 02:33:48 +01:00
parent 1558b2a472
commit 599b2a6c29
11 changed files with 200 additions and 140 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
import encoding from 'k6/encoding';
import * as types from "../types";
export const headersDefault = ({credential}: { credential: types.Account | types.Token }): { [key: string]: string } => {
export const headersDefault = ({credential}: { credential: types.Credential }): { [key: string]: string } => {
const isOIDCGuard = (credential as types.Token).tokenType !== undefined;
const authOIDC = credential as types.Token;
const authBasic = credential as types.Account;
+3 -3
View File
@@ -4,7 +4,7 @@ import * as defaults from "../defaults";
import * as types from "../types";
export const fileUpload = <RT extends ResponseType | undefined>(
{credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }
{credential, userName, asset}: { credential: types.Credential; userName: string; asset: types.Asset }
): RefinedResponse<RT> => {
return http.put(
`${defaults.OC_HOST}/remote.php/dav/files/${userName}/${asset.fileName}`,
@@ -18,7 +18,7 @@ export const fileUpload = <RT extends ResponseType | undefined>(
}
export const fileDownload = <RT extends ResponseType | undefined>(
{credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string }
{credential, userName, fileName}: { credential: types.Credential; userName: string; fileName: string }
): RefinedResponse<RT> => {
return http.get(
`${defaults.OC_HOST}/remote.php/dav/files/${userName}/${fileName}`,
@@ -31,7 +31,7 @@ export const fileDownload = <RT extends ResponseType | undefined>(
}
export const fileDelete = <RT extends ResponseType | undefined>(
{credential, userName, fileName}: { credential: types.Account | types.Token; userName: string; fileName: string }
{credential, userName, fileName}: { credential: types.Credential; userName: string; fileName: string }
): RefinedResponse<RT> => {
return http.del(
`${defaults.OC_HOST}/remote.php/dav/files/${userName}/${fileName}`,
+1 -1
View File
@@ -4,7 +4,7 @@ import * as defaults from "../defaults";
import * as types from "../types";
export const userInfo = <RT extends ResponseType | undefined>(
{credential, userName}: { credential: types.Account | types.Token; userName: string; }
{credential, userName}: { credential: types.Credential; userName: string; }
): RefinedResponse<RT> => {
return http.get(
`${defaults.OC_HOST}/ocs/v1.php/cloud/users/${userName}`,
+142 -66
View File
@@ -5,83 +5,159 @@ import * as types from "./types";
import {fail} from 'k6';
import {get} from 'lodash'
export const oidc = (account: types.Account): types.Token => {
const redirectUri = `${defaults.OC_OIDC_HOST}/oidc-callback.html`;
const logonUri = `${defaults.OC_OIDC_HOST}/signin/v1/identifier/_/logon`;
const logonResponse = http.post(
logonUri,
JSON.stringify(
{
params: [account.login, account.password, '1'],
hello: {
scope: 'openid profile email',
client_id: 'phoenix',
redirect_uri: redirectUri,
flow: 'oidc'
},
'state': 'vp42cf'
},
),
{
headers: {
'Kopano-Konnect-XSRF': '1',
Referer: defaults.OC_OIDC_HOST,
'Content-Type': 'application/json',
},
},
);
const authorizeURI = get(logonResponse.json(), 'hello.continue_uri');
export default class Factory {
private provider!: types.AuthProvider;
public account!: types.Account;
if (logonResponse.status != 200 || !authorizeURI) {
fail(logonUri);
constructor(account: types.Account) {
this.account = account;
if (defaults.OC_OIDC) {
this.provider = new OIDCProvider(account);
}
if (!defaults.OC_OIDC) {
this.provider = new AccountProvider(account);
}
}
const authorizeUri = `${authorizeURI}?${
queryString.stringify(
public get credential(): types.Credential {
return this.provider.credential
}
}
class AccountProvider implements types.AuthProvider {
private account: types.Account;
constructor(account: types.Account) {
this.account = account;
}
public get credential(): types.Account {
return this.account;
}
}
class OIDCProvider implements types.AuthProvider {
private account: types.Account;
private redirectUri = `${defaults.OC_OIDC_HOST}/oidc-callback.html`;
private logonUri = `${defaults.OC_OIDC_HOST}/signin/v1/identifier/_/logon`;
private tokenUrl = `${defaults.OC_OIDC_HOST}/konnect/v1/token`;
private cache!: {
validTo: Date;
token: types.Token;
}
constructor(account: types.Account) {
this.account = account;
}
public get credential(): types.Token {
if (!this.cache || this.cache.validTo <= new Date()) {
const continueURI = this.getContinueURI();
const code = this.getCode(continueURI);
const token = this.getToken(code);
this.cache = {
validTo: ((): Date => {
const offset = 5;
const d = new Date();
d.setSeconds(d.getSeconds() + token.expiresIn - offset)
return d
})(),
token,
}
}
return this.cache.token;
}
private getContinueURI(): string {
const logonResponse = http.post(
this.logonUri,
JSON.stringify(
{
params: [this.account.login, this.account.password, '1'],
hello: {
scope: 'openid profile email',
client_id: 'phoenix',
redirect_uri: this.redirectUri,
flow: 'oidc'
},
'state': 'vp42cf'
},
),
{
client_id: 'phoenix',
prompt: 'none',
redirect_uri: redirectUri,
response_mode: 'query',
response_type: 'code',
scope: 'openid profile email',
headers: {
'Kopano-Konnect-XSRF': '1',
Referer: defaults.OC_OIDC_HOST,
'Content-Type': 'application/json',
},
},
);
const continueURI = get(logonResponse.json(), 'hello.continue_uri');
if (logonResponse.status != 200 || !continueURI) {
fail(this.logonUri);
}
return continueURI;
}
private getCode(continueURI: string): string {
const authorizeUri = `${continueURI}?${
queryString.stringify(
{
client_id: 'phoenix',
prompt: 'none',
redirect_uri: this.redirectUri,
response_mode: 'query',
response_type: 'code',
scope: 'openid profile email',
},
)
}`;
const authorizeResponse = http.get(
authorizeUri,
{
redirects: 0,
},
)
}`;
const authorizeResponse = http.get(
authorizeUri,
{
redirects: 0,
},
)
const authCode = get(queryString.parseUrl(authorizeResponse.headers.Location), 'query.code')
if (authorizeResponse.status != 302 || !authCode) {
fail(authorizeURI);
}
const code = get(queryString.parseUrl(authorizeResponse.headers.Location), 'query.code')
const tokenUrl = `${defaults.OC_OIDC_HOST}/konnect/v1/token`;
const tokenResponse = http.post(
tokenUrl,
{
client_id: 'phoenix',
code: authCode,
redirect_uri: redirectUri,
grant_type: 'authorization_code'
if (authorizeResponse.status != 302 || !code) {
fail(continueURI);
}
)
const token = {
accessToken: get(tokenResponse.json(), 'access_token'),
tokenType: get(tokenResponse.json(), 'token_type'),
idToken: get(tokenResponse.json(), 'id_token'),
expiresIn: get(tokenResponse.json(), 'expires_in'),
return code
}
if (tokenResponse.status != 200 || !token.accessToken || !token.tokenType || !token.idToken || !token.expiresIn) {
fail(authorizeURI);
}
private getToken(code: string): types.Token {
const tokenResponse = http.post(
this.tokenUrl,
{
client_id: 'phoenix',
code,
redirect_uri: this.redirectUri,
grant_type: 'authorization_code'
}
)
return token
}
const token = {
accessToken: get(tokenResponse.json(), 'access_token'),
tokenType: get(tokenResponse.json(), 'token_type'),
idToken: get(tokenResponse.json(), 'id_token'),
expiresIn: get(tokenResponse.json(), 'expires_in'),
}
if (tokenResponse.status != 200 || !token.accessToken || !token.tokenType || !token.idToken || !token.expiresIn) {
fail(this.tokenUrl);
}
return token;
}
}
+27 -11
View File
@@ -11,16 +11,32 @@ export const OC_TEST_FILE = {
fileName: ocTestFile,
bytes: open(ocTestFile, 'b'),
}
export const k6OptionsDefault: Options = {
export const K6_OPTION_DEFAULTS: Options = {
insecureSkipTLSVerify: true,
};
export const knownAccounts: { [key: string]: types.Account; } = {
einstein: {
login: 'einstein',
password: 'relativity',
},
richard: {
login: 'richard',
password: 'superfluidity',
},
}
export class ACCOUNTS {
public static readonly EINSTEIN = 'einstein';
public static readonly RICHARD = 'richard';
private static readonly list: { [key: string]: types.Account; } = {
einstein: {
login: 'einstein',
password: 'relativity',
},
richard: {
login: 'richard',
password: 'superfluidity',
},
}
public static for(key: string): types.Account {
if (OC_LOGIN && OC_PASSWORD) {
return {
login: OC_LOGIN,
password: OC_PASSWORD,
}
}
return this.list[key];
}
}
+3 -3
View File
@@ -8,7 +8,7 @@ export const fileUpload = () => {
const fileUploadTrend = new Trend('occ_file_upload_trend', true);
const fileUploadErrorRate = new Gauge('occ_file_upload_error_rate');
return ({credential, userName, asset}: { credential: types.Account | types.Token; userName: string; asset: types.Asset }): string => {
return ({credential, userName, asset}: { credential: types.Credential; userName: string; asset: types.Asset }): string => {
const fileName = `upload-${userName}-${__VU}-${__ITER}.${utils.extension(asset.fileName)}`;
const uploadResponse = api.dav.fileUpload({
credential: credential as any,
@@ -33,7 +33,7 @@ export const fileDelete = () => {
const fileDeleteTrend = new Trend('occ_file_delete_trend', true);
const fileDeleteErrorRate = new Gauge('occ_file_delete_error_rate');
return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }) => {
return ({credential, userName, fileName}: { credential: types.Credential, userName: string; fileName: string }) => {
const deleteResponse = api.dav.fileDelete({
credential: credential as any,
fileName,
@@ -52,7 +52,7 @@ export const fileDownload = () => {
const fileDownloadTrend = new Trend('occ_file_download_trend', true);
const fileDownloadErrorRate = new Gauge('occ_file_download_error_rate');
return ({credential, userName, fileName}: { credential: types.Account | types.Token, userName: string; fileName: string }): bytes => {
return ({credential, userName, fileName}: { credential: types.Credential, userName: string; fileName: string }): bytes => {
const downloadResponse = api.dav.fileDownload({
credential: credential as any,
fileName,
+7 -1
View File
@@ -9,10 +9,16 @@ export interface Token {
accessToken: string;
tokenType: string;
idToken: string;
expiresIn: string;
expiresIn: number;
}
export interface Account {
login: string
password: string
}
export type Credential = Token | Account
export interface AuthProvider {
credential: Credential
}
-14
View File
@@ -1,6 +1,3 @@
import * as types from "./types";
import * as defaults from "./defaults";
export const randomString = (): string => {
return Math.random().toString(36).slice(2)
}
@@ -9,14 +6,3 @@ export const extension = (p: string): string | undefined => {
return (p.split('/').pop())!.split('.').pop()
}
export const getAccount = (key: string): types.Account => {
if (defaults.OC_LOGIN && defaults.OC_PASSWORD) {
return {
login: defaults.OC_LOGIN,
password: defaults.OC_PASSWORD,
}
}
return defaults.knownAccounts[key];
}
+9 -20
View File
@@ -1,35 +1,24 @@
import {defaults, playbook} from '../../lib'
import {Options} from 'k6/options';
import {sleep} from "k6";
import * as auth from "../../lib/auth";
import * as types from "../../lib/types";
import * as utils from "../../lib/utils";
interface dataI {
credential: types.Account | types.Token;
}
import auth from "../../lib/auth";
export const options: Options = {
...defaults.k6OptionsDefault,
...defaults.K6_OPTION_DEFAULTS,
iterations: 1,
vus: 1,
};
const account = utils.getAccount('einstein');
const authFactory = new auth(defaults.ACCOUNTS.for(defaults.ACCOUNTS.EINSTEIN));
const playbooks = {
fileUpload: playbook.dav.fileUpload(),
fileDownload: playbook.dav.fileDownload(),
fileDelete: playbook.dav.fileDelete(),
}
export const setup = (): dataI => {
return {
credential: defaults.OC_OIDC ? auth.oidc(account) : account,
}
}
export default (data: dataI) => {
const credential = data.credential;
const userName = account.login;
export default () => {
const {login: userName} = authFactory.account;
const fileName = playbooks.fileUpload({
credential,
credential: authFactory.credential,
userName,
asset: defaults.OC_TEST_FILE
});
@@ -37,7 +26,7 @@ export default (data: dataI) => {
sleep(1)
playbooks.fileDownload({
credential,
credential: authFactory.credential,
userName,
fileName,
});
@@ -45,7 +34,7 @@ export default (data: dataI) => {
sleep(1)
playbooks.fileDelete({
credential,
credential: authFactory.credential,
userName,
fileName,
});
+7 -19
View File
@@ -1,34 +1,22 @@
import {defaults, playbook} from '../../lib'
import {Options} from 'k6/options';
import {sleep} from "k6";
import * as auth from "../../lib/auth";
import * as types from "../../lib/types";
import * as utils from "../../lib/utils";
interface dataI {
credential: types.Account | types.Token;
}
import auth from "../../lib/auth";
export const options: Options = {
...defaults.k6OptionsDefault,
...defaults.K6_OPTION_DEFAULTS,
iterations: 1,
vus: 1,
};
const account = utils.getAccount('einstein');
const authFactory = new auth(defaults.ACCOUNTS.for(defaults.ACCOUNTS.EINSTEIN));
const playbooks = {
fileUpload: playbook.dav.fileUpload(),
fileDelete: playbook.dav.fileDelete(),
}
export const setup = (): dataI => {
return {
credential: defaults.OC_OIDC ? auth.oidc(account) : account,
}
}
export default (data: dataI) => {
const credential = data.credential;
const userName = account.login;
export default () => {
const {login: userName} = authFactory.account;
const fileName = playbooks.fileUpload({
credential,
credential: authFactory.credential,
userName,
asset: defaults.OC_TEST_FILE
});
@@ -36,7 +24,7 @@ export default (data: dataI) => {
sleep(1)
playbooks.fileDelete({
credential,
credential: authFactory.credential,
userName,
fileName,
});
@@ -6,5 +6,4 @@ export const options: Options = {
iterations: 200,
vus: 50,
};
export const {setup} = uploadFilesBenchmark;
export default uploadFilesBenchmark.default;