Auth backend: add support for authenticating from additional app origins
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com> Signed-off-by: Samira Mokaram <samiram@spotify.com>
This commit is contained in:
@@ -61,8 +61,7 @@ function defaultJoinScopes(scopes: Set<string>) {
|
||||
* via the OAuthRequestApi.
|
||||
*/
|
||||
export class DefaultAuthConnector<AuthSession>
|
||||
implements AuthConnector<AuthSession>
|
||||
{
|
||||
implements AuthConnector<AuthSession> {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
private readonly environment: string;
|
||||
private readonly provider: AuthProvider & { id: string };
|
||||
@@ -152,7 +151,10 @@ export class DefaultAuthConnector<AuthSession>
|
||||
|
||||
private async showPopup(scopes: Set<string>): Promise<AuthSession> {
|
||||
const scope = this.joinScopesFunc(scopes);
|
||||
const popupUrl = await this.buildUrl('/start', { scope });
|
||||
const popupUrl = await this.buildUrl('/start', {
|
||||
scope,
|
||||
origin: location.origin,
|
||||
});
|
||||
|
||||
const payload = await showLoginPopup({
|
||||
url: popupUrl,
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"jwt-decode": "^3.1.0",
|
||||
"knex": "^0.95.1",
|
||||
"luxon": "^1.25.0",
|
||||
"minimatch": "^3.0.3",
|
||||
"morgan": "^1.10.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"openid-client": "^4.2.1",
|
||||
|
||||
@@ -22,9 +22,9 @@ import {
|
||||
BackstageIdentity,
|
||||
AuthProviderConfig,
|
||||
} from '../../providers/types';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { InputError, NotAllowedError } from '@backstage/errors';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { verifyNonce } from './helpers';
|
||||
import { readState, verifyNonce } from './helpers';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
|
||||
import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest } from './types';
|
||||
|
||||
@@ -40,6 +40,7 @@ export type Options = {
|
||||
cookiePath: string;
|
||||
appOrigin: string;
|
||||
tokenIssuer: TokenIssuer;
|
||||
isOriginAllowed: (origin: string) => boolean;
|
||||
};
|
||||
|
||||
export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
@@ -61,6 +62,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
cookieDomain: url.hostname,
|
||||
cookiePath,
|
||||
secure,
|
||||
isOriginAllowed: config.isOriginAllowed,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
// retrieve scopes from request
|
||||
const scope = req.query.scope?.toString() ?? '';
|
||||
const env = req.query.env?.toString();
|
||||
const origin = req.query.origin?.toString();
|
||||
|
||||
if (!env) {
|
||||
throw new InputError('No env provided in request query parameters');
|
||||
@@ -86,7 +89,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
// set a nonce cookie before redirecting to oauth provider
|
||||
this.setNonceCookie(res, nonce);
|
||||
|
||||
const state = { nonce: nonce, env: env };
|
||||
const state = { nonce, env, origin };
|
||||
const forwardReq = Object.assign(req, { scope, state });
|
||||
|
||||
const { url, status } = await this.handlers.start(
|
||||
@@ -103,7 +106,22 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): Promise<void> {
|
||||
let appOrigin = this.options.appOrigin;
|
||||
|
||||
try {
|
||||
const state: any = readState(req.query.state?.toString() ?? '');
|
||||
|
||||
if (state.origin) {
|
||||
try {
|
||||
appOrigin = new URL(state.origin).origin;
|
||||
} catch {
|
||||
throw new NotAllowedError('App origin is invalid, failed to parse');
|
||||
}
|
||||
if (!this.options.isOriginAllowed(appOrigin)) {
|
||||
throw new NotAllowedError(`Origin '${appOrigin}' is not allowed`);
|
||||
}
|
||||
}
|
||||
|
||||
// verify nonce cookie and state cookie on callback
|
||||
verifyNonce(req, this.options.providerId);
|
||||
|
||||
@@ -129,13 +147,13 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, this.options.appOrigin, {
|
||||
return postMessageResponse(res, appOrigin, {
|
||||
type: 'authorization_response',
|
||||
response,
|
||||
});
|
||||
} catch (error) {
|
||||
// post error message back to popup if failure
|
||||
return postMessageResponse(res, this.options.appOrigin, {
|
||||
return postMessageResponse(res, appOrigin, {
|
||||
type: 'authorization_response',
|
||||
error: {
|
||||
name: error.name,
|
||||
|
||||
@@ -77,6 +77,7 @@ export type OAuthState = {
|
||||
*/
|
||||
nonce: string;
|
||||
env: string;
|
||||
origin?: string;
|
||||
};
|
||||
|
||||
export type OAuthStartRequest = express.Request<{}> & {
|
||||
@@ -106,7 +107,9 @@ export interface OAuthHandlers {
|
||||
* Handles the redirect from the auth provider when the user has signed in.
|
||||
* @param {express.Request} req
|
||||
*/
|
||||
handler(req: express.Request): Promise<{
|
||||
handler(
|
||||
req: express.Request,
|
||||
): Promise<{
|
||||
response: AuthResponse<OAuthProviderInfo>;
|
||||
refreshToken?: string;
|
||||
}>;
|
||||
|
||||
@@ -34,6 +34,11 @@ export type AuthProviderConfig = {
|
||||
* The base URL of the app as provided by app.baseUrl
|
||||
*/
|
||||
appUrl: string;
|
||||
|
||||
/**
|
||||
* A function that is called to check whether an origin other than the apps default one is allowed.
|
||||
*/
|
||||
isOriginAllowed: (origin: string) => boolean;
|
||||
};
|
||||
|
||||
export type RedirectInfo = {
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Config } from '@backstage/config';
|
||||
import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity';
|
||||
import session from 'express-session';
|
||||
import passport from 'passport';
|
||||
import { Minimatch } from 'minimatch';
|
||||
|
||||
type ProviderFactories = { [s: string]: AuthProviderFactory };
|
||||
|
||||
@@ -88,6 +89,8 @@ export async function createRouter({
|
||||
const providersConfig = config.getConfig('auth.providers');
|
||||
const configuredProviders = providersConfig.keys();
|
||||
|
||||
const isOriginAllowed = createOriginFilter(config);
|
||||
|
||||
for (const [providerId, providerFactory] of Object.entries(
|
||||
allProviderFactories,
|
||||
)) {
|
||||
@@ -96,7 +99,7 @@ export async function createRouter({
|
||||
try {
|
||||
const provider = providerFactory({
|
||||
providerId,
|
||||
globalConfig: { baseUrl: authUrl, appUrl },
|
||||
globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed },
|
||||
config: providersConfig.getConfig(providerId),
|
||||
logger,
|
||||
tokenIssuer,
|
||||
@@ -158,3 +161,18 @@ export async function createRouter({
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
function createOriginFilter(config: Config): (origin: string) => boolean {
|
||||
const allowedOrigins = config.getOptionalStringArray('auth.allowedOrigins');
|
||||
if (!allowedOrigins || allowedOrigins.length === 0) {
|
||||
return () => false;
|
||||
}
|
||||
|
||||
const allowedOriginPatterns = allowedOrigins.map(
|
||||
pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),
|
||||
);
|
||||
|
||||
return origin => {
|
||||
return allowedOriginPatterns.some(pattern => pattern.match(origin));
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user