Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/brave-foxes-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@forgerock/iframe-manager': minor
'@forgerock/oidc-client': minor
---

Add `session.check()` method to oidc client for OIDC prompt=none session verification, with `response_type=none` and `response_type=id_token` support.
2 changes: 2 additions & 0 deletions e2e/oidc-app/src/ping-am/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ <h1>OIDC App | PingAM Login</h1>
<button id="logout">Logout</button>
<button id="user-info-btn">User Info</button>
<button id="revoke">Revoke Token</button>
<button id="session-check-btn">Session Check (none)</button>
<button id="session-check-id-token-btn">Session Check (id_token)</button>
<a href="/ping-am/">Start Over</a>
</div>
<script type="module" src="./main.ts"></script>
Expand Down
2 changes: 2 additions & 0 deletions e2e/oidc-app/src/ping-one/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ <h1>OIDC App | P1 Login</h1>
<button id="logout">Logout</button>
<button id="user-info-btn">User Info</button>
<button id="revoke">Revoke Token</button>
<button id="session-check-btn">Session Check (none)</button>
<button id="session-check-id-token-btn">Session Check (id_token)</button>
<a href="/ping-one/">Start Over</a>
</div>
<script type="module" src="./main.ts"></script>
Expand Down
154 changes: 112 additions & 42 deletions e2e/oidc-app/src/utils/oidc-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import { oidc } from '@forgerock/oidc-client';
import type {
AuthorizationError,
GenericError,
GetAuthorizationUrlOptions,
OauthTokens,
OidcClient,
OidcConfig,
SessionCheckOptions,
TokenExchangeErrorResponse,
} from '@forgerock/oidc-client/types';

let tokenIndex = 0;

