!--
Oracle Field Service Sample plugin
Copyright (c) 2023 Oracle and/or its affiliates.
Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
-->
<!DOCTYPE html>
<html lang="en-us">
<head>
<title>Authorization micro plugin</title>
</head>
<body>
<p>1. Configuration.</p>
<p>Use plugin 'Secure Parameters' configuration to provide clientId, getCodeEndpoint and getTokenEndpoint parameters, or fill them manually.</p>
<div>
<label for="field_get_code_endpoint">getCodeEndpoint: </label><input id="field_get_code_endpoint"><br/>
Example: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
Example: https://{idcsUrl}/oauth2/v1/authorize
</div>
<div>
<label for="field_get_token_endpoint">getTokenEndpoint: </label><input id="field_get_token_endpoint"><br/>
Example: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token
Example: https://{idcsUrl}/oauth2/v1/token
</div>
<div>
<label for="field_rest_url">restUrl: </label><input id="field_rest_url"><br/>
Example: https://graph.microsoft.com/v1.0/me
Example: https://{fusionUrl}/hcmRestApi/resources/latest/selfDetails
</div>
<div><label for="field_client_id">clientId: </label><input id="field_client_id"></div>
<div><label for="field_scope">scope: </label><input id="field_scope"></div>
<hr>
<div><label for="field_ofs_origin">Redirect URI: (used in generation of redirect URI)</label><input disabled id="field_ofs_origin">/plugin-auth-redirect/</div>
<div>redirectUri must be the same while get authorization code and access token</div>
<hr>
<input id="field_include_code_challenge" type="checkbox" value="field_include_code_challenge"> Include Code Challenge
<p>Auto-generated code challenge that will be used in Plugin API Call Procedure request (to obtain Authorization Code) and HTTP fetch (to obtain JWT Access Token).</p>
<div><label for="field_code_verifier">Code Verifier (random string): </label><input id="field_code_verifier"></div>
<div><label for="field_code_challenge">Code Challenge (signature): </label><input id="field_code_challenge"></div>
<button id="button_generate_code_challenge">Regenerate</button>
<p>Auto-generated unique procedure call ID (will be used in Plugin API Call Procedure request).</p>
<div><label for="field_call_id">Procedure call ID: </label><input id="field_call_id"></div>
<button id="button_generate_call_id">Regenerate</button>
<hr>
<p>2. Obtain Code using call procedure 'getAuthorizationCode' of Plugin API to open Secure Tab to authorize and get auth code.</p>
<div><textarea id="field_procedure_request" title="Procedure request" rows="10" cols="100"></textarea></div>
<button id="button_get_auth_code">Get code</button>
<div>List of called procedures:</div>
<pre id="call_procedures_list"></pre>
<div><label for="field_auth_code">Last received code: </label><input id="field_auth_code"></div>
<hr>
<p>3. Obtain Access Token (JWT) using Code by HTTP fetch function.</p>
<!--<button id="button_generate_jwt_fetch_request_for_debug">Generate JWT fetch request</button>-->
<pre id="field_jwt_fetch_request_for_debug"></pre>
<button id="button_get_auth_token">Get JWT</button>
<pre id="field_auth_response"></pre>
<div><label for="field_auth_token">Token: </label><input id="field_auth_token"></div>
<hr>
<p>4. Obtain REST data using JWT Access Token by HTTP fetch function. Please note to request new JWT you need request new code.</p>
<!--<button id="button_generate_rest_data_fetch_request_for_debug">Generate Data fetch request</button>-->
<pre id="field_rest_data_fetch_request_for_debug"></pre>
<button id="button_get_rest_data">Get Data</button>
<pre id="field_rest_data"></pre>
</body>
<script>
const INIT_FLOW = 'init';
const OPEN_FLOW = 'open';
let currentFlow;
let resolveReadyMessage;
let listOfRunProcedures = [];
function getRandomString() {
return crypto.randomUUID().replace(/\+/g, '-');
}
function base64UrlEncode(str) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
async function generateCodeChallenge(codeVerifier) {
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64UrlEncode(digest);
}
async function fillCodeChallenge() {
let codeVerifier = getRandomString();
let codeChallenge = await generateCodeChallenge(codeVerifier);
document.getElementById('field_code_verifier').value = codeVerifier;
document.getElementById('field_code_challenge').value = codeChallenge;
prepareCallProcedureMessage();
}
function handleInitMessage(message, event) {
localStorage.setItem("field_ofs_origin", message.origin);
currentFlow = INIT_FLOW;
let initEndMessage = {
apiVersion: 1,
method: 'initEnd'
};
sendPostMessage(initEndMessage);
resolveReadyMessage();
}
function handleOpenMessage(message, event) {
currentFlow = OPEN_FLOW;
if (!message.allowedProcedures) {
console.error("allowedProcedures is not found in open message", JSON.stringify(message));
}
if (!message.allowedProcedures.getAuthorizationCode) {
console.error("getAuthorizationCode procedure must be allowed in 'open' plugin API message", JSON.stringify(message));
alert('getAuthorizationCode procedure must be allowed');
}
let openParams = message.openParams || {};
let securedData = message.securedData || {};
document.getElementById('field_client_id').value = openParams.clientId || securedData.clientId || '';
document.getElementById('field_scope').value = openParams.scope || securedData.scope || '';
document.getElementById('field_get_code_endpoint').value = openParams.getCodeEndpoint || securedData.getCodeEndpoint || '';
document.getElementById('field_get_token_endpoint').value = openParams.getTokenEndpoint || securedData.getTokenEndpoint || '';
document.getElementById('field_rest_url').value = openParams.restUrl || securedData.restUrl || '';
resolveReadyMessage();
}
function updateListOfProcedures() {
document.getElementById('call_procedures_list').innerText = JSON.stringify(listOfRunProcedures, null, 4);
}
function handleProcedureResultMessage(message, event) {
//just log the response
const foundCallIndex = listOfRunProcedures.find(procedureCall => procedureCall.callId === message.callId);
if (foundCallIndex !== -1) {
foundCallIndex.response = message;
}
updateListOfProcedures();
//keep code if request is successful
if (message.resultData.result === 'completed') {
document.getElementById('field_auth_code').value = message.resultData.code;
generateJwtFetchRequestForConsole();
}
}
function handleProcedureErrorMessage(message, event) {
//just log the response
const foundCallIndex = listOfRunProcedures.find(procedureCall => procedureCall.callId === message.callId);
if (foundCallIndex !== -1) {
foundCallIndex.response = message;
}
updateListOfProcedures();
}
function sendPostMessage(message) {
window.parent.postMessage(message, document.referrer);
}
function prepareCallProcedureMessage() {
let getCodeEndpoint = document.getElementById('field_get_code_endpoint').value.trim();
let clientId = document.getElementById('field_client_id').value.trim();
let redirectUri = encodeURIComponent(document.getElementById('field_ofs_origin').value.trim() + '/plugin-auth-redirect/');
let scope = document.getElementById('field_scope').value.trim();
// let state = document.getElementById('field_client_id').value.trim();
let includeCodeChallenge = document.getElementById('field_include_code_challenge').checked;
let codeChallenge = document.getElementById('field_code_challenge').value.trim();
let callId = document.getElementById('field_call_id').value.trim();
let getCodeUrl = `${getCodeEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}`;
if (includeCodeChallenge) {
getCodeUrl += `&code_challenge_method=S256&code_challenge=${codeChallenge}`;
}
// if (state) {
// getCodeUrl += `&state=${state}`;
// }
let message = {
apiVersion: 1,
method: 'callProcedure',
procedure: "getAuthorizationCode",
callId: callId,
params: {
"url": getCodeUrl
}
};
document.getElementById('field_procedure_request').value = JSON.stringify(message, null, 4);
}
function refreshCallId() {
let callId = getRandomString();
document.getElementById('field_call_id').value = callId;
prepareCallProcedureMessage();
}
function sendCallProcedureMessage() {
let callProcedureMessage = document.getElementById('field_procedure_request').value
try {
let parsedCallProcedureMessage = JSON.parse(callProcedureMessage);
listOfRunProcedures.push({
callId: parsedCallProcedureMessage.callId,
request: parsedCallProcedureMessage,
response: 'Wait for response..'
});
updateListOfProcedures();
sendPostMessage(parsedCallProcedureMessage);
refreshCallId();
} catch(e) {
console.error(e);
}
}
function sendReadyMessage() {
let readyMessage = {
apiVersion: 1,
method: 'ready',
sendInitData: true,
sendMessageAsJsObject: true
};
return new Promise((resolve, reject) => {
sendPostMessage(readyMessage);
resolveReadyMessage = resolve;
})
}
function generateJwtFetchRequestForConsole() {
if (!document.getElementById('field_auth_code').value) {
console.error('Auth code could not be empty');
alert('Auth code could not be empty');
return;
}
let getTokenEndpoint = document.getElementById('field_get_token_endpoint').value.trim();
let clientId = document.getElementById('field_client_id').value.trim();
let redirectUri = document.getElementById('field_ofs_origin').value.trim() + '/plugin-auth-redirect/';
let authCode = document.getElementById('field_auth_code').value.trim();
let codeVerifier = document.getElementById('field_code_verifier').value.trim();
let includeCodeChallenge = document.getElementById('field_include_code_challenge').checked;
let requestParams = {
client_id: clientId,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
code: authCode
};
if (includeCodeChallenge) {
requestParams.code_verifier = codeVerifier;
}
let getAccessTokenBody = new URLSearchParams(requestParams).toString();
let fetchRequestForDebug = `
fetch('${getTokenEndpoint}', {
headers: {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
method: "POST",
body: '${getAccessTokenBody}'
});
`;
document.getElementById('field_jwt_fetch_request_for_debug').innerText = fetchRequestForDebug;
}
async function fetchToken() {
if (!document.getElementById('field_auth_code').value) {
console.error('Auth code could not be empty');
alert ('Auth code could not be empty');
return;
}
let getTokenEndpoint = document.getElementById('field_get_token_endpoint').value.trim();
let clientId = document.getElementById('field_client_id').value.trim();
let redirectUri = document.getElementById('field_ofs_origin').value.trim() + '/plugin-auth-redirect/';
let authCode = document.getElementById('field_auth_code').value.trim();
let codeVerifier = document.getElementById('field_code_verifier').value.trim();
let includeCodeChallenge = document.getElementById('field_include_code_challenge').checked;
let requestParams = {
client_id: clientId,
grant_type: 'authorization_code',
redirect_uri: redirectUri,
code: authCode
}
if (includeCodeChallenge) {
requestParams.code_verifier = codeVerifier;
}
let getAccessTokenBody = new URLSearchParams(requestParams).toString();
let response = await fetch(`${getTokenEndpoint}`, {
"headers": {
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
},
"method": "POST",
"body": getAccessTokenBody
});
if (response.status !== 200) {
console.error('Token was not obtained');
alert ('Token was not obtained');
return;
}
let jsonResponse = await response.json();
document.getElementById('field_auth_response').innerText = JSON.stringify(jsonResponse, null, 4);
if (jsonResponse.access_token) {
document.getElementById('field_auth_token').value = jsonResponse.access_token;
generateRestDataFetchRequestForConsole();
}
}
function generateRestDataFetchRequestForConsole() {
let authToken = document.getElementById('field_auth_token').value.trim();
let restUrl = document.getElementById('field_rest_url').value.trim();
let fetchRequestForDebug = `
fetch("${restUrl}", {
headers: {
authorization: 'Bearer ${authToken}'
}
});
`;
document.getElementById('field_rest_data_fetch_request_for_debug').innerText = fetchRequestForDebug;
}
async function getRestData() {
if (!document.getElementById('field_auth_token').value) {
console.error('Auth token could not be empty');
alert ('Auth token could not be empty');
return;
}
let authToken = document.getElementById('field_auth_token').value.trim();
let restUrl = document.getElementById('field_rest_url').value.trim();
let response = await fetch(restUrl, {
"headers": {
authorization: `Bearer ${authToken}`
}
});
if (response.status !== 200) {
console.error('Data was not obtained');
alert ('Data was not obtained');
return;
}
let jsonResponse = await response.json();
document.getElementById('field_rest_data').innerText = JSON.stringify(jsonResponse, null, 4);
}
function initButtonHandling() {
document.getElementById('button_generate_code_challenge').addEventListener('click', async function () {
await fillCodeChallenge();
});
document.getElementById('button_generate_call_id').addEventListener('click', function () {
refreshCallId();
});
document.getElementById('button_get_auth_code').addEventListener('click', function () {
sendCallProcedureMessage();
});
// document.getElementById('button_generate_jwt_fetch_request_for_debug').addEventListener('click', function () {
// generateJwtFetchRequestForConsole();
// });
document.getElementById('button_get_auth_token').addEventListener('click', async function () {
await fetchToken();
});
// document.getElementById('button_generate_rest_data_fetch_request_for_debug').addEventListener('click', function () {
// generateRestDataFetchRequestForConsole();
// });
document.getElementById('button_get_rest_data').addEventListener('click', async function () {
await getRestData();
});
document.getElementById('field_get_code_endpoint').addEventListener('change', function () {
prepareCallProcedureMessage();
});
document.getElementById('field_include_code_challenge').addEventListener('change', function () {
prepareCallProcedureMessage();
});
document.getElementById('field_get_token_endpoint').addEventListener('change', function () {
generateJwtFetchRequestForConsole();
});
document.getElementById('field_client_id').addEventListener('change', function () {
prepareCallProcedureMessage();
});
document.getElementById('field_rest_url').addEventListener('change', function () {
generateRestDataFetchRequestForConsole();
});
document.getElementById('field_scope').addEventListener('change', function () {
prepareCallProcedureMessage();
});
}
function initPostMessageHandling() {
window.addEventListener("message", function(event) {
if (event.source === window) {
// ignore messages from itself
return;
}
if (new URL(event.origin).host !== new URL(document.referrer).host ) {
console.info("Came message from another origin", JSON.stringify(event.data));
return; // Ensure the message is from the expected origin
}
if (!event.data.apiVersion) {
// is not considered as a message from Plugin API
return;
}
if (!event.data.method) {
console.warn("No 'method' field in post message", JSON.stringify(event.data));
return;
}
switch (event.data.method) {
case 'init':
handleInitMessage(event.data, event);
break;
case 'open':
handleOpenMessage(event.data, event);
break;
case 'callProcedureResult':
handleProcedureResultMessage(event.data, event);
break;
case 'error':
handleProcedureErrorMessage(event.data, event);
break;
}
});
}
initPostMessageHandling();
document.getElementById('field_ofs_origin').value = localStorage.getItem("field_ofs_origin");
sendReadyMessage().then(() => {
if (currentFlow === OPEN_FLOW) {
initButtonHandling();
fillCodeChallenge();
refreshCallId();
}
});
</script>