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
10 changes: 5 additions & 5 deletions resources/js/oauth2/consent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -36,7 +37,8 @@ const ConsentPage = (
disclaimer,
formAction,
redirectURL,
requestedScopes
requestedScopes,
website
}) => {
const formEl = useRef(null);
const trustEl = useRef(null);
Expand Down Expand Up @@ -74,7 +76,7 @@ const ConsentPage = (
<img className={styles.app_logo} alt="idpLogo" src={appLogo}/>
</a>
<h1>
<a target='_blank' href={redirectURL}>{appName}</a>&nbsp;
<AppName appName={appName} website={website}/>&nbsp;
<ClickAwayListener onClickAway={handleTooltipClose}>
<HtmlTooltip
arrow
Expand All @@ -101,9 +103,7 @@ const ConsentPage = (
{ix < contactEmails.length - 1 ? ', ' : ''}
</span>)}
</div>}
<div>Clicking 'Accept' will redirect you to: <a href={redirectURL}
target='_blank'>{redirectURL}</a>.
</div>
<RedirectNotice redirectURL={redirectURL}/>
</React.Fragment>
}
>
Expand Down
24 changes: 24 additions & 0 deletions resources/js/oauth2/consent_components.js
Original file line number Diff line number Diff line change
@@ -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 <a target='_blank' rel='noopener noreferrer' href={href}>{appName}</a>;
};

export const RedirectNotice = ({redirectURL}) => (
<div>Clicking 'Accept' will redirect you to: <span style={{wordBreak: 'break-all'}}>{redirectURL}</span>.</div>
);
51 changes: 51 additions & 0 deletions resources/js/oauth2/consent_components.test.js
Original file line number Diff line number Diff line change
@@ -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(<AppName appName="My App" website="https://example.com/"/>);
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(<AppName appName="My App" website=""/>);
expect(html).toBe('My App');
expect(html).not.toContain('<a');
});

it('never emits an anchor for a native-scheme website', () => {
const html = renderToStaticMarkup(<AppName appName="My App" website={NATIVE_REDIRECT}/>);
expect(html).not.toContain('<a');
});
});

describe('RedirectNotice', () => {
it('shows a native redirect_uri as text, never as a link', () => {
const html = renderToStaticMarkup(<RedirectNotice redirectURL={NATIVE_REDIRECT}/>);
expect(html).toContain(NATIVE_REDIRECT);
expect(html).not.toContain('<a');
expect(html).not.toContain('href=');
});
});
1 change: 1 addition & 0 deletions resources/views/oauth2/consent.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
formAction: '{{ URL::action("UserController@postConsent") }}',
contactEmails: contactEmails,
redirectURL: '{!! $redirect_to !!}',
website: @json($website ?? null),
disclaimer: disclaimer,
}

Expand Down
Loading