Authentication and Authorization

Authentication verifies who a user is. Authorization determines what that user can do in Trusted Answer Search and which search spaces the user can access.

Authentication

The following sections describe Admin application sign-in and an OCI IAM sign-in example for an application that hosts the search widget. The widget example configures sign-in for the host application separately from Admin application authentication.

Admin Application Authentication

The guided installation configures the Admin application to use Database Accounts authentication, with TASADMIN as the initial Search Administrator.

Use this default configuration to follow Create Your First Search Space.

Database Accounts

Sign in to the Admin application as TASADMIN, using the password you set during installation. The guided installer configures this authentication scheme automatically.

Authentication scheme configured for Database Accounts

Description of the illustration authentication-scheme-database-accounts.png

Social Sign-In

Social Sign-In authenticates users through an external identity provider. It requires separate configuration in Oracle APEX and the identity provider. See Social Sign-In in the Oracle APEX App Builder User’s Guide.

Users also require Trusted Answer Search roles and search space access. See Managing User Access.

Authentication scheme configured for Social Sign-In

Description of the illustration authentication-scheme-social-sign-in.png

Configure SSO for an Application Hosting the Search Widget

Configure Oracle Cloud Infrastructure Identity and Access Management (OCI IAM) to authenticate users of a local Trusted Answer Search widget example.

The example uses an OCI IAM identity domain, a Trusted Answer Search database with Oracle REST Data Services (ORDS), and a locally hosted web page. The host page signs in the user and obtains an access token. ORDS validates the access token before processing requests to the Trusted Answer Search REST API.

The browser uses the OAuth 2.0 Authorization Code flow with Proof Key for Code Exchange (PKCE). Database credentials remain in the installation environment; the browser does not connect directly to the database.

Before You Begin

Prepare the following:

The local page uses these values:

Setting Value
Redirect URL http://localhost:8080/widget-example.html
Browser origin http://localhost:8080
API application name TAS Widget API
Browser application name TAS Widget Client
API scope suffix search_access

Use localhost consistently in the redirect URL and browser origin. Replace the example database and identity-domain values with values from your environment. TAS Widget Client is the name of the OCI IAM browser client in this example.

The screenshots illustrate the example configuration. Use your own API audience and application values rather than copying environment-specific values from an image.

Note: HTTP is used only for the local example. Use HTTPS for a deployed application. This example does not implement automatic token refresh or identity-provider logout; see Session Handling and Limitations.

Register the TAS API in OCI IAM

Create a resource application to represent the TAS ORDS API.

  1. Open your identity domain in the OCI Console and select Integrated applications.
  2. Select Add application, Confidential Application, and Launch workflow.

    The Confidential Application option is selected for the TAS API registration.

    Select Confidential Application for the TAS API

    Description of the illustration widget-sso-confidential-application.png

    Name the application TAS Widget API and enter a description for the TAS ORDS registration.

    Enter the TAS Widget API name and description

    Description of the illustration widget-sso-resource-details.png

  3. Configure the application as a resource server. For this example, set the access-token lifetime to 3600 seconds and leave token refresh disabled.
  4. Set Primary audience to the value that you will configure as JWT_AUDIENCE in the widget installer. For example, use https://<public-ords-host>/ords/trusted-search/. Preserve the exact path, capitalization, and trailing slash.
  5. Add the scope suffix search_access and enable user consent for this example. Record the full scope value from the registration for use by the browser application.

  6. Leave client configuration disabled for this API registration. Save and activate the application. Confirm activation of TAS Widget API in the Activate application dialog.

For OCI IAM field descriptions, see Adding a Confidential Application.

Register the Browser Application

Create a separate public client for the page that hosts the widget.

  1. From Integrated applications, select Add application, Mobile Application, and Launch workflow. Name the application TAS Widget Client.

    Select Mobile Application for the public browser client.

    Select Mobile Application for the browser client

    Description of the illustration widget-sso-mobile-application.png

  2. Enable the Authorization code grant. Leave the Implicit, Device code, and Refresh token grants disabled for this example.
  3. For local development, enable Allow non-HTTPS URLs. Register http://localhost:8080/widget-example.html as the exact Redirect URL.

    The client configuration shows the Authorization code grant selected and the localhost redirect URL entered for this example.

    Configure Authorization code and the localhost redirect URL

    Description of the illustration widget-sso-code-redirect.png

  4. Leave logout URLs empty for this local-session example and keep Bypass consent disabled. Select a client IP policy that permits requests from the example host.
  5. In the Token issuance policy section, use Add scope to associate the TAS API scope created in the previous section. Creating the API scope alone does not associate it with this client. Do not grant IAM administrator application roles to this browser client.
  6. Finish and activate the application. Assign the intended users or groups if application grants are enforced, and record the browser application’s Client ID.

