assets/serve/js/icdemo.js (296 lines of code) (raw):

/* * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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. */ let winParams = new URLSearchParams(window.location.search); let clientId = winParams.get('client_id') || '903cfaeb-57d9-4ef6-5659-04377794ed65'; let clientSecret = winParams.get('client_secret') || '48f7e552-d9b7-42f3-ba76-e5ab5b3c70ab'; // IMPORTANT: Scopes "account_admin" and "link" should only be requested when // the user is actively requesting the account to be modified or linked. let defaultScope = 'openid+offline+profile+identities+ga4gh_passport_v1'; let linkScope = defaultScope + '+account_admin+link'; let loginURL = '_HYDRA_URL_/oauth2/auth?audience=&client_id=' + clientId + '&nonce=_NONCE_&redirect_uri=_REDIRECT_&response_type=code&scope=_SCOPE_&state=_STATE_'; let tokenURL = '_HYDRA_URL_/oauth2/token'; let userinfoURL = '_HYDRA_URL_/userinfo'; let authCodeExchangeToken = 'grant_type=authorization_code&redirect_uri=_REDIRECT_&code=_AUTH_CODE_'; let refreshExchangeToken = 'grant_type=refresh_token&redirect_uri=_REDIRECT_&refresh_token=_REFRESH_TOKEN_'; let realm = 'master'; let accountURL = '_IC_URL_/identity/scim/v2/_REALM_/Me?client_id=' + clientId + '&client_secret=' + clientSecret; let refreshToken = ''; /** * validateState ... * @param {string} stateID * @param {string} nonce * @return {boolean} if the state is valid */ function validateState(stateID, nonce) { let state = window.localStorage.getItem('state'); if (!state) { displayError( `request with invalid 'state' ${stateID}, no 'state' in database`, `app maybe under attack or test page refreshed using same code.`); return false; } let s = JSON.parse(state); if (s.id !== stateID) { displayError( `request with invalid 'state' ${stateID}, 'state' in database is ${ s.id}`, `app maybe under attack.`); return false; } if (s.nonce && s.nonce !== nonce) { displayError( `request with invalid 'nonce' ${nonce}, 'nonce' in database is ${ s.nonce}`, `app maybe under attack.`); return false; } clientId = s.clientId; clientSecret = s.clientSecret; window.localStorage.removeItem('state'); return true; } /** * randomString ... * @param {string} length * @return {string} */ function randomString(length) { let charset = '0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz'; result = ''; while (length > 0) { let bytes = new Uint8Array(16); let random = window.crypto.getRandomValues(bytes); random.forEach(function(c) { if (length == 0) { return; } if (c < charset.length) { result += charset[c]; length--; } }); } return result; } /** * makeURL ... * @param {string} pattern * @param {string} token * @param {string} scope * @param {string} state * @param {string} nonce * @return {string} url */ function makeURL(pattern, token, scope, state, nonce) { let path = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '') + window.location.pathname; let redirect = window.location.href.split('?')[0]; state = state || ''; nonce = nonce || ''; return pattern.replace(/_PATH_/g, encodeURI(path)) .replace(/_AUTH_CODE_/g, encodeURIComponent(token)) .replace(/_REFRESH_TOKEN_/g, encodeURIComponent(token)) .replace(/_REDIRECT_/g, encodeURIComponent(redirect)) .replace(/_HYDRA_URL_/g, encodeURI($('#hydra_url').val())) .replace(/_IC_URL_/g, encodeURI($('#ic_url').val())) .replace(/_TOKEN_/g, encodeURIComponent($('#passport').val())) .replace(/_SCOPE_/g, scope || defaultScope) .replace(/_REALM_/g, realm) .replace(/_STATE_/g, state) .replace(/_NONCE_/g, nonce); } /** * auth starts a login */ function auth() { let stateID = randomString(16); let nonce = ''; // not supplying nonce for code flow let state = { id: stateID, nonce: nonce, clientId: clientId, clientSecret: clientSecret }; window.localStorage.setItem('state', JSON.stringify(state)); let url = makeURL( loginURL, /*token*/ undefined, defaultScope, stateID, nonce); window.location.href = url; } /** * linkauth starts a login for link account. */ function linkauth() { let authCode = $('#auth_code').val(); let tok = $('#access_token').val(); if (authCode && !tok) { displayError( 'must exchange code first (assuming it has the \'link\' scope)...'); return; } if (tok) { window.localStorage.setItem('primary_token', tok); } auth(linkScope); } /** * tokenExchange exchanges authcode to token. */ function tokenExchange() { let authCode = $('#auth_code').val(); if (!authCode) { displayError('must login first...'); return; } let url = makeURL(tokenURL); $.ajax({ url: url, type: 'POST', data: makeURL(authCodeExchangeToken, authCode), beforeSend: function(xhr) { xhr.setRequestHeader( 'Authorization', 'Basic ' + btoa(clientId + ':' + clientSecret)); }, success: function(resp) { $('#log').text('Authorization: ' + JSON.stringify(resp, undefined, 2)); $('#access_token').val(resp.access_token); $('#refresh_token').val(resp.refresh_token || ''); refreshToken = resp.refresh_token; }, error: function(err) { displayError( 'token exchange failed', '', JSON.stringify(err, undefined, 2)); } }); } /** * linkAccount ... * @param {string} authCode */ function linkAccount(authCode) { let primaryToken = window.localStorage.getItem('primary_token'); window.localStorage.removeItem('primary_token'); $('#access_token').val(primaryToken); let url = makeURL(tokenURL); $.ajax({ url: url, type: 'POST', data: makeURL(authCodeExchangeToken, authCode), beforeSend: function(xhr) { xhr.setRequestHeader( 'Authorization', 'Basic ' + btoa(clientId + ':' + clientSecret)); }, success: function(resp) { scimLink(primaryToken, resp.access_token); }, error: function(err) { displayError( 'token exchange (for linking) failed', '', JSON.stringify(err, undefined, 2)); } }); } /** * scimLink links scims * @param {string} primaryToken * @param {string} linkToken */ function scimLink(primaryToken, linkToken) { let url = makeURL(accountURL); let data = `{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"add","path":"emails","value":"X-Link-Authorization"}]}`; $.ajax({ url: url, type: 'PATCH', contentType: 'application/json; charset=utf-8', dataType: 'json', data: data, processData: false, beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + primaryToken); xhr.setRequestHeader('X-Link-Authorization', 'Bearer ' + linkToken); }, success: function(resp) { $('#log').text( 'LINK ACCOUNT SUCCESS:\n\n' + JSON.stringify(resp, undefined, 2)); }, error: function(err, status, info) { displayError( 'link account failed', `status: "${status}", info: "${info}"`, JSON.stringify(err, undefined, 2)); } }); } /** * userinfo fetches userinfo */ function userinfo() { let tok = $('#access_token').val(); if (!tok) { displayError('must login first...'); return; } let url = makeURL(userinfoURL); $.ajax({ url: url, type: 'GET', beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + tok); }, success: function(resp) { $('#log').text('Userinfo: ' + JSON.stringify(resp, undefined, 2)); }, error: function(err) { displayError('user info failed', '', JSON.stringify(err, undefined, 2)); } }); } /** * refresh tokens with refresh token */ function refresh() { if (!refreshToken) { $('#log').text('must login first...'); return; } let url = makeURL(tokenURL); $.ajax({ url: url, type: 'POST', data: makeURL(refreshExchangeToken, refreshToken), beforeSend: function(xhr) { xhr.setRequestHeader( 'Authorization', 'Basic ' + btoa(clientId + ':' + clientSecret)); }, success: function(resp) { $('#log').text('Authorization: ' + JSON.stringify(resp, undefined, 2)); $('#access_token').val(resp.access_token); $('#refresh_token').val(resp.refresh_token); refreshToken = resp.refresh_token; }, error: function(err) { displayError( 'refresh token failed', '', JSON.stringify(err, undefined, 2)); } }); } /** * accountInfo fetches account info */ function accountInfo() { let tok = $('#access_token').val(); if (!tok) { $('#log').text('must login first...'); return; } let url = makeURL(accountURL); $.ajax({ url: url, type: 'GET', beforeSend: function(xhr) { xhr.setRequestHeader('Authorization', 'Bearer ' + tok); }, success: function(resp) { $('#log').text('Account Info:\n\n' + JSON.stringify(resp, undefined, 2)); }, error: function(err) { displayError( 'account info failed', '', JSON.stringify(err, undefined, 2)); } }); } /** * displayError in page * @param {string} error * @param {string} desc * @param {string} hint */ function displayError(error, desc, hint) { $('#error').text(error); $('#error_desc').text(desc || ''); $('#error_hint').text(hint || ''); $('#error_info').removeClass('hidden'); } /** * defaultHydra resets hydra to default */ function defaultHydra() { let url = document.getElementById("hydra_url"); url.value = document.getElementById("default-hydra").dataset.url; } /** * clearPage clears outputs in page */ function clearPage() { window.localStorage.removeItem('primary_token'); window.location.href = makeURL('_PATH_'); } /** * debugJWT open token in jwt * @param {string} selector */ function debugJWT(selector) { let token = $(selector).val(); window.open('https://jwt.io/#debugger-io?token=' + token); } /** * init ... */ function init() { let code = winParams.get('code'); if (code) { $('#auth_code').val(code); validateState(winParams.get('state'), winParams.get('nonce')); if (window.localStorage.getItem('primary_token')) { linkAccount(code); } } // register onclick events document.getElementById("auth").onclick = auth; document.getElementById("linkauth").onclick = linkauth; document.getElementById("default-hydra").onclick = defaultHydra; document.getElementById("clear").onclick = clearPage; document.getElementById("token-exchange").onclick = tokenExchange; document.getElementById("userinfo").onclick = userinfo; document.getElementById("refresh").onclick = refresh; document.getElementById("account-info").onclick = accountInfo; document.getElementById("debug-jwt").onclick = function(){ debugJWT('#access_token'); }; } window.onload = init;