@aws-amplify/auth
Version:
Auth category of aws-amplify
287 lines (232 loc) • 8.61 kB
text/typescript
/*
* Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/
import { parse } from 'url'; // Used for OAuth parsing of Cognito Hosted UI
import { launchUri } from './urlOpener';
import * as oAuthStorage from './oauthStorage';
import {
OAuthOpts,
isCognitoHostedOpts,
CognitoHostedUIIdentityProvider
} from '../types/Auth';
import {
ConsoleLogger as Logger,
Hub
} from '@aws-amplify/core';
const SHA256 = require("crypto-js/sha256");
const Base64 = require("crypto-js/enc-base64");
const AMPLIFY_SYMBOL = ((typeof Symbol !== 'undefined' && typeof Symbol.for === 'function') ?
Symbol.for('amplify_default') : '@@amplify_default') as Symbol;
const dispatchAuthEvent = (event:string, data:any, message:string) => {
Hub.dispatch('auth', { event, data, message }, 'Auth', AMPLIFY_SYMBOL);
};
const logger = new Logger('OAuth');
export default class OAuth {
private _urlOpener;
private _config;
private _cognitoClientId;
private _scopes;
constructor({
config,
cognitoClientId,
scopes = []
}: {
scopes: string[],
config: OAuthOpts,
cognitoClientId: string
}) {
this._urlOpener = config.urlOpener || launchUri;
this._config = config;
this._cognitoClientId = cognitoClientId;
this._scopes = scopes;
}
public oauthSignIn(
responseType = 'code',
domain: string,
redirectSignIn: string,
clientId: string,
provider: CognitoHostedUIIdentityProvider | string = CognitoHostedUIIdentityProvider.Cognito) {
const state = this._generateState(32);
oAuthStorage.setState(state);
const pkce_key = this._generateRandom(128);
oAuthStorage.setPKCE(pkce_key);
const code_challenge = this._generateChallenge(pkce_key);
const code_challenge_method = 'S256';
const queryString = Object.entries({
redirect_uri: redirectSignIn,
response_type: responseType,
client_id: clientId,
identity_provider: provider,
scopes: this._scopes,
state,
...(responseType === 'code'?{code_challenge}:{}),
...(responseType === 'code'?{code_challenge_method}:{})
}).map(([k,v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
const URL = `https://${domain}/oauth2/authorize?${queryString}`;
logger.debug(`Redirecting to ${URL}`);
this._urlOpener(URL, redirectSignIn);
}
private async _handleCodeFlow(currentUrl: string) {
/* Convert URL into an object with parameters as keys
{ redirect_uri: 'http://localhost:3000/', response_type: 'code', ...} */
const { code } = (parse(currentUrl).query || '')
.split('&')
.map((pairings) => pairings.split('='))
.reduce((accum, [k, v]) => ({ ...accum, [k]: v }), { code: undefined });
if (!code) { return; }
const oAuthTokenEndpoint = 'https://' + this._config.domain + '/oauth2/token';
dispatchAuthEvent(
'codeFlow',
{},
`Retrieving tokens from ${oAuthTokenEndpoint}`
);
const client_id = isCognitoHostedOpts(this._config)
? this._cognitoClientId
: this._config.clientID;
const redirect_uri = isCognitoHostedOpts(this._config)
? this._config.redirectSignIn
: this._config.redirectUri;
const code_verifier = oAuthStorage.getPKCE();
const oAuthTokenBody = {
grant_type: 'authorization_code',
code,
client_id,
redirect_uri,
...(code_verifier ? { code_verifier } : {})
};
logger.debug(`Calling token endpoint: ${oAuthTokenEndpoint} with`, oAuthTokenBody);
const body = Object.entries(oAuthTokenBody)
.map(([k, v]) =>`${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
const { access_token, refresh_token, id_token, error } = await (await fetch(oAuthTokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: typeof URLSearchParams !== 'undefined' ? new URLSearchParams(body) : body
}) as any).json();
if (error) {
throw new Error(error);
}
return {
accessToken: access_token,
refreshToken: refresh_token,
idToken: id_token
};
}
private async _handleImplicitFlow(currentUrl: string) {
const { id_token, access_token } = parse(currentUrl).hash
.substr(1) // Remove # from returned code
.split('&')
.map((pairings) => pairings.split('='))
.reduce((accum, [k, v]) => ({ ...accum, [k]: v }), {
id_token: undefined, access_token: undefined
});
dispatchAuthEvent(
'implicitFlow',
{},
`Got tokens from ${currentUrl}`
);
logger.debug(`Retrieving implicit tokens from ${currentUrl} with`);
return {
accessToken: access_token,
idToken: id_token,
refreshToken: null
};
}
public async handleAuthResponse(currentUrl?: string) {
const urlParams = currentUrl ? {
...(parse(currentUrl).hash || '#').substr(1)
.split('&')
.map(entry => entry.split('='))
.reduce((acc, [k, v]) => (acc[k] = v, acc), {}),
...(parse(currentUrl).query || '')
.split('&')
.map(entry => entry.split('='))
.reduce((acc, [k, v]) => (acc[k] = v, acc), {})
} as any : {};
const { error, error_description } = urlParams;
if (error) {
throw new Error(error_description);
}
this._validateState(urlParams);
logger.debug(`Starting ${this._config.responseType} flow with ${currentUrl}`);
if (this._config.responseType === 'code') {
return this._handleCodeFlow(currentUrl);
} else {
return this._handleImplicitFlow(currentUrl);
}
}
private _validateState(urlParams: any) {
if (!urlParams) { return; }
const savedState = oAuthStorage.getState();
const { state: returnedState } = urlParams;
// This is because savedState only exists if the flow was initiated by Amplify
if (savedState && savedState !== returnedState) {
throw new Error('Invalid state in OAuth flow');
}
}
public async signOut() {
let oAuthLogoutEndpoint = 'https://' + this._config.domain + '/logout?';
const client_id = isCognitoHostedOpts(this._config)
? this._cognitoClientId
: this._config.oauth.clientID;
const signout_uri = isCognitoHostedOpts(this._config)
? this._config.redirectSignOut
: this._config.returnTo;
oAuthLogoutEndpoint += Object.entries({
client_id,
logout_uri: encodeURIComponent(signout_uri)
}).map(([k, v]) => `${k}=${v}`).join('&');
dispatchAuthEvent(
'oAuthSignOut',
{oAuth: 'signOut'},
`Signing out from ${oAuthLogoutEndpoint}`
);
logger.debug(`Signing out from ${oAuthLogoutEndpoint}`);
this._urlOpener(oAuthLogoutEndpoint);
}
private _generateState(length: number) {
let result = '';
let i = length;
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
for (; i > 0; --i) result += chars[Math.round(Math.random() * (chars.length - 1))];
return result;
}
private _generateChallenge(code:string) {
return this._base64URL(SHA256(code));
}
private _base64URL(string) {
return string.toString(Base64).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
}
private _generateRandom(size: number) {
const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
const buffer = new Uint8Array(size);
if (typeof window !== 'undefined' && !!(window.crypto)) {
window.crypto.getRandomValues(buffer);
} else {
for (let i = 0; i < size; i += 1) {
buffer[i] = (Math.random() * CHARSET.length) | 0;
}
}
return this._bufferToString(buffer);
}
private _bufferToString(buffer: Uint8Array) {
const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const state = [];
for (let i = 0; i < buffer.byteLength; i += 1) {
const index = buffer[i] % CHARSET.length;
state.push(CHARSET[index]);
}
return state.join('');
}
}