OCI IAM uses the Mobile Application type for a public client that cannot keep a client secret. Do not add a client secret to the web page. See Adding a Mobile Application.

The browser example uses Authorization Code with PKCE S256. See Authorization Code Grant Type.

Configure IAM Browser Access and Collect Metadata

Allow the local page to call IAM and obtain the values needed by the widget installer and browser application.

  1. In the identity domain’s session settings, enable Allow cross-origin resource sharing (CORS) and add http://localhost:8080 to the allowed origins. Preserve existing entries and save the change. See Changing Data Sharing Settings.

    The Data sharing settings section shows CORS enabled and http://localhost:8080 in Allowed CORS domain names.

    Allow the localhost origin in OCI IAM CORS settings

    Description of the illustration widget-sso-iam-cors.png

  2. Read the identity domain’s OpenID Connect discovery document. Replace <identity-domain-host> with your domain host. This command requires curl and jq.

    curl -sS \
      -H 'Accept: application/json' \
      'https://<identity-domain-host>/.well-known/openid-configuration' |
    jq '{issuer, jwks_uri, authorization_endpoint, token_endpoint}'
  3. Record the returned values without changing their paths or trailing slashes.
  4. Verify that the JWK Set URL can be read without interactive login and is reachable from the database/ORDS environment. Browser access alone does not verify this connectivity.
Discovery or registration value Where to use it
issuer Widget installer JWT_ISSUER
jwks_uri Widget installer JWT_JWK_URL
API primary audience Widget installer JWT_AUDIENCE
API scope suffix search_access Widget installer JWT_SCOPE_NAME
Full API scope from the registration Browser authorization request, together with openid profile
authorization_endpoint Browser sign-in request
token_endpoint Browser authorization-code exchange
Browser application Client ID Browser authorization and token requests

Install the Widget Endpoints with JWT Authentication

Extract widget_ship.zip and configure install_widget.conf using values collected in the preceding sections.

DB_CONNECT_STRING='<database-connect-string>'
INSTALLER_USER='<installer-user>'
INSTALLER_USER_PASSWORD='<installer-password>'
TASADMIN_PASSWORD='<tasadmin-password>'
JWT_AUTHENTICATION_ENABLED=TRUE
JWT_ISSUER='<issuer-from-discovery>'
JWT_AUDIENCE='<api-primary-audience>'
JWT_JWK_URL='<jwks-uri-from-discovery>'
JWT_SCOPE_NAME='search_access'

Set JWT_AUTHENTICATION_ENABLED=TRUE for this SSO example and supply JWT_ISSUER, JWT_AUDIENCE, and JWT_JWK_URL together. The installer rejects incomplete JWT configuration. Keep the configuration file outside the web directory and source control because it contains database passwords.

From the extracted widget directory, run:

./install_widget.sh --config install_widget.conf

Review the installation output and install_widget.log for failures. Record the actual API base URL, including its trailing slash, for example:

https://<public-ords-host>/ords/trusted-search/search-api/

For installer prerequisites and the identity response fields, see Administrator Setup.

Allow the Local Origin in TAS

Configure CORS separately for the TAS API. The IAM setting in the preceding section permits calls to IAM; it does not configure browser access to ORDS.

Connect as TASADMIN and update TRUSTED_SEARCH_WIDGET_ALLOWED_ORIGINS. When other origins are already configured, include them in the comma-separated value to retain their access.

BEGIN
  dbms_trusted_search.update_config_parameter(
    p_name  => 'TRUSTED_SEARCH_WIDGET_ALLOWED_ORIGINS',
    p_value => 'http://localhost:8080'
  );
END;
/
COMMIT;

An origin contains the scheme, host, and optional port, without a path. Use exact origins rather than a wildcard. The browser’s preflight request must permit the headers used by the API, including Authorization, X-Q, X-N, and Content-Type.

Create the Example Website

Create a separate website directory with the following layout. Copy search_bar_widget.js from the matching release’s widget package into assets. Create the other three files using the code in this section.

website/
  widget-example.html
  auth.js
  serve.py
  assets/
    search_bar_widget.js

