Share on:

Authentication in a Single‐Page Application via OpenAM using OAuth2 OIDC Protocol

Original article: https://github.com/OpenIdentityPlatform/OpenAM/wiki/Authentication-in-a-Single%E2%80%90Page-Application-via-OpenAM-using-OAuth2-OIDC-Protocol

Introduction

This article is intended for developers of single-page applications (SPAs) who seek to configure user authentication. The authentication process will utilize the OAuth2/OIDC protocol with PKCE for enhanced security. OpenAM will serve as a designated authentication server.

OpenAM Configuration

Install OpenAM

Let OpenAM hostname is openam.example.org Before starting, add the hostname and IP address to the hosts file, for example 127.0.0.0.1 openam.example.org.

On Windows systems, the hosts file is located at C:Windows/System32/drivers/etc/hosts , on Linux and Mac it is located at /etc/hosts.

Next, start the OpenAM Docker container. Run the following command:

docker run -h openam.example.org -p 8080:8080 --name openam openidentityplatform/openam

There is no need to configure OpenAM manually via UI. The following command helps to automate the process:

 docker exec -w '/usr/openam/ssoconfiguratortools' openam bash -c \
'echo "ACCEPT_LICENSES=true
SERVER_URL=http://openam.example.org:8080
DEPLOYMENT_URI=/$OPENAM_PATH
BASE_DIR=$OPENAM_DATA_DIR
locale=en_US
PLATFORM_LOCALE=en_US
AM_ENC_KEY=
ADMIN_PWD=passw0rd
AMLDAPUSERPASSWD=p@passw0rd
COOKIE_DOMAIN=example.org
ACCEPT_LICENSES=true
DATA_STORE=embedded
DIRECTORY_SSL=SIMPLE
DIRECTORY_SERVER=openam.example.org
DIRECTORY_PORT=50389
DIRECTORY_ADMIN_PORT=4444
DIRECTORY_JMX_PORT=1689
ROOT_SUFFIX=dc=openam,dc=example,dc=org
DS_DIRMGRDN=cn=Directory Manager
DS_DIRMGRPASSWD=passw0rd" > conf.file && java -jar openam-configurator-tool*.jar --file conf.file'

After successful configuration, you can proceed with further customization.

OAuth2/OIDC Provider Configuration

Go to the OpenAM administrator console at

http://openam.example.org:8080/openam/XUI/#login/

In the login field enter the value amadmin, in the password field enter the value from the ADMIN_PWD parameter of the setup command, in this case passw0rd.

OAuth2/OIDC Configuration

Select the desired realm. In the Dashboard section, click the Configure OAuth Provider item.

OpenAM Configure OAuth2 Provider

Next, Configure OpenID Connect

OpenAM Configure OpenID Connect

In the opened form, leave all settings unchanged and click the Create button

OpenAM Configure OpenID Connect Properties

Now let’s create an OAuth2/OIDC client that will use SPA application for authentication.

Go to the admin console, select the required realm, select Applications and then OAuth 2.0 from the left menu

In the Agents table click the New button

OpenAM Agents Table

CORS Setup

SPA performs cross-domain queries to get access_token and id_token. To prevent these requests from being blocked by the browser, you need to enable CORS support in OpenAM.

Open the administrator console. In the top menu, select Configure → Global Services.

OpenAM Global Services

Next, go to CORS Settings and enable CORS support

OpenAM CORS settings

Pres the Save Changes button.

React-Based Example SPA Application

As an example, we will use a React application. For simplicity, we will not validate the state parameter, verify the correctness of the signature of the returned id_token, and so on. However, in a production environment, we highly recommend doing so.

To create a new application, execute the following command in the console:

npm create vite@latest react-openam-example -- --template react

Add the CryptoJS library to the dependencies. It will be needed for code_challenge generation.

cd react-openam-example
npm install crypto-js

Replace the contents of the react-openam-example/src/App.jsx file with the following code:

import { useEffect, useState } from 'react'

import CryptoJS from 'crypto-js';

import './App.css'

const OPENAM_URL = "http://openam.example.org:8080/openam";
const OAUTH2_ENDPOINT = OPENAM_URL + "/oauth2";
const OAUTH2_AUTHORIZE_ENDPOINT = OAUTH2_ENDPOINT + "/authorize";
const OAUTH2_TOKEN_ENDPOINT = OAUTH2_ENDPOINT + "/access_token";
const CLIENT_ID = "test_client";
const SCOPE = "openid";

function App() {

  const [user, setUser] = useState("");

  //TODO should be randomly generated, saved and then restored in production evironment
  const codeVerifier = "a116cb8c-5a1e-4918-a164-255ae3d8f1b1"; 

  useEffect(() => {
    const params = new URLSearchParams(window.location.search)
    const code = params.get('code')
    if(!code) {
      return;
    }
    getToken(code)
  }, [])

  const getToken = async (code) => {
    const resp = await fetch(OAUTH2_TOKEN_ENDPOINT, {
      method: "POST",
      mode: "cors",
      cache: "no-cache", 
      credentials: "include", 
      headers: {'content-type': 'application/x-www-form-urlencoded'},
      redirect: "follow", 
      referrerPolicy: "no-referrer", 
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: CLIENT_ID,
        code_verifier: codeVerifier,
        code: code,
        redirect_uri: window.location.origin
      }),
    });
    if(resp.ok) {
      const accessToken = await resp.json()
      //TODO verify id_token signature
      const idToken = accessToken['id_token'];
      const parts = idToken.split('.')
      const payload = parts[1];
      const jsonPayload = JSON.parse(atob(payload));
      const sub = jsonPayload["sub"]
      setUser(sub)
      console.log(sub, "authenticated")
    } else {
      console.log(resp.status)
    }
  }
  

  const authOpenAM = () => {
    const state = "state";
    const codeChallenge = CryptoJS.SHA256(codeVerifier).toString(CryptoJS.enc.Base64url);
    console.log(codeChallenge);
    const queryString = "?redirect_uri=" + encodeURIComponent(window.location.origin) +
    "&client_id=" + CLIENT_ID +
    "&response_type=code" +
    "&state=" + state +
    "&scope=" + encodeURIComponent(SCOPE) +
    "&code_challenge=" + codeChallenge +
    "&code_challenge_method=S256";
    window.location = OAUTH2_AUTHORIZE_ENDPOINT + queryString;
  }

  const getComponent = () => {
    if (!user) {
      return <>
      <div>
        <h1>Not authenticated</h1>
      </div>
      <button onClick={authOpenAM}>Login</button>
    </>
    } else {
      return <h1>User {user} authenticated</h1>
    }
  }
  return getComponent()
}

export default App

Test Solution

Start the SPA with the following command:

npm run dev

Open the application in your browser by URL http://localhost:5173/

SPA not authenticated

Click the Login button. You will be redirected to OpenAM authentication. Enter the user login demo and password changeit.

OpenAM demo user authentication

Confirm consent to access the data

OpenAM demo user consent

After that, the browser will redirect back to the SPA application and successfully authenticate the user. If everything is configured correctly, the SPA application will display a message about successful authentication:

SPA user demo authenticated