Authentication in SPA application via OpenAM using OAuth2

This article will be useful for developers of browser (SPA) applications who want to set up user authentication. For authentication, we will use OAuth2/OIDC protocol with PKCE. The open source authentication management server will be used as the authentication server. OpenAM.

Setting up OpenAM

Installing OpenAM

Let OpenAM be located on the host openam.example.org. If you already have OpenAM installed, you can skip this step. The easiest way to deploy OpenAM is in a Docker container. Before running, add the hostname and IP address to the file hostsFor example 127.0.0.1 openam.example.org .

In Windows systems the file hosts is located at the address C:\Windows\System32\drivers\etc\hosts on Linux and Mac is located at /etc/hosts

After that, start the OpenAM Docker container. Run the following command:

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

Once the server is up and running, run the initial OpenAM configuration. Run the following command:

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=openam.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'

Once OpenAM has been successfully configured, further configuration can begin.

Setting up an OAuth2/OIDC provider

Log in to the admin console using the link

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

In the login field, enter the value amadminin the password field, enter the value from the parameter ADMIN_PWD installation commands, in this case passw0rd

Setting up OAuth2/OIDC

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

Then Configure OpenID Connect

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

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

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

In the Agents table, click the button New

  • Enter Name (client_id) test_client_id and Password (client_secret) changeit new application

  • Open the application settings

  • Set Client type to Public

  • Add your SPA application's URI to the Redirection URIs list. In our case, it will be http://localhost:5173/

  • Add a value to the scope list openidthis is necessary to immediately get the user ID from the returned object id_token.

  • Set Token Endpoint Authentication Method client_secret_post

Setting up CORS

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

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

Next, go to CORS Settings and enable CORS support.

Click `Save Changes`

Example SPA application in React

As an example, we will use an application written in React. For simplicity, we will not check the validity of the state parameter, the correctness of the signature of the returned id_token, etc. In a production environment, we strongly recommend doing this.

Create a new application by running 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 to generate the code_challenge.

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

Checking the solution

Run SPA with the command

npm run dev

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

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

Please confirm your consent to access data

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

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *