Expose identity token for oauth2 (#3211)

This commit is contained in:
Sayooj Sojen authored and GitHub committed 2021-04-05 13:03:34 -04:00
1 parent 3b2306adb2
commit 351afde43f
15 files changed
+133 -14

No files matched your search

@@ -1,6 +1,12 @@
import * as misc from '../misc';
import { globalBeforeEach } from '../../__jest__/before-each';
import { diffPatchObj, isNotNullOrUndefined, pluralize, snapNumberToLimits } from '../misc';
import {
diffPatchObj,
isNotNullOrUndefined,
pluralize,
snapNumberToLimits,
convertEpochToMilliseconds,
} from '../misc';
describe('hasAuthHeader()', () => {
beforeEach(globalBeforeEach);
@@ -248,6 +254,20 @@ describe('diffPatchObj()', () => {
});
});
describe('convertEpochToMilliseconds()', () => {
it('should convert microseconds to milliseconds', () => {
expect(convertEpochToMilliseconds(1617616858412123)).toBe(1617616858412);
});
it('should convert seconds to milliseconds', () => {
expect(convertEpochToMilliseconds(1617617010)).toBe(1617617010000);
});
it('should output same if value already in milliseconds', () => {
expect(convertEpochToMilliseconds(1617617141412)).toBe(1617617141412);
});
});
describe('snapNumberToLimits()', () => {
it('should return value', () => {
expect(snapNumberToLimits(2)).toBe(2);
+10
View File
@@ -441,6 +441,16 @@ export function isObject(obj: any) {
return obj !== null && typeof obj === 'object';
}
export function convertEpochToMilliseconds(epoch: number) {
/*
Finds epoch's digit count and converts it to make it exactly 13 digits.
Which is the epoch millisecond represntation.
*/
const expDigitCount = epoch.toString().length;
const convertedEpoch = parseInt(epoch * 10 ** (13 - expDigitCount));
return convertedEpoch;
}
export function snapNumberToLimits(value: number, min?: number, max?: number): number {
const moreThanMax = max && !Number.isNaN(max) && value > max;
const lessThanMin = min && !Number.isNaN(min) && value < min;
@@ -6,6 +6,7 @@ import * as db from '../common/database';
type BaseOAuth2Token = {
refreshToken: string,
accessToken: string,
identityToken: string,
expiresAt: number | null, // Should be Date.now() if valid
// Debug
@@ -30,6 +31,7 @@ export function init(): BaseOAuth2Token {
return {
refreshToken: '',
accessToken: '',
identityToken: '',
expiresAt: null, // Should be Date.now() if valid
// Debug
@@ -95,6 +95,7 @@ describe('authorization_code', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
refresh_token: null,
expires_in: null,
token_type: 'token_type',
@@ -182,6 +183,7 @@ describe('authorization_code', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
refresh_token: null,
expires_in: null,
token_type: 'token_type',
@@ -241,6 +243,7 @@ describe('authorization_code', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
refresh_token: null,
expires_in: null,
token_type: 'token_type',
@@ -85,6 +85,7 @@ describe('client_credentials', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
expires_in: null,
token_type: 'token_type',
scope: SCOPE,
@@ -165,6 +166,7 @@ describe('client_credentials', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
expires_in: null,
token_type: 'token_type',
scope: SCOPE,
@@ -19,6 +19,7 @@ describe('implicit', () => {
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
token_type: null,
expires_in: null,
scope: null,
@@ -87,6 +87,7 @@ describe('password', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
expires_in: null,
token_type: 'token_type',
refresh_token: null,
@@ -168,6 +169,7 @@ describe('password', () => {
// Check the expected value
expect(result).toEqual({
access_token: 'token_123',
id_token: null,
expires_in: null,
token_type: 'token_type',
refresh_token: null,
@@ -15,6 +15,7 @@ import {
P_ERROR,
P_EXPIRES_IN,
P_REFRESH_TOKEN,
P_ID_TOKEN,
X_RESPONSE_ID,
X_ERROR,
} from './constants';
@@ -92,7 +93,6 @@ async function _getOAuth2ClientCredentialsHeader(
authentication.audience,
authentication.resource,
);
return _updateOAuth2Token(requestId, results);
}
@@ -218,6 +218,7 @@ async function _updateOAuth2Token(requestId: string, authResults: Object): Promi
expiresAt,
refreshToken: authResults[P_REFRESH_TOKEN] || null,
accessToken: authResults[P_ACCESS_TOKEN] || null,
identityToken: authResults[P_ID_TOKEN] || null,
error: authResults[P_ERROR] || null,
errorDescription: authResults[P_ERROR_DESCRIPTION] || null,
errorUri: authResults[P_ERROR_URI] || null,
@@ -192,6 +192,7 @@ async function _getToken(
const results = responseToObject(bodyBuffer.toString('utf8'), [
c.P_ACCESS_TOKEN,
c.P_ID_TOKEN,
c.P_REFRESH_TOKEN,
c.P_EXPIRES_IN,
c.P_TOKEN_TYPE,
@@ -67,6 +67,7 @@ export default async function(
const results = responseToObject(bodyBuffer.toString('utf8'), [
c.P_ACCESS_TOKEN,
c.P_ID_TOKEN,
c.P_TOKEN_TYPE,
c.P_EXPIRES_IN,
c.P_SCOPE,
@@ -56,7 +56,6 @@ export default async function(
c.P_ERROR_URI,
]);
results[c.P_ACCESS_TOKEN] = results[c.P_ACCESS_TOKEN] || results[c.P_ID_TOKEN];
delete results[c.P_ID_TOKEN];
return results;
} else {
// Bad redirect
@@ -71,6 +71,7 @@ export default async function(
const results = responseToObject(bodyBuffer.toString(), [
c.P_ACCESS_TOKEN,
c.P_ID_TOKEN,
c.P_TOKEN_TYPE,
c.P_EXPIRES_IN,
c.P_REFRESH_TOKEN,
@@ -80,6 +80,7 @@ export default async function(
bodyBuffer.toString(),
[
c.P_ACCESS_TOKEN,
c.P_ID_TOKEN,
c.P_REFRESH_TOKEN,
c.P_EXPIRES_IN,
c.P_TOKEN_TYPE,
@@ -29,6 +29,7 @@ import { showModal } from '../../modals';
import ResponseDebugModal from '../../modals/response-debug-modal';
import type { Settings } from '../../../../models/settings';
import { initNewOAuthSession } from '../../../../network/o-auth-2/misc';
import { convertEpochToMilliseconds } from '../../../../common/misc';
type Props = {
handleRender: Function,
@@ -87,6 +88,20 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
}
}
async _handleUpdateIdentityToken(e: SyntheticEvent<HTMLInputElement>): Promise<void> {
const { oAuth2Token } = this.props;
const identityToken = e.currentTarget.value;
if (oAuth2Token) {
await models.oAuth2Token.update(oAuth2Token, { identityToken });
} else {
await models.oAuth2Token.create({
identityToken,
parentId: this.props.request._id,
});
}
}
async _handleUpdateRefreshToken(e: SyntheticEvent<HTMLInputElement>): Promise<void> {
const { oAuth2Token } = this.props;
const refreshToken = e.currentTarget.value;
@@ -483,7 +498,31 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
return { basic: basicFields, advanced: advancedFields };
}
static renderExpireAt(token: ?OAuth2Token): React.Element<*> | string | null {
static renderIdentityTokenExpiry(token: ?OAuth2Token): React.Element<*> | string | null {
if (!token || !token.identityToken) {
return null;
}
const base64Url = token.identityToken.split('.')[1];
let decodedString = '';
try {
decodedString = window.atob(base64Url);
} catch (error) {
return null;
}
const { exp } = JSON.parse(decodedString);
if (!exp) {
return '(never expires)';
}
const convertedExp = convertEpochToMilliseconds(exp);
return (
<span>
&#x28;expires <TimeFromNow timestamp={convertedExp} />
&#x29;
</span>
);
}
static renderAccessTokenExpiry(token: ?OAuth2Token): React.Element<*> | string | null {
if (!token || !token.accessToken) {
return null;
}
@@ -491,7 +530,6 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
if (!token.expiresAt) {
return '(never expires)';
}
return (
<span>
&#x28;expires <TimeFromNow timestamp={token.expiresAt} />
@@ -541,7 +579,8 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
const { request, oAuth2Token: tok } = this.props;
const { loading, error, showAdvanced } = this.state;
const expireLabel = OAuth2Auth.renderExpireAt(tok);
const accessExpireLabel = OAuth2Auth.renderAccessTokenExpiry(tok);
const identityExpireLabel = OAuth2Auth.renderIdentityTokenExpiry(tok);
const fields = this.renderGrantTypeFields(request.authentication.grantType);
return (
@@ -608,7 +647,17 @@ class OAuth2Auth extends React.PureComponent<Props, State> {
</div>
<div className="form-control form-control--outlined">
<label>
<small>Access Token {tok ? <em>{expireLabel}</em> : null}</small>
<small>Identity Token {tok ? <em>{identityExpireLabel}</em> : null}</small>
<input
value={(tok && tok.identityToken) || ''}
placeholder="n/a"
onChange={this._handleUpdateIdentityToken}
/>
</label>
</div>
<div className="form-control form-control--outlined">
<label>
<small>Access Token {tok ? <em>{accessExpireLabel}</em> : null}</small>
<input
value={(tok && tok.accessToken) || ''}
placeholder="n/a"
+33 -7
View File
@@ -46,15 +46,29 @@ module.exports.templateTags = [
description: 'cookie value by name',
},
{
displayName: 'OAuth 2.0 Token',
displayName: 'OAuth 2.0 Access Token',
value: 'oauth2',
description: 'access token',
/*
This value is left as is and not renamed to 'oauth2-access' so as to not
break the current release's usage of `oauth2`.
*/
},
{
displayName: 'OAuth 2.0 Identity Token',
value: 'oauth2-identity',
},
{
displayName: 'OAuth 2.0 Refresh Token',
value: 'oauth2-refresh',
},
],
},
{
type: 'string',
hide: args => ['url', 'oauth2', 'name', 'folder'].includes(args[0].value),
hide: args =>
['url', 'oauth2', 'oauth2-identity', 'oauth2-refresh', 'name', 'folder'].includes(
args[0].value,
),
displayName: args => {
switch (args[0].value) {
case 'cookie':
@@ -150,11 +164,23 @@ module.exports.templateTags = [
const headerNamesStr = headerNames.map(n => `"${n}"`).join(',\n\t');
throw new Error(`No header with name "${name}".\nChoices are [\n\t${headerNamesStr}\n]`);
case 'oauth2':
const token = await context.util.models.oAuth2Token.getByRequestId(request._id);
if (!token) {
throw new Error('No OAuth 2.0 tokens found for request');
const access = await context.util.models.oAuth2Token.getByRequestId(request._id);
if (!access || !access.accessToken) {
throw new Error('No OAuth 2.0 access tokens found for request');
}
return token.accessToken;
return access.accessToken;
case 'oauth2-identity':
const identity = await context.util.models.oAuth2Token.getByRequestId(request._id);
if (!identity || !identity.identityToken) {
throw new Error('No OAuth 2.0 identity tokens found for request');
}
return identity.identityToken;
case 'oauth2-refresh':
const refresh = await context.util.models.oAuth2Token.getByRequestId(request._id);
if (!refresh || !refresh.refreshToken) {
throw new Error('No OAuth 2.0 refresh tokens found for request');
}
return refresh.refreshToken;
case 'name':
return request.name;
case 'folder':