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 hosts
For 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 amadmin
in 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 applicationOpen 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
openid
this is necessary to immediately get the user ID from the returned objectid_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: