From 3ee6993a0fcd218cdf401adeded64c83998428c7 Mon Sep 17 00:00:00 2001 From: maximthomas Date: Fri, 22 May 2026 15:26:37 +0300 Subject: [PATCH 1/2] OAuth2 IDP E2E test --- e2e/common/openam-commons.mjs | 40 +++++ e2e/oauth2/oauth2-test.spec.mjs | 261 ++++++++++++++++++++++++++++++++ e2e/saml/saml-test.spec.mjs | 5 +- 3 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 e2e/common/openam-commons.mjs create mode 100644 e2e/oauth2/oauth2-test.spec.mjs diff --git a/e2e/common/openam-commons.mjs b/e2e/common/openam-commons.mjs new file mode 100644 index 0000000000..6f84272277 --- /dev/null +++ b/e2e/common/openam-commons.mjs @@ -0,0 +1,40 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ + +export const OPENAM_BASE = process.env.OPENAM_BASE_URL ?? "http://openam.example.org:8080/openam"; +export const ADMIN_USER = process.env.OPENAM_ADMIN_USER ?? "amadmin"; +export const ADMIN_PASS = process.env.OPENAM_ADMIN_PASS ?? "ampassword"; + +export const USERNAME = process.env.OPENAM_USERNAME ?? "demo"; +export const PASSWORD = process.env.OPENAM_PASSWORD ?? "changeit"; + +export async function getAdminToken(request) { + return getAuthToken(request, ADMIN_USER, ADMIN_PASS) +} + +export async function getAuthToken(request, username, password) { + const resp = await request.post(`${OPENAM_BASE}/json/authenticate`, { + headers: { + "Content-Type": "application/json", + "X-OpenAM-Username": username, + "X-OpenAM-Password": password, + "Content-Type": "application/json", + "Accept-API-Version": "resource=2.0, protocol=1.0", + } + }); + const json = await resp.json(); + return json.tokenId; +} \ No newline at end of file diff --git a/e2e/oauth2/oauth2-test.spec.mjs b/e2e/oauth2/oauth2-test.spec.mjs new file mode 100644 index 0000000000..388d0109b3 --- /dev/null +++ b/e2e/oauth2/oauth2-test.spec.mjs @@ -0,0 +1,261 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ + +import { test, expect } from "@playwright/test"; +import { OPENAM_BASE, getAdminToken, getAuthToken, PASSWORD, USERNAME } from "../common/openam-commons.mjs"; + +const REALM = "root"; +const CLIENT_ID = "test_client_app"; +const SCOPE="profile" +const REDIRECT_URI="http://app.invalid/cb" +/** + * Ensures the OAuth2 service exists in the OpenAM instance. + * Creates it with default configuration if it doesn't exist. + */ +async function ensureOAuth2ServiceExists(adminToken, request) { + const response = await request.get(`${OPENAM_BASE}/json/realms/${REALM}/realm-config/services/oauth-oidc`, + { + headers: { + "iPlanetDirectoryPro": adminToken, + "Accept-API-Version": "protocol=1.0,resource=1.0", + }, + } + ); + + if (response.status() === 404) { + // OAuth2 service doesn't exist, create it + const createResponse = await request.post(`${OPENAM_BASE}/json/realms/${REALM}/realm-config/services/oauth-oidc?_action=create`, + { + headers: { + "iPlanetDirectoryPro": adminToken, + "Content-Type": "application/json", + "Accept-API-Version": "protocol=1.0,resource=1.0", + }, + data: { + advancedOAuth2Config: { + clientsCanSkipConsent: true, + supportedScopes: [SCOPE], + defaultScopes: [SCOPE], + }, + }, + } + ); + + if (!createResponse.ok()) { + throw new Error( + `Failed to create OAuth2 service: ${createResponse.statusText()}` + ); + } + console.log("OAuth2 service created successfully"); + } else if (!response.ok()) { + throw new Error( + `Failed to check OAuth2 service: ${createResponse.statusText()}` + ); + } else { + console.log("OAuth2 service already exists"); + } +} + +/** + * Ensures an OAuth2 client application exists in the OpenAM instance. + * Creates it with default configuration if it doesn't exist. + */ + +// curl -sS -X PUT \ +// -H "iPlanetDirectoryPro: ${ADMIN_TOKEN}" \ +// -H "Content-Type: application/json" -H "Accept-API-Version: protocol=2.0,resource=1.0" \ +// -d "{ +// \"com.forgerock.openam.oauth2provider.clientType\": \"Public\", +// \"com.forgerock.openam.oauth2provider.redirectionURIs\": [\"[0]=${REDIRECT_URI}\"], +// \"com.forgerock.openam.oauth2provider.scopes\": [\"[0]=${SCOPE}\"], +// \"com.forgerock.openam.oauth2provider.defaultScopes\": [\"[0]=${SCOPE}\"], +// \"com.forgerock.openam.oauth2provider.grantTypes\": [\"[0]=authorization_code\"], +// \"com.forgerock.openam.oauth2provider.responseTypes\": [\"[0]=code\"], +// \"com.forgerock.openam.oauth2provider.tokenEndPointAuthMethod\": \"none\", +// \"isConsentImplied\": true, +// \"sunIdentityServerDeviceStatus\": \"Active\" +// }" \ +// "${BASE}/json/realms/${REALM}/realm-config/agents/OAuth2Client/${CLIENT_ID}" \ +// -o "${TMP}/client.json" -w " client provisioned HTTP %{http_code}\n" + +async function ensureOAuth2ClientExists(adminToken, request) { + const response = await request.get( + `${OPENAM_BASE}/json/realms/${REALM}/realm-config/agents/OAuth2Client/${CLIENT_ID}`, + { + method: "GET", + headers: { + "iPlanetDirectoryPro": adminToken, + "Accept-API-Version": "protocol=2.0,resource=1.0", + }, + } + ); + + if (response.status() === 404) { + // Client doesn't exist, create it + const createResponse = await request.put( + `${OPENAM_BASE}/json/realms/${REALM}/realm-config/agents/OAuth2Client/${CLIENT_ID}`, + { + headers: { + "iPlanetDirectoryPro": adminToken, + "Content-Type": "application/json", + "Accept-API-Version": "protocol=2.0,resource=1.0", + }, + data: { + "com.forgerock.openam.oauth2provider.clientType": "Public", + "com.forgerock.openam.oauth2provider.redirectionURIs": [`[0]=${REDIRECT_URI}`], + "com.forgerock.openam.oauth2provider.scopes": [`[0]=${SCOPE}`], + "com.forgerock.openam.oauth2provider.defaultScopes": [`[0]=${SCOPE}`], + "com.forgerock.openam.oauth2provider.grantTypes": ["[0]=authorization_code"], + "com.forgerock.openam.oauth2provider.responseTypes": ["[0]=code"], + "com.forgerock.openam.oauth2provider.tokenEndPointAuthMethod": "none", + "isConsentImplied": true, + "sunIdentityServerDeviceStatus": "Active" + }, + } + ); + + if (!createResponse.ok()) { + throw new Error( + `Failed to create OAuth2 client: ${createResponse.statusText}` + ); + } + console.log(`OAuth2 client "${CLIENT_ID}" created successfully`); + } else if (!response.ok()) { + throw new Error( + `Failed to check OAuth2 client: ${response.statusText}` + ); + } else { + console.log(`OAuth2 client "${CLIENT_ID}" already exists`); + } +} + +test.beforeAll(async ({ request }) => { + const adminToken = await getAdminToken(request) + + if (!adminToken) { + test.skip("Skipping: ADMIN_TOKEN not set"); + + } + await ensureOAuth2ServiceExists(adminToken, request); + await ensureOAuth2ClientExists(adminToken, request); +}); + +let accessToken; + +test.describe("OAuth Service test", () => { + test("Should receive an auth code and exchange it to access token", async ({ request }) => { + + function generateVerifier(length = 64) { + const array = new Uint32Array(length); + crypto.getRandomValues(array); + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + return Array.from(array, x => chars[x % chars.length]).join(''); + } + + async function generateChallenge(verifier) { + const encoder = new TextEncoder(); + const data = encoder.encode(verifier); + const hash = await crypto.subtle.digest('SHA-256', data); + + return btoa(String.fromCharCode(...new Uint8Array(hash))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + } + + const demoToken = await getAuthToken(request, USERNAME, PASSWORD); + + const state = "random-state"; + + const verifier = generateVerifier(); + + const challenge = await generateChallenge(verifier); + + const codeResponse = await request.get( + `${OPENAM_BASE}/oauth2/authorize`, { + headers: { + "iPlanetDirectoryPro": demoToken, + }, + params: { + response_type: "code", + client_id: CLIENT_ID, + redirect_uri: REDIRECT_URI, + scope: SCOPE, + state: state, + code_challenge: challenge, + code_challenge_method: "S256" + }, + maxRedirects: 0 + } + ) + + expect(codeResponse.status()).toBe(302); + + const headers = codeResponse.headers(); + + const location = headers['location']; + + const locationURL = new URL(location); + + const code = locationURL.searchParams.get("code"); + + expect(code).toBeTruthy() + + + const response = await request.post(`${OPENAM_BASE}/oauth2/access_token`, { + form: { + grant_type: 'authorization_code', + client_id: CLIENT_ID, + code: code, + redirect_uri: REDIRECT_URI, + code_verifier: verifier, + state: state + }, + headers: { + 'Accept': 'application/json' + } + }); + + expect(response.ok()).toBeTruthy(); + + const tokens = await response.json(); + + expect(tokens).toHaveProperty('access_token'); + + accessToken = tokens.access_token + + console.log(`Got access token: ${accessToken}`); + }); + + test("Get user info with access token", async ({ request }) => { + const response = await request.get(`${OPENAM_BASE}/oauth2/userinfo`, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Accept': 'application/json' + } + }); + + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + + // 4. Получение и вывод тела ответа + const userInfo = await response.json(); + expect(userInfo.sub).toBe('demo'); + console.log('User Info Claims:', userInfo); + + }); + +}); diff --git a/e2e/saml/saml-test.spec.mjs b/e2e/saml/saml-test.spec.mjs index 2137d18816..a870f0e10a 100644 --- a/e2e/saml/saml-test.spec.mjs +++ b/e2e/saml/saml-test.spec.mjs @@ -20,13 +20,12 @@ import { test, expect } from "@playwright/test"; import { execSync } from "child_process"; import { resolve } from "path"; import { fileURLToPath } from "url"; +import { PASSWORD, USERNAME } from "../common/openam-commons.mjs"; /** * OpenAM XUI Login Test Suite * * Configuration (override via environment variables): - * OPENAM_USERNAME – login username (default: demo) - * OPENAM_PASSWORD – login password (default: changeit) * BOOTSTRAP_SCRIPT – path to the startup script (default: ./bootstrap.sh) */ @@ -35,8 +34,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = fileURLToPath(new URL(".", import.meta.url)); // ─── Configuration ──────────────────────────────────────────────────────────── -const USERNAME = process.env.OPENAM_USERNAME ?? "demo"; -const PASSWORD = process.env.OPENAM_PASSWORD ?? "changeit"; const BOOTSTRAP_SCRIPT = process.env.BOOTSTRAP_SCRIPT ?? "./bootstrap.sh"; // Derived URLs From 2e911444033ca2c44e8c5982d044deba677fdcee Mon Sep 17 00:00:00 2001 From: Maxim Thomas Date: Fri, 22 May 2026 16:39:55 +0300 Subject: [PATCH 2/2] Remove commented curl command for OAuth2 client Removed commented-out curl command for OAuth2 client provisioning. --- e2e/oauth2/oauth2-test.spec.mjs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/e2e/oauth2/oauth2-test.spec.mjs b/e2e/oauth2/oauth2-test.spec.mjs index 388d0109b3..deedec5861 100644 --- a/e2e/oauth2/oauth2-test.spec.mjs +++ b/e2e/oauth2/oauth2-test.spec.mjs @@ -74,23 +74,6 @@ async function ensureOAuth2ServiceExists(adminToken, request) { * Creates it with default configuration if it doesn't exist. */ -// curl -sS -X PUT \ -// -H "iPlanetDirectoryPro: ${ADMIN_TOKEN}" \ -// -H "Content-Type: application/json" -H "Accept-API-Version: protocol=2.0,resource=1.0" \ -// -d "{ -// \"com.forgerock.openam.oauth2provider.clientType\": \"Public\", -// \"com.forgerock.openam.oauth2provider.redirectionURIs\": [\"[0]=${REDIRECT_URI}\"], -// \"com.forgerock.openam.oauth2provider.scopes\": [\"[0]=${SCOPE}\"], -// \"com.forgerock.openam.oauth2provider.defaultScopes\": [\"[0]=${SCOPE}\"], -// \"com.forgerock.openam.oauth2provider.grantTypes\": [\"[0]=authorization_code\"], -// \"com.forgerock.openam.oauth2provider.responseTypes\": [\"[0]=code\"], -// \"com.forgerock.openam.oauth2provider.tokenEndPointAuthMethod\": \"none\", -// \"isConsentImplied\": true, -// \"sunIdentityServerDeviceStatus\": \"Active\" -// }" \ -// "${BASE}/json/realms/${REALM}/realm-config/agents/OAuth2Client/${CLIENT_ID}" \ -// -o "${TMP}/client.json" -w " client provisioned HTTP %{http_code}\n" - async function ensureOAuth2ClientExists(adminToken, request) { const response = await request.get( `${OPENAM_BASE}/json/realms/${REALM}/realm-config/agents/OAuth2Client/${CLIENT_ID}`,