Skip to content

Commit

Permalink
refactor: openIdVariableReplace splitted to separete files and better…
Browse files Browse the repository at this point in the history
… userSession management
  • Loading branch information
AnWeber committed Mar 3, 2021
1 parent 97c8653 commit 5bb91c9
Show file tree
Hide file tree
Showing 16 changed files with 527 additions and 368 deletions.
31 changes: 26 additions & 5 deletions src/environments/environmentStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ENVIRONMENT_NONE, isString } from '../utils';
import { Variables, EnvironmentProvider, HttpFile, EnvironmentConfig } from '../models';
import { Variables, EnvironmentProvider, HttpFile, EnvironmentConfig, UserSession } from '../models';
import {httpYacApi} from '../httpYacApi';
import { JsonEnvProvider } from './jsonEnvProvider';
import { EnvVariableProvider } from '../variables/provider/envVariableProvider';
Expand All @@ -11,8 +11,7 @@ class EnvironmentStore{
readonly environmentProviders: Array<EnvironmentProvider> = [];

private environments: Record<string, Variables> = {};

public additionalResets: Array<() => void> = [];
readonly userSessions: Array<UserSession> = [];


async reset() {
Expand All @@ -28,8 +27,30 @@ class EnvironmentStore{
}
}

for (const reset of this.additionalResets) {
reset();
for (const userSession of this.userSessions) {
if (userSession.logout) {
userSession.logout();
}
}
this.userSessions.length = 0;
}

getUserSession(id: string) {
return this.userSessions.find(obj => obj.id === id);
}

setUserSession(userSession: UserSession) {
this.removeUserSession(userSession.id);
this.userSessions.push(userSession);
}

removeUserSession(id: string) {
const userSession = this.userSessions.find(obj => obj.id === id);
if (userSession) {
if (userSession.logout) {
userSession.logout();
}
this.userSessions.splice(this.userSessions.indexOf(userSession), 1);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ export * from './processorContext';
export * from './variableReplacer';
export * from './variableProvider';
export * from './variableReplacerType';
export * from './variables';
export * from './variables';
export * from './userSession';
8 changes: 8 additions & 0 deletions src/models/userSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@


export interface UserSession{
id: string;
title: string;
description: string;
logout?: () => void;
}
8 changes: 8 additions & 0 deletions src/utils/requestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,12 @@ export function decodeJWT(str: string) : JWTToken | null{
log.warn(err);
return null;
}
}


export function toQueryParams(params: Record<string, any>) {
return Object.entries(params)
.filter(([, value]) => !!value)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
}
105 changes: 105 additions & 0 deletions src/variables/replacer/oauth/authorizationCodeFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { OpenIdConfiguration, assertConfiguration } from './openIdConfiguration';
import { OpenIdInformation, requestOpenIdInformation } from './openIdInformation';
import { OpenIdFlow } from './openIdFlow';
import { toQueryParams } from '../../../utils';
import { HttpClient, Progress } from '../../../models';
import open from 'open';
import { registerListener, unregisterListener } from './openIdHttpserver';

class AuthorizationCodeFlow implements OpenIdFlow {
supportsFlow(flow: string): boolean{
return ['authorization_code', 'code'].indexOf(flow) >= 0;
}

getCacheKey(config: OpenIdConfiguration) {
if (assertConfiguration(config, ['tokenEndpoint', 'authorizationEndpoint', 'clientId', 'clientSecret'])) {
return `authorization_code_${config.clientId}_${config.tokenEndpoint}`;
}
return false;
}

async perform(config: OpenIdConfiguration, context: {httpClient: HttpClient, progress: Progress | undefined, cacheKey: string}): Promise<OpenIdInformation | false> {
return new Promise<OpenIdInformation | false>(async (resolve, reject) => {
const state = this.stateGenerator();
try {
const redirectUri = `http://localhost:${config.port}/callback`;
const authUrl = `${config.authorizationEndpoint}${config.authorizationEndpoint.indexOf('?') > 0 ? '&' : '?'}${toQueryParams({
client_id: config.clientId,
scope: config.scope || 'openid',
response_type: 'code',
state,
redirect_uri: redirectUri
})}`;

let unregisterProgress: (() => void) | undefined;
if (context.progress) {
unregisterProgress = context.progress.register(() => {
unregisterListener(state);
reject();
});
}

registerListener({
id: state,
name: `authorization for ${config.clientId}: ${config.authorizationEndpoint}`,
resolve: (params) => {
if (params.code && params.state === state) {
if (unregisterProgress) {
unregisterProgress();
}
const openIdInformation = requestOpenIdInformation({
url: config.tokenEndpoint,
method: 'POST',
headers: {
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: toQueryParams({
grant_type: 'authorization_code',
scope: config.scope,
code: params.code,
redirect_uri: redirectUri
})
}, {
httpClient: context.httpClient,
config: config,
id: context.cacheKey,
title: `authorization_code: ${config.clientId}`,
description: config.tokenEndpoint
});
resolve(openIdInformation);
return {
valid: true,
message: 'code received.',
statusMessage: 'code and state valid. starting code exchange'
};
}

return {
valid: false,
message: 'no code received',
statusMessage: 'no code received'
};
},
reject,
});
await open(authUrl);
} catch (err) {
unregisterListener(state);
reject(err);
}
});
}

private stateGenerator(length: number = 30) {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
const result = [];
for (var i = length; i > 0; --i){
result.push(chars[Math.floor(Math.random() * chars.length)]);
}
return result.join('');
}
}


export const authorizationCodeFlow = new AuthorizationCodeFlow();
42 changes: 42 additions & 0 deletions src/variables/replacer/oauth/clientCredentialsFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { OpenIdConfiguration, assertConfiguration } from './openIdConfiguration';
import { OpenIdInformation, requestOpenIdInformation } from './openIdInformation';
import { OpenIdFlow } from './openIdFlow';
import { toQueryParams } from '../../../utils';
import { HttpClient } from '../../../models';

class ClientCredentialsFlow implements OpenIdFlow {
supportsFlow(flow: string): boolean{
return ['client_credentials', 'client'].indexOf(flow) >= 0;
}

getCacheKey(config: OpenIdConfiguration) {
if (assertConfiguration(config, ['tokenEndpoint', 'clientId', 'clientSecret'])) {
return `client_credentials_${config.clientId}_${config.tokenEndpoint}`;
}
return false;
}


async perform(config: OpenIdConfiguration, context: {httpClient: HttpClient, cacheKey: string}): Promise<OpenIdInformation | false> {
return requestOpenIdInformation({
url: config.tokenEndpoint,
method: 'POST',
headers: {
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: toQueryParams({
grant_type: 'client_credentials',
scope: config.scope
})
}, {
httpClient: context.httpClient,
config: config,
id: context.cacheKey,
title: `clientCredentials: ${config.clientId}`,
description: config.tokenEndpoint
});
}
}

export const clientCredentialsFlow = new ClientCredentialsFlow();
8 changes: 8 additions & 0 deletions src/variables/replacer/oauth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from './authorizationCodeFlow';
export * from './clientCredentialsFlow';
export * from './openIdConfiguration';
export * from './openIdFlow';
export * from './openIdInformation';
export * from './refreshTokenFlow';
export * from './passwordFlow';
export * from './tokenExchangeFlow';
Empty file.
57 changes: 57 additions & 0 deletions src/variables/replacer/oauth/openIdConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import get from 'lodash/get';
import { popupService, log } from '../../../logger';

export interface OpenIdConfiguration{
variablePrefix: string;
authorizationEndpoint: string;
tokenEndpoint: string;
clientId: string;
clientSecret: string;
scope: string;
keepAlive: boolean;
username?: string;
password?: string;
port?: string;
subjectIssuer?: string;
noLog?: boolean;
}

export function getOpenIdConfiguration(variablePrefix: string, variables: Record<string, any>) {
if (variablePrefix) {
const getVariable = (name: string) => variables[`${variablePrefix}_${name}`] || get(variables, `${variablePrefix}.${name}`);

const config: OpenIdConfiguration = {
variablePrefix,
authorizationEndpoint: getVariable('authorizationEndpoint'),
tokenEndpoint: getVariable('tokenEndpoint'),
clientId: getVariable('clientId'),
clientSecret: getVariable('clientSecret'),
scope: getVariable('scope'),
username: getVariable('username'),
password: getVariable('password'),
subjectIssuer: getVariable('subjectIssuer'),
noLog: getVariable('noLog'),
port: getVariable('port') || 3000,
keepAlive: ['false', '0', false].indexOf(getVariable('keepAlive')) < 0,
};
return config;
}
return false;
}


export function assertConfiguration(config: OpenIdConfiguration, keys: string[]) {
const missingKeys = [];
for (const key of keys) {
if (!Object.entries(config).some(([obj, value]) => obj === key && !!value)) {
missingKeys.push(key);
}
}
if (missingKeys.length > 0) {
const message = `missing configuration: ${missingKeys.map(obj => `${config.variablePrefix}_${obj}`).join(', ')}`;
log.error(message);
popupService.error(message);
return false;
}
return true;
}
10 changes: 10 additions & 0 deletions src/variables/replacer/oauth/openIdFlow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { HttpClient, Progress } from '../../../models';
import { OpenIdConfiguration } from './openIdConfiguration';
import { OpenIdInformation } from './openIdInformation';


export interface OpenIdFlow{
supportsFlow(flow: string): boolean;
getCacheKey(config: OpenIdConfiguration): string | false;
perform(config: OpenIdConfiguration, context: {httpClient: HttpClient, progress?: Progress | undefined, cacheKey: string}): Promise<OpenIdInformation | false>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { createServer, Server } from 'http';
import { log } from '../../logger';
import { log } from '../../../logger';


interface RequestListener{
Expand Down
66 changes: 66 additions & 0 deletions src/variables/replacer/oauth/openIdInformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { OpenIdConfiguration } from './openIdConfiguration';
import { log, logRequest } from '../../../logger';
import { HttpClient, HttpClientOptions, HttpResponse, UserSession } from '../../../models';
import { decodeJWT, isString, toConsoleOutput } from '../../../utils';

export interface OpenIdInformation extends UserSession{
time: number;
config: OpenIdConfiguration;
accessToken: string;
expiresIn: number;
timeSkew: number;
refreshToken?: string;
refreshExpiresIn?: number;
}


export async function requestOpenIdInformation(options: HttpClientOptions | false,context: {
config: OpenIdConfiguration,
httpClient: HttpClient,
id: string,
title: string,
description: string,
}): Promise<OpenIdInformation | false>{
if (options) {
const time = new Date().getTime();
const response = await context.httpClient(options, { showProgressBar: false });
if (response) {
response.request = options;
}
return toOpenIdInformation(response, time, context);
}
return false;
}

export function toOpenIdInformation(response: false | HttpResponse, time: number, context: {
config: OpenIdConfiguration,
id: string,
title: string,
description: string,
}): OpenIdInformation | false {
if (response) {
if (!context.config.noLog) {
logRequest.info(toConsoleOutput(response, true));
}
if (response.statusCode === 200 && isString(response.body)) {
const jwtToken = JSON.parse(response.body);
const parsedToken = decodeJWT(jwtToken.access_token);
if (!context.config.noLog) {
log.info(JSON.stringify(parsedToken, null, 2));
}
return {
id: context.id,
title: context.title,
description: context.description,
time,
config: context.config,
accessToken: jwtToken.access_token,
expiresIn: jwtToken.expires_in,
refreshToken: jwtToken.refresh_token,
refreshExpiresIn: jwtToken.refresh_expires_in,
timeSkew: parsedToken?.iat ? Math.floor(time / 1000) - parsedToken.iat : 0,
};
}
}
return false;
}
Loading

0 comments on commit 5bb91c9

Please sign in to comment.