Skip to content

Commit

Permalink
Merge 407dcd7 into 0e78e0a
Browse files Browse the repository at this point in the history
  • Loading branch information
huangjoyce3 committed Aug 30, 2019
2 parents 0e78e0a + 407dcd7 commit dd5cceb
Show file tree
Hide file tree
Showing 17 changed files with 389 additions and 83 deletions.
20 changes: 10 additions & 10 deletions dist/appid.js

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions errors/AppIDError.js

This file was deleted.

8 changes: 8 additions & 0 deletions errors/AuthenticationError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AuthenticationError extends Error {
constructor({error, description}) {
super(description || error);
this.error = error;
this.description = description;
}
}
module.exports = AuthenticationError;
2 changes: 2 additions & 0 deletions errors/IFrameError.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class IFrameError extends Error {}
module.exports = IFrameError;
11 changes: 11 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
"test": "mocha",
"coverage": "nyc --reporter=lcov mocha"
},
"nyc": {
"all": true,
"include": [
"src/*.js"
],
"exclude": [
"**/PopupController.js",
"**/IFrameController.js",
"**/OpenIDConfigurationResource.js"
]
},
"dependencies": {
"jsrsasign": "^8.0.12",
"node-fetch": "^2.6.0"
Expand Down
80 changes: 80 additions & 0 deletions sample/public/auth0.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Sample App</title>
<link rel='stylesheet' href='index.css'/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<div class="main-container">
<div class="logo">
<img class="logo-icon" src="images/app_id_icon.svg">
</div>
<div class="text-div" id="header">
Welcome to your<br/>App ID SPA sample
</div>
<div class="text-div" id="welcome">
</div>
<div class="flex-bottom" id="error"></div>
<div id="login-button-div" class="flex-top">
<button id="login" class="button">Login</button>
</div>
<div class="bottom-container">
<div class="flex-bottom" id="id_token"></div>
<div class="flex-bottom" id="user_info"></div>
</div>
</div>
</div>
<script type='text/javascript' src="appid.js"></script>
<script src="https://cdn.auth0.com/js/auth0-spa-js/1.0/auth0-spa-js.production.js"></script>
<script>
(async function () {
const auth0 = await createAuth0Client({
domain: 'customsamlappid.auth0.com',
client_id: 'dWyjoqtt6eh_fGLmv6VDTpolILYD3GDb'
});
try {
const token = await auth0.getTokenSilently();
document.getElementById('id_token').textContent = JSON.stringify(token);
// await appID.init({
// clientId: '1b0e3658-fc1f-402e-843c-18402d4dbe58',
// discoveryEndpoint: 'http://7cd6e215.ngrok.io/oauth/v4/5b1eb5f1-34bd-41fd-b6dd-e257c188a4dd/.well-known/openid-configuration'
// });
} catch (e) {
document.getElementById('error').textContent = 'failed';
}
// try {
// const tokens = await appID.silentSignin();
// if (tokens) {
// document.getElementById('id_token').textContent = JSON.stringify(tokens.idTokenPayload);
// } else {
// document.getElementById('login').setAttribute('class', 'button');
// }
// } catch (e) {
// document.getElementById('error').textContent = e;
// document.getElementById('login').setAttribute('class', 'button');
// }
document.getElementById('login').addEventListener('click', async () => {
document.getElementById('error').textContent = '';
document.getElementById('login').setAttribute('class', 'hidden');
let signin = await auth0.loginWithPopup();
//logged in. you can get the user profile like this:
const user = await auth0.getUser();
console.log(user);
// try {
// const tokens = await appID.signinWithPopup();
// let userInfo = await appID.getUserInfo(tokens.accessToken);
// document.getElementById("login").style.display = 'none';
// let decodeIDToken = tokens.idTokenPayload;
// document.getElementById('welcome').innerHTML = 'Hello, ' + decodeIDToken.name;
// document.getElementById('id_token').textContent = JSON.stringify(decodeIDToken);
document.getElementById('user_info').textContent = JSON.stringify(user);
// } catch (e) {
// document.getElementById('error').textContent = e;
// }

});
})()
</script>
</body>
</html>
17 changes: 12 additions & 5 deletions sample/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
</div>
<div class="text-div" id="welcome">
</div>
<div class="flex-bottom" id="error"></div>
<div id="login-button-div" class="flex-top">
<button id="login" class="button hidden">Login</button>
</div>
Expand All @@ -34,12 +33,20 @@
clientId: '<SPA_CLIENT_ID>',
discoveryEndpoint: '<WELL_KNOWN_ENDPOINT>'
});
document.getElementById('login').setAttribute('class', 'button');
} catch (e) {
document.getElementById('error').textContent = e;
console.error(e);
}
try {
const tokens = await appID.silentSignin();
if (tokens) {
document.getElementById('id_token').textContent = JSON.stringify(tokens.idTokenPayload);
}
} catch (e) {
console.error(e);
document.getElementById('login').setAttribute('class', 'button');
}

document.getElementById('login').addEventListener('click', async () => {
document.getElementById('login').setAttribute('class', 'hidden');
try {
const tokens = await appID.signinWithPopup();
let userInfo = await appID.getUserInfo(tokens.accessToken);
Expand All @@ -49,7 +56,7 @@
document.getElementById('id_token').textContent = JSON.stringify(decodeIDToken);
document.getElementById('user_info').textContent = JSON.stringify(userInfo);
} catch (e) {
document.getElementById('error').textContent = e;
console.error(e);
}
});
})()
Expand Down
36 changes: 36 additions & 0 deletions src/IFrameController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const IFrameError = require('../errors/IFrameError');

class IFrameController {
constructor({w = window} = {}) {
this.window = w;
};

open(url) {
this.iFrame = this.window.document.createElement('iframe');
this.iFrame.src = url;
this.iFrame.width = 0;
this.iFrame.height = 0;
this.window.document.body.appendChild(this.iFrame);
}

remove() {
window.document.body.removeChild(this.iFrame);
}

async waitForMessage({messageType}) {
return new Promise((resolve, reject) => {
const timer = setInterval(() => {
reject(new IFrameError('Silent sign-in timed out'));
}, 5 * 1000);
window.addEventListener('message', message => {
clearInterval(timer);
if (!message.data || message.data.type !== messageType) {
return;
}

resolve(message);
});
});
}
}
module.exports = IFrameController;
9 changes: 0 additions & 9 deletions src/PopupController.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,6 @@ class PopupController {
}
};

openIFrame(url) {
this.iFrame = this.window.document.createElement('iframe');
this.iFrame.src = url;
this.iFrame.width = 0;
this.iFrame.height = 0;

window.document.body.appendChild(this.iFrame);
}

navigate({authUrl}) {
this.popup.location.href = authUrl;
};
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ module.exports = {
INVALID_NONCE: 'Invalid nonce',
EXPIRED_TOKEN: 'Expired token',
INVALID_STATE: 'Invalid state',
INVALID_ORIGIN: 'Invalid origin',
INVALID_TOKEN: 'Invalid token',
MISSING_PUBLIC_KEY: 'Cannot find public key',
INVALID_ACCESS_TOKEN: 'Access token must be a string',
RESPONSE_TYPE: 'code',
RESPONSE_MODE: 'web_message',
PROMPT: 'none',
SCOPE: 'openid',
STATE_LENGTH: 20,
NONCE_LENGTH: 20,
Expand Down
92 changes: 69 additions & 23 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
const Utils = require('./utils');
const RequestHandler = require('./RequestHandler');
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');
const AuthenticationError = require('../errors/AuthenticationError');

class AppID {
constructor(
{
popup = new PopupController(),
iframe = new IFrameController(),
tokenValidator = new TokenValidator(),
openID = new OpenIdConfigurationResource(),
utils = new Utils(),
requestHandler = new RequestHandler(),
w = window
w = window,
url = URL
} = {}) {

this.popup = popup;
this.iframe = iframe;
this.tokenValidator = tokenValidator;
this.openIdConfigResource = openID;
this.utils = utils;
this.request = requestHandler.request;
this.window = w;
this.URL = url;
}

async init({clientId, discoveryEndpoint}) {
Expand All @@ -32,6 +37,33 @@ class AppID {
}

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

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

this.verifyMessage({message, state});

let authCode = message.data.code;

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

async getUserInfo(accessToken) {
if (typeof accessToken !== 'string') {
throw new AuthenticationError({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);
Expand All @@ -45,38 +77,51 @@ class AppID {
code_challenge_method: constants.CHALLENGE_METHOD,
redirect_uri: this.window.origin,
response_mode: constants.RESPONSE_MODE,
nonce: nonce,
scope: constants.SCOPE
nonce,
scope: constants.SCOPE,
prompt
};

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

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

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

if (rs.b64utos(message.data.state) !== state) {
throw new AppIDError({description: constants.INVALID_STATE});
throw new AuthenticationError({description: constants.INVALID_STATE});
}
let messageOrigin = message.origin;
let oauthOrigin = new this.URL(this.openIdConfigResource.getAuthorizationEndpoint()).origin;
if (messageOrigin !== oauthOrigin) {
throw new AuthenticationError({description: constants.INVALID_ORIGIN});
}
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});
async silentSignin() {
const {codeVerifier, nonce, state, authUrl} = this.getAuthParams({prompt: constants.PROMPT});

this.iframe.open(authUrl);

let message;
try {
message = await this.iframe.waitForMessage({messageType: 'authorization_response'});
} finally {
this.iframe.remove();
}

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

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

async exchangeTokens({authCode, nonce, codeVerifier}) {
Expand Down Expand Up @@ -120,4 +165,5 @@ class AppID {
return {accessToken: tokens.access_token, accessTokenPayload, idToken: tokens.id_token, idTokenPayload}
}
}

module.exports = AppID;

0 comments on commit dd5cceb

Please sign in to comment.