Keep database configuration files, passwords, and wallets outside this directory. The local server serves files from the directory that contains serve.py.

Create the Host Page

Save the following as widget-example.html. The page loads auth.js, which completes sign-in and calls identity/ before loading the widget.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer">
<title>Wikimedia | Trusted Answer Search</title>
<style>
:root{font-family:system-ui,sans-serif;color:#252a2c;background:#f5f4f0}body{margin:0}header{background:#183b38;color:white;padding:24px 7%;display:flex;justify-content:space-between;align-items:center}header span{font-size:13px;letter-spacing:.1em}main{max-width:880px;margin:70px auto;padding:0 24px}h1{font-size:44px;letter-spacing:-.04em;margin-bottom:12px}p{line-height:1.6}button{background:#bf4938;color:white;border:0;border-radius:7px;padding:12px 20px;font:inherit;cursor:pointer}button:disabled{opacity:.6;cursor:wait}.card{background:white;padding:28px;border:1px solid #ddd;border-radius:12px;margin-top:30px}#status{padding:14px 0}#status[data-error=true]{color:#a3261d}pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:13px}#widget-container{min-height:90px}footer{color:#656a69;font-size:13px;margin-top:32px}
</style>
<script src="/auth.js" defer></script>
</head>
<body>
<header><span>ORACLE TRUSTED ANSWER SEARCH</span><button id="session-button">Sign in with OCI IAM</button></header>
<main><h1>Explore Wikimedia</h1><p>Sign in with your OCI IAM account, then search the Wikimedia Search Space.</p>
<div id="status" role="status" aria-live="polite">Ready to sign in.</div>
<section class="card"><div id="widget-container">Search will appear after sign-in and the TAS identity check.</div></section>
<details id="identity-panel" hidden><summary>TAS identity response</summary><pre id="identity"></pre></details>
<footer>Demo: access tokens remain in memory. When the session expires, sign in again. Clearing this page’s session does not sign you out of OCI IAM.</footer>
</main></body></html>
Add the Authentication Code

Save the following as auth.js. Replace each placeholder in CONFIG before running the example.

Setting Value to supply
clientId Client ID of the TAS Widget Client public application.
authorizationEndpoint authorization_endpoint from your identity domain’s discovery document.
tokenEndpoint token_endpoint from the same discovery document.
redirectUri Exact registered redirect URL; retain the supplied localhost URL for this example.
scope openid profile followed by the full TAS API scope associated with the public client. Replace <scope> with the full registered scope, not just its suffix.
apiBase Installed API base URL ending in /trusted-search/search-api/, including the trailing slash.
searchSpace The published wikimedia Search Space that the signed-in user can access.

The example stores the access token in memory. Session storage holds only the temporary PKCE transaction. The synchronous getAuthHeaders function supplies the current bearer token to widget requests.

'use strict';
const CONFIG = Object.freeze({
  clientId: '<client-ID>',
  authorizationEndpoint: '<AuthorizationEndpoint>',
  tokenEndpoint: '<TokenEndpoint>',
  redirectUri: 'http://localhost:8080/widget-example.html',
  scope: 'openid profile <scope>',
  apiBase: '<ORDS_ENDPOINT>',
  searchSpace: 'wikimedia'
});
const statusElement = document.querySelector('#status');
const button = document.querySelector('#session-button');
const container = document.querySelector('#widget-container');
const transactionKey = 'tas-demo-pkce';
let accessToken = '';
let expiresAt = 0;
let expiryTimer;
function status(message, error = false) { statusElement.textContent = message; statusElement.dataset.error = String(error); }
function base64url(bytes) { return btoa(String.fromCharCode(...bytes)).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); }
function random() { return base64url(crypto.getRandomValues(new Uint8Array(32))); }
function clearSession() {
  accessToken = ''; expiresAt = 0; clearTimeout(expiryTimer);
  sessionStorage.removeItem(transactionKey);
  location.replace(CONFIG.redirectUri);
}
async function signIn() {
  if (`${location.origin}${location.pathname}` !== CONFIG.redirectUri) throw new Error('Open http://localhost:8080/widget-example.html to sign in.');
  button.disabled = true;
  const verifier = random();
  const state = random();
  const challenge = base64url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))));
  sessionStorage.setItem(transactionKey, JSON.stringify({ verifier, state, createdAt: Date.now() }));
  const url = new URL(CONFIG.authorizationEndpoint);
  url.search = new URLSearchParams({client_id: CONFIG.clientId, response_type: 'code', redirect_uri: CONFIG.redirectUri,
    scope: CONFIG.scope, state, nonce: random(), code_challenge: challenge, code_challenge_method: 'S256'}).toString();
  location.assign(url.href);
}
function showError(error) {
  status(error instanceof TypeError ? 'Network request failed. Check IAM or TAS CORS settings and browser Network details.' : error.message, true);
  button.disabled = false;
}
button.addEventListener('click', () => {
  if (accessToken) clearSession();
  else signIn().catch(showError);
});
function expire() {
  accessToken = ''; expiresAt = 0;
  container.replaceChildren(document.createTextNode('Session expired. Sign in again to continue searching.'));
  button.textContent = 'Sign in with OCI IAM'; button.disabled = false;
  document.querySelector('#identity-panel').hidden = true;
  document.querySelector('#identity').textContent = '';
  status('Session expired. Sign in again.');
}
function getAuthHeaders() {
  if (!accessToken || Date.now() >= expiresAt) { if (accessToken) expire(); return {}; }
  return { Authorization: `Bearer ${accessToken}` };
}
async function initialize() {
  const params = new URLSearchParams(location.search);
  const code = params.get('code');
  const oauthError = params.get('error');
  if (!code && !oauthError) return;
  const saved = sessionStorage.getItem(transactionKey);
  sessionStorage.removeItem(transactionKey);
  history.replaceState({}, '', CONFIG.redirectUri);
  const transaction = saved ? JSON.parse(saved) : null;
  if (!transaction || transaction.state !== params.get('state') || Date.now() - transaction.createdAt > 600000) {
    throw new Error('Login response is missing a valid, recent state. Please sign in again.');
  }
  if (oauthError) {
    const detail = (params.get('error_description') || '').slice(0, 1200);
    throw new Error(`OCI sign-in was not completed (${oauthError}). ${detail || 'Check that TAS Widget Client is active and permits the Authorization code grant.'}`);
  }
  button.disabled = true; status('Completing OCI sign-in…');
  const response = await fetch(CONFIG.tokenEndpoint, {
    method: 'POST', credentials: 'omit', cache: 'no-store',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({grant_type: 'authorization_code', client_id: CONFIG.clientId,
      redirect_uri: CONFIG.redirectUri, code, code_verifier: transaction.verifier})
  });
  if (!response.ok) throw new Error(`Token exchange failed (HTTP ${response.status}). Check the client registration, redirect URL and scope.`);
  const tokens = await response.json();
  const lifetime = Number(tokens.expires_in);
  if (!tokens.access_token || !Number.isFinite(lifetime) || lifetime <= 30) throw new Error('IAM did not return a usable access token and lifetime.');
  accessToken = tokens.access_token;
  expiresAt = Date.now() + (lifetime - 30) * 1000;
  expiryTimer = setTimeout(expire, Math.min(expiresAt - Date.now(), 2147483647));
  button.textContent = 'Clear local session'; button.disabled = false;
  status('Signed in to OCI IAM. Checking TAS identity…');
  const identityResponse = await fetch(`${CONFIG.apiBase}identity/`, { headers: getAuthHeaders(), credentials: 'omit', cache: 'no-store' });
  if (!identityResponse.ok) throw new Error(`TAS identity check failed (HTTP ${identityResponse.status}). Check JWT audience, issuer, scope and key access.`);
  const identity = await identityResponse.json();
  document.querySelector('#identity').textContent = JSON.stringify(identity, null, 2);
  document.querySelector('#identity-panel').hidden = false;
  window.SEARCH_WIDGET_CONFIG = {placeholder: 'Search Wikimedia…', debounceMs: 300, telemetryEnabled: true, getAuthHeaders};
  container.replaceChildren();
  const script = document.createElement('script');
  script.src = 'assets/search_bar_widget.js';
  script.dataset.ordsSearchUrl = `${CONFIG.apiBase}search/`;
  script.dataset.ordsPreviewUrl = `${CONFIG.apiBase}preview/`;
  script.dataset.ordsSelectionUrl = `${CONFIG.apiBase}selection/`;
  script.dataset.searchSpace = CONFIG.searchSpace;
  script.addEventListener('load', () => status('TAS accepted your token. Search Wikimedia below; expand the identity response to verify your user.'));
  script.addEventListener('error', () => status('The widget script failed to load.', true));
  container.append(script);
}
initialize().catch(showError);

