Simple Authorization Plugin (Pure JavaScript)

This topic provides step-by-step instructions to use a simple JavaScript plugin to showcase the OAuth Authorization Code flow within Oracle Fusion Field Service.

The plugin enables you to:

  • Obtain an Authorization Code,
  • Retrieve a JWT (JSON Web Token), and
  • Leverage the JWT to access data from a REST API, with Fusion as an example.
    Note: This plugin is intended for demonstration and testing to understand the authorization flow. It works with Oracle Fusion Field Service version 25B and later.

Prerequisites

Before using this plugin, ensure that:

  • IDCS Application Configuration: You have a properly configured application within Oracle Identity Cloud Service (IDCS) or your chosen Identity Provider. This application is set up to support the OAuth Authorization Code grant type.

  • Plugin Installation: The Simple Authorization Plugin is successfully installed and configured within your Oracle Fusion Field Service environment.

Plugin Parameters

These parameters are configured at the plugin level and define the endpoints and credentials required for the authorization flow. Your administrator will typically configure these.

  • getCodeEndpoint: The authorization endpoint of your Identity Provider. This is where the user will be redirected to authenticate and authorize the application. URL for obtaining the authorization code, For example:
    • IDCS: https://{idcsUrl}/oauth2/v1/authorize
    • Microsoft: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize
  • getTokenEndpoint: The token endpoint of your Identity Provider. This endpoint exchanges the Authorization Code for an Access Token (JWT). URL for obtaining the token (For example:
    • IDCS: https://{idcsUrl}/oauth2/v1/token
    • Microsoft: https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token)
  • restUrl: The URL of the REST API you want to access. (For example: IDCS: https://{fusionUrl}/hcmRestApi/resources/latest/selfDetails, Microsoft: https://graph.microsoft.com/v1.0/me)
  • clientId: The client ID of the application you configured in your Identity Provider. This identifies your application to the Identity Provider.
  • scope: The permissions your application requests from the user. These are defined in your Identity Provider application configuration.
    Example of plugin parameters configuration:
    This screenshot shows the plugin parameters.
    Example of button parameters configuration:
    This screenshot shows the plugin parameters.

Basic Plugin Workflow

You can utilize the Simple Authorization Plugin to go through the complete OAuth 2.0 Authorization Code flow and interact with protected REST APIs. Here's a step-by-step guide on how to use it.

  1. Click Get Code within the plugin's user interface in Oracle Fusion Field Service. You will be automatically redirected to the login page of your configured Identity Provider. On desktop browsers, a new popup window or a separate tab appears if you are using the Oracle Fusion Field Service Mobile Application for user login.
  2. On the Identity Provider's login page, enter your username and password to authenticate your identity.
    Note: If you have an active Single Sign-On (SSO) session, you might be automatically redirected to Oracle Fusion Field Service without having to enter your credentials again.
  3. Upon successful authentication and authorization, the Identity Provider will redirect you to the Oracle Fusion Field Service application. The URL in your browser will now contain an Authorization Code. This code is a temporary, single-use credential.
  4. Click Get JWT. The plugin will communicate with the Identity Provider's token endpoint to obtain the access token (JWT). Upon a successful request, the Identity Provider will respond with an Access Token. This token is a JWT (JSON Web Token) and is a temporary credential granting access to protected resources.
  5. Click Get Data to retrieve data from the REST API (restUrl from plugin parameters). The plugin will use the obtained JWT to request to the configured REST API endpoint in the background. If the Access Token is valid and the API call is successful, the REST API will respond with the requested data. This data will be displayed within the Simple Authorization Plugin user interface in Oracle Fusion Field Service.
  6. This plugin can also be used to test the flow on pure JS to check if everything works fine. Moreover, you could use an access token to authorize the wizard of the REST API backend in the creation of a VBCS application (it is applicable if the VBCS instance has no access to REST API without authorization).
    Here is an example plugin screen with successfully received code:
    This screenshot shows the plugin screen with successfully received code.

    The listing of plugin's code:

    !--
    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>