From 012a3aff0d19d5de71db5504e6cd437429a47d66 Mon Sep 17 00:00:00 2001 From: romanetar Date: Mon, 5 Oct 2026 19:56:36 +0200 Subject: [PATCH] fix(oauth2): do not render client redirect_uri as a link on the consent page For native clients registered with a custom URI scheme, tapping the app title or the tooltip link navigated the auth browser to the callback without an authorization code and killed the login. The app title now links to the client website (http/https only, new tab) when configured, otherwise it is plain text. The redirect URL is shown as plain text in the tooltip. website is exposed to the view config via @json. --- resources/js/oauth2/consent.js | 10 ++-- resources/js/oauth2/consent_components.js | 24 +++++++++ .../js/oauth2/consent_components.test.js | 51 +++++++++++++++++++ resources/views/oauth2/consent.blade.php | 1 + 4 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 resources/js/oauth2/consent_components.js create mode 100644 resources/js/oauth2/consent_components.test.js diff --git a/resources/js/oauth2/consent.js b/resources/js/oauth2/consent.js index 898141a49..3bf7e9937 100644 --- a/resources/js/oauth2/consent.js +++ b/resources/js/oauth2/consent.js @@ -14,6 +14,7 @@ import Typography from "@material-ui/core/Typography"; import Tooltip from '@material-ui/core/Tooltip'; import {ClickAwayListener} from "@material-ui/core"; +import {AppName, RedirectNotice} from "./consent_components"; import styles from "./consent.module.scss"; const HtmlTooltip = withStyles((theme) => ({ @@ -36,7 +37,8 @@ const ConsentPage = ( disclaimer, formAction, redirectURL, - requestedScopes + requestedScopes, + website }) => { const formEl = useRef(null); const trustEl = useRef(null); @@ -74,7 +76,7 @@ const ConsentPage = ( idpLogo

- {appName}  +   )} } -
Clicking 'Accept' will redirect you to: {redirectURL}. -
+ } > diff --git a/resources/js/oauth2/consent_components.js b/resources/js/oauth2/consent_components.js new file mode 100644 index 000000000..f6914cc7f --- /dev/null +++ b/resources/js/oauth2/consent_components.js @@ -0,0 +1,24 @@ +import React from "react"; + +// only plain web URLs are safe to expose as a navigable link (blocks javascript:, custom schemes, etc.) +export const safeWebsiteUrl = (website) => { + if (typeof website !== 'string' || website.trim() === '') return null; + try { + const url = new URL(website.trim()); + return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null; + } catch (e) { + return null; + } +}; + +// NOTE: never link to the client's redirect_uri; for native clients (custom URI scheme) +// navigating to it without an authorization code kills the login. +export const AppName = ({appName, website}) => { + const href = safeWebsiteUrl(website); + if (!href) return <>{appName}; + return {appName}; +}; + +export const RedirectNotice = ({redirectURL}) => ( +
Clicking 'Accept' will redirect you to: {redirectURL}.
+); diff --git a/resources/js/oauth2/consent_components.test.js b/resources/js/oauth2/consent_components.test.js new file mode 100644 index 000000000..04201b676 --- /dev/null +++ b/resources/js/oauth2/consent_components.test.js @@ -0,0 +1,51 @@ +import React from "react"; +import {renderToStaticMarkup} from "react-dom/server"; +import {AppName, RedirectNotice, safeWebsiteUrl} from "./consent_components"; + +const NATIVE_REDIRECT = 'com.fntech.ftnattendee://callback'; + +describe('safeWebsiteUrl', () => { + it('accepts http and https', () => { + expect(safeWebsiteUrl('https://example.com/app')).toBe('https://example.com/app'); + expect(safeWebsiteUrl('http://example.com/')).toBe('http://example.com/'); + }); + + it('rejects empty, non-string, malformed and non-web schemes', () => { + expect(safeWebsiteUrl(null)).toBeNull(); + expect(safeWebsiteUrl(undefined)).toBeNull(); + expect(safeWebsiteUrl(' ')).toBeNull(); + expect(safeWebsiteUrl('not a url')).toBeNull(); + expect(safeWebsiteUrl('javascript:alert(1)')).toBeNull(); + expect(safeWebsiteUrl(NATIVE_REDIRECT)).toBeNull(); + }); +}); + +describe('AppName', () => { + it('links to the website in a new tab when configured', () => { + const html = renderToStaticMarkup(); + expect(html).toContain('href="https://example.com/"'); + expect(html).toContain('target="_blank"'); + expect(html).toContain('rel="noopener noreferrer"'); + expect(html).toContain('My App'); + }); + + it('is plain text without a website', () => { + const html = renderToStaticMarkup(); + expect(html).toBe('My App'); + expect(html).not.toContain(' { + const html = renderToStaticMarkup(); + expect(html).not.toContain(' { + it('shows a native redirect_uri as text, never as a link', () => { + const html = renderToStaticMarkup(); + expect(html).toContain(NATIVE_REDIRECT); + expect(html).not.toContain('