The widget uses data-ords-search-url, data-ords-preview-url, and data-ords-selection-url for search, preview, and selection requests. This example sets all three explicitly from apiBase.

Add the Local Server

Save the following as serve.py. It binds to the loopback interface, suppresses request logging, and adds headers to prevent caching and referrer transmission.

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
class DemoHandler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=str(Path(__file__).parent), **kwargs)
    def log_message(self, *args):
        pass
    def end_headers(self):
        self.send_header('Cache-Control', 'no-store')
        self.send_header('Referrer-Policy', 'no-referrer')
        super().end_headers()
server = ThreadingHTTPServer(('127.0.0.1', 8080), DemoHandler)
print('TAS demo ready at http://localhost:8080/widget-example.html', flush=True)
server.serve_forever()

Run and Verify the Example

  1. From the website directory, start the server:

    python3 serve.py
  2. Open http://localhost:8080/widget-example.html. Use localhost, rather than 127.0.0.1, so that the URL matches the registered redirect.
  3. Select Sign in with OCI IAM and complete sign-in and consent.
  4. After returning to the page, expand TAS identity response. Verify that remote_ident and the resolved TAS identity identify the intended end user.
  5. Search for a query supported by your Search Space. For Wikimedia, try italian page views and verify the results against your configured targets.
  6. Verify preview and result-selection responses. Successful sign-in alone does not verify these operations or access to a Search Space.
  7. Verify that requests with expired tokens or insufficient scope are rejected, and that an unapproved browser origin cannot read API responses.

