fix errors with auth redirect flow

Signed-off-by: Stephen Glass <stephen@stephen.glass>
This commit is contained in:
Stephen Glass
2024-07-28 03:15:00 -04:00
parent 16cc26c0a2
commit 8542af998a
12 changed files with 232 additions and 39 deletions
@@ -0,0 +1,56 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Response } from 'express';
import { CookieConfigurer } from '../types';
import { serializeError } from '@backstage/errors';
const ONE_MINUTE_MS = 60 * 1000;
const AUTH_ERROR_COOKIE = 'auth-error';
function configureAuthErrorCookie(redirectUrl: string, appOrigin: string) {
const { hostname: domain, pathname: path, protocol } = new URL(redirectUrl);
const secure = protocol === 'https:';
// For situations where the auth-backend is running on a
// different domain than the app, we set the SameSite attribute
// to 'none' to allow third-party access to the cookie, but
// only if it's in a secure context (https).
let sameSite: ReturnType<CookieConfigurer>['sameSite'] = 'lax';
if (new URL(appOrigin).hostname !== domain && secure) {
sameSite = 'none';
}
return { domain, path, secure, sameSite };
}
export function createAuthErrorCookie(
res: Response,
origin: string,
options: {
error: Error;
redirectUrl: string;
},
) {
const { error, redirectUrl } = options;
const jsonData = serializeError(error);
res.cookie(AUTH_ERROR_COOKIE, jsonData, {
maxAge: ONE_MINUTE_MS,
httpOnly: true,
...configureAuthErrorCookie(redirectUrl, origin),
});
}
@@ -42,6 +42,7 @@ import {
import { OAuthAuthenticator, OAuthAuthenticatorResult } from './types';
import { Config } from '@backstage/config';
import { CookieScopeManager } from './CookieScopeManager';
import { createAuthErrorCookie } from './createAuthErrorCookie';
/** @public */
export interface OAuthRouteHandlersOptions<TProfile> {
@@ -164,9 +165,10 @@ export function createOAuthRouteHandlers<TProfile>(
res: express.Response,
): Promise<void> {
let origin = defaultAppOrigin;
let state;
try {
const state = decodeOAuthState(req.query.state?.toString() ?? '');
state = decodeOAuthState(req.query.state?.toString() ?? '');
if (state.origin) {
try {
@@ -248,11 +250,25 @@ export function createOAuthRouteHandlers<TProfile>(
const { name, message } = isError(error)
? error
: new Error('Encountered invalid error'); // Being a bit safe and not forwarding the bad value
// post error message back to popup if failure
sendWebMessageResponse(res, origin, {
type: 'authorization_response',
error: { name, message },
});
if (state?.flow === 'redirect' && state?.redirectUrl) {
createAuthErrorCookie(res, state?.redirectUrl, {
error: { name, message },
redirectUrl: `${baseUrl}/.backstage/error`,
});
const redirectUrl = new URL(state.redirectUrl);
redirectUrl.searchParams.set('error', 'true');
// set the error in a cookie and redirect user back to sign in where the error can be rendered
res.redirect(redirectUrl.toString());
} else {
// post error message back to popup if failure
sendWebMessageResponse(res, origin, {
type: 'authorization_response',
error: { name, message },
});
}
}
},