Skip to content

Commit

Permalink
fix: scope parameters can be specified #1
Browse files Browse the repository at this point in the history
  • Loading branch information
AnWeber committed Feb 22, 2021
1 parent 0b75937 commit 91c302a
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 76 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"license": "MIT",
"publisher": "weber.andreas",
"description": "HTTP/REST Client for *.http files",
"version": "1.13.0",
"version": "1.14.0",
"repository": {
"type": "git",
"url": "https://github.com/AnWeber/httpyac"
Expand Down
60 changes: 39 additions & 21 deletions src/utils/requestUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { HttpMethod } from '../models';
import { log } from '../logger';


export function isRequestMethod(method: string | undefined): method is HttpMethod {
Expand All @@ -23,28 +24,45 @@ export function getHeader(headers: Record<string, string | string[] | undefined
}


export function decodeJWT(str: string) {
let jwtComponents = str.split('.');
if (jwtComponents.length !== 3) {
return;
}
let payload = jwtComponents[1];
payload = payload.replace(/-/g, '+');
payload = payload.replace(/_/g, '/');
switch (payload.length % 4) {
case 0:
break;
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
default:
export interface JWTToken {
iss?: string;
sub?: string;
aud?: string[];
exp?: number;
iat?: number;
jti?: string;
scope?: string;
name?: string;
}


export function decodeJWT(str: string) : JWTToken | null{
try {
let jwtComponents = str.split('.');
if (jwtComponents.length !== 3) {
return null;
}
}
let payload = jwtComponents[1];
payload = payload.replace(/-/g, '+');
payload = payload.replace(/_/g, '/');
switch (payload.length % 4) {
case 0:
break;
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
default:
return null;
}

const result = decodeURIComponent(escape(Buffer.from(payload, 'base64').toString()));
const result = decodeURIComponent(escape(Buffer.from(payload, 'base64').toString()));

return JSON.parse(result);
return JSON.parse(result);
} catch (err) {
log.warn(err);
return null;
}
}
135 changes: 82 additions & 53 deletions src/variables/replacer/openIdVariableReplacer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ interface OpenIdConfig{
tokenEndpoint: string;
clientId: string;
clientSecret: string;
scope: string;
keepAlive: boolean;
time: number;
username?: string;
password?: string;
port?: string;
subjectIssuer?: string;
}

interface OpenIdInformation{
clientId: string;
clientSecret: string;
url: string;
config: OpenIdConfig;
accessToken: string;
expiresIn: number;
time: number;
timeSkew: number;
refreshToken?: string;
refreshExpiresIn?: number;
timeSkew: number;
dispose?: () => void;
}

Expand Down Expand Up @@ -63,7 +64,7 @@ export async function openIdVariableReplacer(text: string, type: string, context
openIdInformation = await requestOpenIdInformation(match.groups.tokenExchangePrefix, config => tokenExchangeFlow(config, openIdInformation), context);
}
if (!openIdInformation) {
throw new Error(`Anmeldung gescheitert`);
throw new Error(`authorization failed`);
}
}
}
Expand All @@ -73,6 +74,7 @@ export async function openIdVariableReplacer(text: string, type: string, context
removeOpenIdInformation(cacheKey);
oauthStore[cacheKey] = openIdInformation;
keepAlive(cacheKey, context.httpClient);

return `Bearer ${openIdInformation.accessToken}`;
}
} catch (err) {
Expand All @@ -85,7 +87,7 @@ export async function openIdVariableReplacer(text: string, type: string, context

function keepAlive(cacheKey: string, httpClient: HttpClient) {
const openIdInformation = oauthStore[cacheKey];
if (openIdInformation && openIdInformation.refreshToken) {
if (openIdInformation && openIdInformation.refreshToken && openIdInformation.config.keepAlive) {
const timeoutId = setTimeout(async () => {
const result = await refreshToken(openIdInformation, httpClient, undefined);
if (result) {
Expand All @@ -105,14 +107,11 @@ function removeOpenIdInformation(cacheKey: string) {
}
delete oauthStore[cacheKey];
}


}


async function refreshTokenOrReuse(cachedOpenIdInformation: OpenIdInformation, httpClient: HttpClient, progress: Progress | undefined) {
if (cachedOpenIdInformation) {
if (!isTokenExpired(cachedOpenIdInformation.time, cachedOpenIdInformation.expiresIn, cachedOpenIdInformation.timeSkew)) {
if (!isTokenExpired(cachedOpenIdInformation.config.time, cachedOpenIdInformation.expiresIn, cachedOpenIdInformation.timeSkew)) {
return cachedOpenIdInformation;
}

Expand All @@ -125,44 +124,47 @@ async function refreshTokenOrReuse(cachedOpenIdInformation: OpenIdInformation, h
async function refreshToken(cachedOpenIdInformation: OpenIdInformation, httpClient: HttpClient, progress: Progress | undefined) {
if (cachedOpenIdInformation.refreshToken
&& cachedOpenIdInformation.refreshExpiresIn
&& !isTokenExpired(cachedOpenIdInformation.time, cachedOpenIdInformation.refreshExpiresIn, cachedOpenIdInformation.timeSkew)) {
&& !isTokenExpired(cachedOpenIdInformation.config.time, cachedOpenIdInformation.refreshExpiresIn, cachedOpenIdInformation.timeSkew)) {

const time = new Date().getTime();
const config = {
...cachedOpenIdInformation.config,
time: new Date().getTime()
};
const options: HttpClientOptions = {
url: cachedOpenIdInformation.url,
url: config.tokenEndpoint,
method: 'POST',
headers: {
'authorization': `Basic ${Buffer.from(`${cachedOpenIdInformation.clientId}:${cachedOpenIdInformation.clientSecret}`).toString('base64')}`,
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: `grant_type=refresh_token&refresh_token=${cachedOpenIdInformation.refreshToken}`
body: toQueryParams({
grant_type: 'refresh_token',
refresh_token: cachedOpenIdInformation.refreshToken
})
};
const response = await httpClient(options, progress, false);
if (response) {
response.request = options;
}
return toOpenIdInformation(cachedOpenIdInformation.url, cachedOpenIdInformation.clientId, cachedOpenIdInformation.clientSecret, time, response);
return toOpenIdInformation(response, config);
}
return false;
}

function toOpenIdInformation(url: string, clientId: string, clientSecret: string, time: number, response: false | HttpResponse): OpenIdInformation | false {
function toOpenIdInformation(response: false | HttpResponse, config: OpenIdConfig): OpenIdInformation | false {
if (response) {
log.info(toConsoleOutput(response, true));
if (response.statusCode === 200 && isString(response.body)) {
const jwtToken = JSON.parse(response.body);
const parsedToken = decodeJWT(jwtToken.access_token);
log.info(JSON.stringify(parsedToken, null, 2));
return {
url,
clientId,
clientSecret,
time,
config,
accessToken: jwtToken.access_token,
expiresIn: jwtToken.expires_in,
refreshToken: jwtToken.refresh_token,
refreshExpiresIn: jwtToken.refresh_expires_in,
timeSkew: Math.floor(time / 1000) - parsedToken.iat,
timeSkew: parsedToken?.iat ? Math.floor(config.time / 1000) - parsedToken.iat : 0,
};
}
}
Expand All @@ -174,18 +176,20 @@ function isTokenExpired(time: number, expiresIn: number, timeSkew: number) {
}

async function requestOpenIdInformation(variablePrefix: string, getOptions: (config: OpenIdConfig) => Promise<HttpClientOptions | false>, { httpClient, progress, variables }: ProcessorContext) : Promise<OpenIdInformation | false>{
const time = new Date().getTime();

const getVariable = (name: string) => variables[`${variablePrefix}_${name}`] || get(variables, `${variablePrefix}.${name}`);

const config: OpenIdConfig = {
authorizationEndpoint: getVariable('authorizationEndpoint'),
tokenEndpoint: getVariable('tokenEndpoint'),
clientId: getVariable('clientId'),
clientSecret: getVariable('clientSecret'),
scope: getVariable('scope'),
username: getVariable('username'),
password: getVariable('password'),
subjectIssuer: getVariable('subjectIssuer'),
port: getVariable('port') || 3000,
keepAlive: ['false', '0', false].indexOf(getVariable('keepAlive')) < 0,
time: new Date().getTime()
};

const options = await getOptions(config);
Expand All @@ -194,7 +198,7 @@ async function requestOpenIdInformation(variablePrefix: string, getOptions: (con
if (response) {
response.request = options;
}
return toOpenIdInformation(config.tokenEndpoint, config.clientId, config.clientSecret, time, response);
return toOpenIdInformation(response, config);
}
return false;
}
Expand All @@ -207,7 +211,10 @@ async function clientCredentialsFlow(config: OpenIdConfig) : Promise<HttpClientO
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials'
body: toQueryParams({
grant_type: 'client_credentials',
scope: config.scope
})
};
}

Expand All @@ -221,7 +228,12 @@ async function passwordFlow(config: OpenIdConfig) : Promise<HttpClientOptions |
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: `grant_type=password&username=${encodeURIComponent(config.username)}&password=${encodeURIComponent(config.password)}`
body: toQueryParams({
grant_type: 'password',
scope: config.scope,
username: config.username,
password: config.password
})
};
}
return false;
Expand All @@ -231,39 +243,45 @@ async function tokenExchangeFlow(config: OpenIdConfig, openIdInformation: OpenId
if (openIdInformation) {
const jwtToken = decodeJWT(openIdInformation.accessToken);


const bodyLines = [
"grant_type=urn:ietf:params:oauth:grant-type:token-exchange",
"requested_token_type=urn:ietf:params:oauth:token-type:access_token",
"subject_token_type=urn:ietf:params:oauth:token-type:access_token",
"scope=openid",
`subject_issuer=${jwtToken.iss}`,
`subject_token=${encodeUrl(openIdInformation.accessToken)}`
];
return {
url: config.tokenEndpoint,
method: 'POST',
headers: {
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: bodyLines.join('&')
};
if (config.subjectIssuer || jwtToken?.iss) {
const issuer = config.subjectIssuer || jwtToken?.iss;
return {
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: 'urn:ietf:params:oauth:grant-type:token-exchange',
requested_token_type: 'urn:ietf:params:oauth:token-type:access_token',
subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',
scope: config.scope || 'openid',
subject_issuer: issuer,
subject_token: encodeUrl(openIdInformation.accessToken)
})
};
}
}
return false;
}


function authorizationCodeFlow(config: OpenIdConfig, filename: string) : Promise<HttpClientOptions | false> {
return new Promise<HttpClientOptions | false>(async (resolve, reject) => {

try {


const redirectUri = `http://localhost:${config.port}/callback`;
const state = stateGenerator();

const authUrl = `${config.authorizationEndpoint}?client_id=${encodeURIComponent(config.clientId)}&response_type=code&state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
const queryString = toQueryParams({
client_id: config.clientId,
scope: config.scope || 'openid',
response_type: 'code',
state: state,
redirect_uri: redirectUri
});

const authUrl = `${config.authorizationEndpoint}${config.authorizationEndpoint.indexOf('?') > 0 ? '&' : '?'}${queryString}`;

let close: false | (() => void) = false;
const server = createServer((req, res) => {
Expand All @@ -281,11 +299,14 @@ function authorizationCodeFlow(config: OpenIdConfig, filename: string) : Promise
'authorization': `Basic ${Buffer.from(`${config.clientId}:${config.clientSecret}`).toString('base64')}`,
'content-type': 'application/x-www-form-urlencoded',
},
body: `grant_type=authorization_code&code=${encodeURIComponent(queryParams.code)}&redirect_uri=${encodeURIComponent(redirectUri)}`
body: toQueryParams({
grant_type: 'authorization_code',
scope: config.scope,
code: queryParams.code,
redirect_uri: redirectUri
})
});



res.setHeader("Location", `vscode://${filename}`);
statusCode = 302;
message = 'code and valid state received. switch back to vscode';
Expand Down Expand Up @@ -338,6 +359,14 @@ function parseQueryParams(url: string) {
}, {} as Record<string,string>);
}


function toQueryParams(params: Record<string, any>) {
return Object.entries(params)
.filter(([, value]) => !!value)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
}

function getHtml(message: string) {

return `
Expand Down

0 comments on commit 91c302a

Please sign in to comment.