If port 8080 is already in use, stop the conflicting local server before starting this example. Changing the port also requires updating the redirect URL and the IAM and TAS allowed origins.

Note: CORS controls browser access to responses. It does not replace token validation or authorization.

Session Handling and Limitations

This example demonstrates the login handoff. It does not implement automatic token refresh or identity-provider logout.

Troubleshoot the Example

Symptom Check
unauthorized_client with a scope error Associate the correct TAS API scope with the public client. Compare the full requested scope and API audience with the registration.
Other unauthorized_client errors Check the application activation, Authorization code grant, Client ID, and exact redirect URL.
Token exchange fails with a browser network error Check IAM CORS settings and the token request in the browser’s network tools.
identity/ returns HTTP 401 or 403 Check the access token’s issuer, audience, scope, and expiry, along with signing-key reachability and the ORDS scope-to-privilege mapping.
TAS requests are blocked by the browser Check the exact origin in TRUSTED_SEARCH_WIDGET_ALLOWED_ORIGINS and the preflight response.
Search does not return expected results Check the Search Space name, published version, configured targets, and end-user access.
Widget script fails to load Verify that assets/search_bar_widget.js is present and comes from the matching TAS release.

Do not include access tokens or database passwords when sharing diagnostic information.

Authorization

Grant Trusted Answer Search roles and search space access through Users in the Admin application. See Personas and Access for the Search Administrator, Search Space Expert, and Search Space User roles.

Users who sign in through SSO must also be explicitly granted access to private search spaces by a Search Administrator. SSO authentication alone does not grant that access. See Configure Search Space Visibility.

Managing User Access

Trusted Answer Search user access depends on the Admin application’s authentication scheme. For Database Accounts deployments, each user must exist as a database account before you authorize that user to access the Trusted Answer Search application. For Social Sign-In deployments, users authenticate through your SSO provider, and you grant the appropriate Trusted Answer Search roles and search space access in the admin app.

The following steps add a Search Space User called tasuser for a Database Accounts deployment. Ask your database administrator (DBA) to provision the database account with a strong password, or create it if you have the required privileges. Replace the password placeholder before running this example:

CREATE USER tasuser IDENTIFIED BY "<strong-password>";
  1. Sign in to the admin app as tasadmin, select Users in the left navigation, and confirm that you are managing access for the Wikimedia search space. The report lists existing administrators, experts, and search-only users.

  2. Select Add User to open the drawer. Enter tasuser, select only the User role, and select wikimedia from the Search Space list so the new account is scoped to the intended search space. Finish by selecting Add.

  3. The Users table immediately shows TASUSER. Select the username to review the access drawer. The Search Space Access region confirms that the account has User access to wikimedia; add or revoke other spaces here as needed.

After adding the search space user, configure the host application to authenticate that user and embed the widget as described in Search Widgets. Search italian page views and review the returned Wikimedia targets and resolved inputs against the version you published.