function displayError(error) {
function displayError(error: unknown) {
const errorEl = document.createElement('div');
errorEl.innerHTML = `<p><strong>Error:</strong> <span class="error">${JSON.stringify(error, null, 2)}</span></p>`;
document.body.appendChild(errorEl);
Expand All @@ -27,25 +28,44 @@ function displayError(error) {
function displayTokenResponse(
response: OauthTokens | TokenExchangeErrorResponse | GenericError | AuthorizationError,
) {
const appEl = document.getElementById('app');
if ('error' in response || !('accessToken' in response)) {
console.error('Token Error:', response);
displayError(response);
} else {
console.log('Token Response:', response);
document.getElementById('logout').style.display = 'block';
document.getElementById('user-info-btn').style.display = 'block';
document.getElementById('login-background').style.display = 'none';
document.getElementById('login-redirect').style.display = 'none';
const appEl = document.getElementById('app');
const logoutEl = document.getElementById('logout');
const userInfoBtnEl = document.getElementById('user-info-btn');
const loginBackgroundEl = document.getElementById('login-background');
const loginRedirectEl = document.getElementById('login-redirect');

if (logoutEl) {
logoutEl.style.display = 'block';
}
if (userInfoBtnEl) {
userInfoBtnEl.style.display = 'block';
}
if (loginBackgroundEl) {
loginBackgroundEl.style.display = 'none';
}
if (loginRedirectEl) {
loginRedirectEl.style.display = 'none';
}

const tokenInfoEl = document.createElement('div');
tokenInfoEl.innerHTML = `<p><strong>Access Token:</strong> <span id="accessToken-${tokenIndex}">${response.accessToken}</span></p>`;
appEl.appendChild(tokenInfoEl);
appEl?.appendChild(tokenInfoEl);
tokenIndex++;
}
}

export async function oidcApp({ config, urlParams }) {
export async function oidcApp({
config,
urlParams,
}: {
config: OidcConfig;
urlParams: URLSearchParams;
}) {
const code = urlParams.get('code');
const state = urlParams.get('state');
const piflow = urlParams.get('piflow');
Expand All @@ -56,20 +76,23 @@ export async function oidcApp({ config, urlParams }) {
});
if ('error' in oidcClient) {
displayError(oidcClient);
return;
}

document.getElementById('login-background').addEventListener('click', async () => {
const authorizeOptions: GetAuthorizationUrlOptions =
document.getElementById('login-background')?.addEventListener('click', async () => {
const authorizeOptions =
piflow === 'true'
? {
clientId: config.clientId,
redirectUri: config.redirectUri,
scope: config.scope,
responseType: config.responseType ?? 'code',
responseMode: 'pi.flow',
responseMode: 'pi.flow' as const,
}
: undefined;
const response = await oidcClient.authorize.background(authorizeOptions);
const response = await oidcClient.authorize?.background(authorizeOptions);

if (!response) return;

if ('error' in response) {
console.error('Authorization Error:', response);
Expand All @@ -85,13 +108,16 @@ export async function oidcApp({ config, urlParams }) {
// Handle success response from background authorization
} else if ('code' in response) {
console.log('Authorization Code:', response.code);
const tokenResponse = await oidcClient.token.exchange(response.code, response.state);
displayTokenResponse(tokenResponse);
const tokenResponse = await oidcClient.token?.exchange(response.code, response.state);
if (tokenResponse) {
displayTokenResponse(tokenResponse);
}
}
});

document.getElementById('login-redirect').addEventListener('click', async () => {
const authorizeUrl = await oidcClient.authorize.url();
document.getElementById('login-redirect')?.addEventListener('click', async () => {
const authorizeUrl = await oidcClient.authorize?.url();
if (!authorizeUrl) return;
if (typeof authorizeUrl !== 'string' && 'error' in authorizeUrl) {
console.error('Authorization URL Error:', authorizeUrl);
displayError(authorizeUrl);
Expand All @@ -102,23 +128,31 @@ export async function oidcApp({ config, urlParams }) {
}
});

document.getElementById('get-tokens').addEventListener('click', async () => {
const response = await oidcClient.token.get();
displayTokenResponse(response);
document.getElementById('get-tokens')?.addEventListener('click', async () => {
const response = await oidcClient.token?.get();
if (response) {
displayTokenResponse(response);
}
});

document.getElementById('get-tokens-background').addEventListener('click', async () => {
const response = await oidcClient.token.get({ backgroundRenew: true });
displayTokenResponse(response);
document.getElementById('get-tokens-background')?.addEventListener('click', async () => {
const response = await oidcClient.token?.get({ backgroundRenew: true });
if (response) {
displayTokenResponse(response);
}
});

document.getElementById('renew-tokens').addEventListener('click', async () => {
const response = await oidcClient.token.get({ backgroundRenew: true, forceRenew: true });
displayTokenResponse(response);
document.getElementById('renew-tokens')?.addEventListener('click', async () => {
const response = await oidcClient.token?.get({ backgroundRenew: true, forceRenew: true });
if (response) {
displayTokenResponse(response);
}
});

document.getElementById('user-info-btn').addEventListener('click', async () => {
const userInfo = await oidcClient.user.info();
document.getElementById('user-info-btn')?.addEventListener('click', async () => {
const userInfo = await oidcClient.user?.info();

if (!userInfo) return;

if ('error' in userInfo) {
console.error('User Info Error:', userInfo);
Expand All @@ -129,42 +163,78 @@ export async function oidcApp({ config, urlParams }) {
const appEl = document.getElementById('app');
const userInfoEl = document.createElement('div');
userInfoEl.innerHTML = `<p><strong>User Info:</strong> <span id="userInfo">${JSON.stringify(userInfo, null, 2)}</span></p>`;
appEl.appendChild(userInfoEl);
appEl?.appendChild(userInfoEl);
}
});

document.getElementById('revoke').addEventListener('click', async () => {
const response = await oidcClient.token.revoke();
document.getElementById('revoke')?.addEventListener('click', async () => {
const response = await oidcClient.token?.revoke();

if (!response) return;

if ('error' in response) {
console.error('Token Revocation Error:', response);
displayError(response);
} else {
const appEl = document.getElementById('app');
const userInfoEl = document.createElement('div');
userInfoEl.innerHTML = `<p>Token successfully revoked</p>`;
appEl.appendChild(userInfoEl);
const revokeEl = document.createElement('div');
revokeEl.innerHTML = `<p>Token successfully revoked</p>`;
appEl?.appendChild(revokeEl);
}
});

document.getElementById('logout').addEventListener('click', async () => {
const response = await oidcClient.user.logout();
document.getElementById('logout')?.addEventListener('click', async () => {
const response = await oidcClient.user?.logout();

if (!response) return;

if ('error' in response) {
console.error('Logout Error:', response);
displayError(response);
} else {
console.log('Logout successful');
document.getElementById('logout').style.display = 'none';
document.getElementById('user-info-btn').style.display = 'none';
document.getElementById('login-background').style.display = 'block';
document.getElementById('login-redirect').style.display = 'block';
const logoutEl = document.getElementById('logout');
const userInfoBtnEl = document.getElementById('user-info-btn');
const loginBackgroundEl = document.getElementById('login-background');
const loginRedirectEl = document.getElementById('login-redirect');

if (logoutEl) {
logoutEl.style.display = 'none';
}
if (userInfoBtnEl) {
userInfoBtnEl.style.display = 'none';
}
if (loginBackgroundEl) {
loginBackgroundEl.style.display = 'block';
}
if (loginRedirectEl) {
loginRedirectEl.style.display = 'block';
}
window.location.assign(window.location.origin + window.location.pathname);
}
});

document.getElementById('session-check-btn')?.addEventListener('click', async () => {
const result = await oidcClient.session?.check();
const appEl = document.getElementById('app');
const el = document.createElement('div');
el.innerHTML = `<p><strong>Session Check (none):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>`;
appEl?.appendChild(el);
});

document.getElementById('session-check-id-token-btn')?.addEventListener('click', async () => {
const options: SessionCheckOptions = { responseType: 'id_token' };
const result = await oidcClient.session?.check(options);
const appEl = document.getElementById('app');
const el = document.createElement('div');
el.innerHTML = `<p><strong>Session Check (id_token):</strong></p><pre id="session-check-id-token-result">${JSON.stringify(result, null, 2)}</pre>`;
appEl?.appendChild(el);
});
Comment on lines +217 to +232

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use textContent instead of innerHTML to prevent potential XSS.

The session check result handlers set innerHTML with JSON.stringify(result). While the result comes from the OIDC client (not direct user input) and JSON.stringify provides some escaping, it's not complete XSS protection. Claims or error descriptions from the authorization server could theoretically contain malicious content. Best practice is to use textContent for untrusted data.

🛡️ Proposed fix for session check handlers
 document.getElementById('session-check-btn')?.addEventListener('click', async () => {
   const result = await oidcClient.session?.check();
   const appEl = document.getElementById('app');
   const el = document.createElement('div');
-  el.innerHTML = `<p><strong>Session Check (none):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>`;
+  const pre = document.createElement('pre');
+  pre.id = 'session-check-result';
+  pre.textContent = JSON.stringify(result, null, 2);
+  el.innerHTML = '<p><strong>Session Check (none):</strong></p>';
+  el.appendChild(pre);
   appEl?.appendChild(el);
 });

 document.getElementById('session-check-id-token-btn')?.addEventListener('click', async () => {
   const options: SessionCheckOptions = { responseType: 'id_token' };
   const result = await oidcClient.session?.check(options);
   const appEl = document.getElementById('app');
   const el = document.createElement('div');
-  el.innerHTML = `<p><strong>Session Check (id_token):</strong></p><pre id="session-check-id-token-result">${JSON.stringify(result, null, 2)}</pre>`;
+  const pre = document.createElement('pre');
+  pre.id = 'session-check-id-token-result';
+  pre.textContent = JSON.stringify(result, null, 2);
+  el.innerHTML = '<p><strong>Session Check (id_token):</strong></p>';
+  el.appendChild(pre);
   appEl?.appendChild(el);
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
document.getElementById('session-check-btn')?.addEventListener('click', async () => {
const result = await oidcClient.session?.check();
const appEl = document.getElementById('app');
const el = document.createElement('div');
el.innerHTML = `<p><strong>Session Check (none):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>`;
appEl?.appendChild(el);
});
document.getElementById('session-check-id-token-btn')?.addEventListener('click', async () => {
const options: SessionCheckOptions = { responseType: 'id_token' };
const result = await oidcClient.session?.check(options);
const appEl = document.getElementById('app');
const el = document.createElement('div');
el.innerHTML = `<p><strong>Session Check (id_token):</strong></p><pre id="session-check-id-token-result">${JSON.stringify(result, null, 2)}</pre>`;
appEl?.appendChild(el);
});
document.getElementById('session-check-btn')?.addEventListener('click', async () => {
const result = await oidcClient.session?.check();
const appEl = document.getElementById('app');
const el = document.createElement('div');
const pre = document.createElement('pre');
pre.id = 'session-check-result';
pre.textContent = JSON.stringify(result, null, 2);
el.innerHTML = '<p><strong>Session Check (none):</strong></p>';
el.appendChild(pre);
appEl?.appendChild(el);
});
document.getElementById('session-check-id-token-btn')?.addEventListener('click', async () => {
const options: SessionCheckOptions = { responseType: 'id_token' };
const result = await oidcClient.session?.check(options);
const appEl = document.getElementById('app');
const el = document.createElement('div');
const pre = document.createElement('pre');
pre.id = 'session-check-id-token-result';
pre.textContent = JSON.stringify(result, null, 2);
el.innerHTML = '<p><strong>Session Check (id_token):</strong></p>';
el.appendChild(pre);
appEl?.appendChild(el);
});
🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 219-219: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <p><strong>Session Check (none):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(dom-content-modification)


[warning] 228-228: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: el.innerHTML = <p><strong>Session Check (id_token):</strong></p><pre id="session-check-id-token-result">${JSON.stringify(result, null, 2)}</pre>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(dom-content-modification)


[warning] 219-219: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <p><strong>Session Check (none):</strong></p><pre id="session-check-result">${JSON.stringify(result, null, 2)}</pre>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(unsafe-html-content-assignment)


[warning] 228-228: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: el.innerHTML = <p><strong>Session Check (id_token):</strong></p><pre id="session-check-id-token-result">${JSON.stringify(result, null, 2)}</pre>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation

(unsafe-html-content-assignment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/oidc-app/src/utils/oidc-app.ts` around lines 216 - 231, Replace the use
of innerHTML in the session check button handlers with safe DOM text insertion:
instead of setting el.innerHTML use createElement/textContent to build the nodes
and set the JSON string into a text node (or set el.textContent / the pre
element's textContent) so untrusted values from oidcClient.session?.check() are
not interpreted as HTML; update both the handler for 'session-check-btn' (that
currently creates a div and sets innerHTML with "Session Check (none)" and the
JSON) and the handler for 'session-check-id-token-btn' (that sets innerHTML with
"Session Check (id_token)" and the JSON) to construct elements and assign
textContent for the JSON output before appending to the app element.

Source: Linters/SAST tools


if (code && state) {
const response = await oidcClient.token.exchange(code, state);
displayTokenResponse(response);
const response = await oidcClient.token?.exchange(code, state);
if (response) {
displayTokenResponse(response);
}
}
}
Loading
Loading