Skip to content

Commit

Permalink
Merge 868e6c9 into eb07abb
Browse files Browse the repository at this point in the history
  • Loading branch information
huangjoyce3 committed Sep 26, 2019
2 parents eb07abb + 868e6c9 commit 12e03cd
Show file tree
Hide file tree
Showing 12 changed files with 322 additions and 142 deletions.
12 changes: 6 additions & 6 deletions dist/appid.min.js

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions dist/appid.umd.min.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions sample/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
<script type='text/javascript' src="appid.min.js"></script>
<script>
function showError(e) {
console.error(e);
document.getElementById('error').textContent = e;
document.getElementById('login').setAttribute('class', 'button');
console.error(e);
document.getElementById('error').textContent = e;
document.getElementById('login').setAttribute('class', 'button');
}

(async function () {
Expand All @@ -57,7 +57,7 @@
document.getElementById('error').textContent = '';

try {
const tokens = await appID.signinWithPopup();
const tokens = await appID.signin();
let userInfo = await appID.getUserInfo(tokens.accessToken);
let decodeIDToken = tokens.idTokenPayload;
document.getElementById('welcome').textContent = 'Hello, ' + decodeIDToken.name;
Expand Down
6 changes: 3 additions & 3 deletions src/PopupController.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ class PopupController {
this.window = w;
};

open() {
const h = 700;
const w = 400;
open(popup) {
const h = popup.height;
const w = popup.width;
const left = (screen.width - w) / 2;
const top = (screen.height - h) / 2;
this.popup = this.window.open('', 'popup', `left=${left},top=${top},width=${w},height=${h},resizable,scrollbars=yes,status=1`);
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ module.exports = {
INVALID_TOKEN: 'Invalid token',
MISSING_PUBLIC_KEY: 'Cannot find public key',
INVALID_ACCESS_TOKEN: 'Access token must be a string',
MISSING_CLIENT_ID: 'Missing client ID',
MISSING_DISCOVERY_ENDPOINT: 'Missing discovery endpoint',
RESPONSE_TYPE: 'code',
RESPONSE_MODE: 'web_message',
PROMPT: 'none',
Expand Down
8 changes: 1 addition & 7 deletions src/errors/AppIDError.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
class AppIDError extends Error {
constructor({error, description}) {
super(description || error);
this.error = error;
this.description = description;
}
}
class AppIDError extends Error {}
module.exports = AppIDError;
9 changes: 9 additions & 0 deletions src/errors/OAuthError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class OAuthError extends Error {
// See https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for the possible error messages
constructor({error, description}) {
super(description || error);
this.error = error;
this.description = description;
}
}
module.exports = OAuthError;
189 changes: 84 additions & 105 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ const PopupController = require('./PopupController');
const IFrameController = require('./IFrameController');
const OpenIdConfigurationResource = require('./OpenIDConfigurationResource');
const TokenValidator = require('./TokenValidator');
const rs = require('jsrsasign');
const constants = require('./constants');
const AppIDError = require('./errors/AppIDError');

/**
* todo:
* README - detailed desc for each function and errors, params, npm install
* Function comments in index.js
* Move helper functions to different file
* Update tests
* Add popup obj to init - default {height: screen.height * .85, width: 400}
*
*/
class AppID {
constructor(
{
Expand All @@ -31,86 +39,73 @@ class AppID {
this.URL = url;
}

async init({clientId, discoveryEndpoint}) {
/**
* Initialize AppID
* @param clientId - The clientId from the singlepageapp application credentials.
* @param discoveryEndpoint - The discoveryEndpoint from the singlepageapp application credentials.
* @param popup - (optional) The popup configuration.
* @returns {Promise<void>}
* @throws AppIDError - For missing required params.
* @throws RequestError - Any errors during a HTTP request.
*/
async init({clientId, discoveryEndpoint, popup = {height: screen.height * .80, width: 400}}) {
if (!clientId) {
throw new AppIDError(constants.MISSING_CLIENT_ID);
}
if (!discoveryEndpoint) {
throw new AppIDError(constants.MISSING_DISCOVERY_ENDPOINT);
}
await this.openIdConfigResource.init({discoveryEndpoint, requestHandler: this.request});
this.clientId = clientId;
this.popupConfig = popup;
this.utils = new Utils({openId: this.openIdConfigResource, clientId: this.clientId});
}

async signinWithPopup() {
const {codeVerifier, nonce, state, authUrl} = this.getAuthParams();

this.popup.open();
/**
* This will open a login widget in a popup which will prompt the user to enter their credentials.
* After a successful login, the popup will close and tokens are returned.
* @returns {Promise<*|tokens|*>} - The tokens of the authenticated user or an error.
* e.g. {accessToken: 'eyg...', accessTokenPayload: { iss: 'https...' }, idToken: 'eyg...', idTokenPayload: { email: 'example@gmail.com' }}
* @throws AppIDError "Popup closed" - The user closed the popup before authentication was completed.
* @throws TokenError - Any token validation error.
* @throws OAuthError - Any errors from the server. e.g. {error: 'server_error', description: ''}
* @throws RequestError - Any errors during a HTTP request.
*/
async signin() {
const {codeVerifier, nonce, state, authUrl} = this.utils.getAuthParams(this.clientId, this.window.origin);

this.popup.open(this.popupConfig);
this.popup.navigate({authUrl});
const message = await this.popup.waitForMessage({messageType: 'authorization_response'});
this.popup.close();

this.verifyMessage({message, state});
this.utils.verifyMessage({message, state});

let authCode = message.data.code;

return await this.exchangeTokens({authCode, codeVerifier, nonce});
}

async getUserInfo(accessToken) {
if (typeof accessToken !== 'string') {
throw new AppIDError({description: constants.INVALID_ACCESS_TOKEN});
}

return await this.request(this.openIdConfigResource.getUserInfoEndpoint(), {
headers: {
'Authorization': 'Bearer ' + accessToken
}
});
}

getAuthParams({prompt} = {}) {
const codeVerifier = this.utils.getRandomString(constants.CODE_VERIFIER_LENGTH);
const codeChallenge = this.utils.sha256(codeVerifier);
const nonce = this.utils.getRandomString(constants.NONCE_LENGTH);
const state = this.utils.getRandomString(constants.STATE_LENGTH);

let authParams = {
client_id: this.clientId,
response_type: constants.RESPONSE_TYPE,
state: rs.stob64(state),
code_challenge: rs.stob64(codeChallenge),
code_challenge_method: constants.CHALLENGE_METHOD,
redirect_uri: this.window.origin,
response_mode: constants.RESPONSE_MODE,
nonce,
scope: constants.SCOPE
};

if (prompt) {
authParams.prompt = prompt;
}

const authUrl = this.openIdConfigResource.getAuthorizationEndpoint() + '?' + this.utils.buildParams(authParams);
return {
return await this.utils.exchangeTokens({
authCode,
codeVerifier,
nonce,
state,
authUrl
};
}

verifyMessage({message, state}) {
if (message.data.error || message.data.error_description) {
throw new AppIDError({description: message.data.error_description, error: message.data.error});
}

if (rs.b64utos(message.data.state) !== state) {
throw new AppIDError({description: constants.INVALID_STATE});
}
let messageOrigin = message.origin;
let oauthOrigin = new this.URL(this.openIdConfigResource.getAuthorizationEndpoint()).origin;
if (messageOrigin !== oauthOrigin) {
throw new AppIDError({description: constants.INVALID_ORIGIN});
}
windowOrigin: this.window.origin
});
}

/**
* Silent sign in will attempt to authenticate the user in a hidden iframe.
* Sign in will be successful only if there is a Cloud Directory SSO token in the browser.
* You will need to enable SSO on the App ID dashboard.
* Possible errors include:
* User not signed in - there is no Cloud Directory SSO token
* @returns {Promise<object>} - The tokens of the authenticated user.
* e.g. {accessToken: 'eyg...', accessTokenPayload: { iss: 'https...' }, idToken: 'eyg...', idTokenPayload: { email: 'example@gmail.com' }}
* @throws OAuthError - Any errors from the server. e.g. {error: 'access_denied', description: 'User not signed in'}
* @throws AppIDError "Silent sign-in timed out" - The iframe will close after 5 seconds if authentication could not be completed.
* @throws TokenError - Any token validation error.
* @throws RequestError - Any errors during a HTTP request.
*/
async silentSignin() {
const {codeVerifier, nonce, state, authUrl} = this.getAuthParams({prompt: constants.PROMPT});
const {codeVerifier, nonce, state, authUrl} = this.utils.getAuthParams(this.clientId, this.window.origin, constants.PROMPT);

this.iframe.open(authUrl);

Expand All @@ -120,52 +115,36 @@ class AppID {
} finally {
this.iframe.remove();
}

this.verifyMessage({message, state});

this.utils.verifyMessage({message, state});
let authCode = message.data.code;
return await this.exchangeTokens({authCode, codeVerifier, nonce});
}

async exchangeTokens({authCode, nonce, codeVerifier}) {
let issuer = await this.openIdConfigResource.getIssuer();
let params = {
grant_type: 'authorization_code',
redirect_uri: this.window.origin,
code: authCode,
code_verifier: codeVerifier
};

const requestParams = this.utils.buildParams(params);
const tokenEndpoint = this.openIdConfigResource.getTokenEndpoint();

const tokens = await this.request(tokenEndpoint, {
method: 'POST',
headers: {
'Authorization': 'Basic ' + rs.stob64(`${this.clientId}:${codeVerifier}`),
'Content-Type': 'application/x-www-form-urlencoded'
},
body: requestParams
});
const publicKeys = await this.openIdConfigResource.getPublicKeys();

const accessTokenPayload = this.tokenValidator.decodeAndValidate({
token: tokens.access_token,
publicKeys,
issuer,
return await this.utils.exchangeTokens({
clientId: this.clientId,
nonce
authCode,
codeVerifier,
nonce,
openId: this.openIdConfigResource,
windowOrigin: this.window.origin
});
}

const idTokenPayload = this.tokenValidator.decodeAndValidate({
token: tokens.id_token,
publicKeys,
issuer,
clientId: this.clientId,
nonce
});
/**
* This method will made a GET request to the user info endpoint using the access token of the authenticated user.
* @param accessToken - The App ID access token of the user
* @returns {Promise<object>} - The user information for the authenticated user. e.g. {sub: '', email: ''}
* @throws AppIDError "Access token must be a string" - Invalid access token.
* @throws RequestError - Any errors during a HTTP request.
*/
async getUserInfo(accessToken) {
if (typeof accessToken !== 'string') {
throw new AppIDError(constants.INVALID_ACCESS_TOKEN);
}

return {accessToken: tokens.access_token, accessTokenPayload, idToken: tokens.id_token, idTokenPayload}
return await this.request(this.openIdConfigResource.getUserInfoEndpoint(), {
headers: {
'Authorization': 'Bearer ' + accessToken
}
});
}
}

Expand Down
Loading

0 comments on commit 12e03cd

Please sign in to comment.