Merge pull request #25823 from stephenglass/fix-redirect-error-handling
Fix error handling using auth redirect flow
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/plugin-auth-node': patch
|
||||
---
|
||||
|
||||
Fix authentication error handling using redirect flow via `enableExperimentalRedirectFlow` config. If an error is caught during authentication, the user is redirected back to app origin with `error` query parameter containing the error message.
|
||||
@@ -37,6 +37,7 @@ import { GridItem, useStyles } from './styles';
|
||||
import { IdentityProviders, SignInProviderConfig } from './types';
|
||||
import { coreComponentsTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
type MultiSignInPageProps = SignInPageProps & {
|
||||
providers: IdentityProviders;
|
||||
@@ -114,6 +115,10 @@ export const SingleSignInPage = ({
|
||||
// displayed for a split second when the user is already logged-in.
|
||||
const [showLoginPage, setShowLoginPage] = useState<boolean>(false);
|
||||
|
||||
// User was redirected back to sign in page with error from auth redirect flow
|
||||
const [searchParams, _setSearchParams] = useSearchParams();
|
||||
const errorParam = searchParams.get('error');
|
||||
|
||||
type LoginOpts = { checkExisting?: boolean; showPopup?: boolean };
|
||||
const login = async ({ checkExisting, showPopup }: LoginOpts) => {
|
||||
try {
|
||||
@@ -126,7 +131,7 @@ export const SingleSignInPage = ({
|
||||
}
|
||||
|
||||
// If no session exists, show the sign-in page
|
||||
if (!identityResponse && (showPopup || auto)) {
|
||||
if (!identityResponse && (showPopup || auto) && !errorParam) {
|
||||
// Unless auto is set to true, this step should not happen.
|
||||
// When user intentionally clicks the Sign In button, autoShowPopup is set to true
|
||||
setShowLoginPage(true);
|
||||
@@ -160,7 +165,12 @@ export const SingleSignInPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
useMountEffect(() => login({ checkExisting: true }));
|
||||
useMountEffect(() => {
|
||||
if (errorParam) {
|
||||
setError(new Error(errorParam));
|
||||
}
|
||||
login({ checkExisting: true });
|
||||
});
|
||||
|
||||
return showLoginPage ? (
|
||||
<Page themeId="home">
|
||||
|
||||
@@ -31,6 +31,11 @@ import { commonProvider } from './commonProvider';
|
||||
import { guestProvider } from './guestProvider';
|
||||
import { customProvider } from './customProvider';
|
||||
import { IdentityApiSignOutProxy } from './IdentityApiSignOutProxy';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useMountEffect } from '@react-hookz/web';
|
||||
import { ForwardedError } from '@backstage/errors';
|
||||
import { coreComponentsTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider';
|
||||
|
||||
@@ -88,6 +93,19 @@ export const useSignInProviders = (
|
||||
const apiHolder = useApiHolder();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const { t } = useTranslationRef(coreComponentsTranslationRef);
|
||||
// User was redirected back to sign in page with error from auth redirect flow
|
||||
const [searchParams, _setSearchParams] = useSearchParams();
|
||||
|
||||
useMountEffect(() => {
|
||||
const errorParam = searchParams.get('error');
|
||||
if (errorParam) {
|
||||
errorApi.post(
|
||||
new ForwardedError(t('signIn.loginFailed'), new Error(errorParam)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// This decorates the result with sign out logic from this hook
|
||||
const handleWrappedResult = useCallback(
|
||||
(identityApi: IdentityApi) => {
|
||||
|
||||
@@ -742,5 +742,33 @@ describe('createOAuthRouteHandlers', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set error search param and redirect on caught error', async () => {
|
||||
const app = wrapInApp(createOAuthRouteHandlers(baseConfig));
|
||||
const res = await request(app)
|
||||
.get('/my-provider/handler/frame')
|
||||
.query({
|
||||
state: encodeOAuthState({
|
||||
env: 'development',
|
||||
nonce: '123',
|
||||
flow: 'redirect',
|
||||
redirectUrl: 'http://localhost:3000',
|
||||
}),
|
||||
});
|
||||
|
||||
// Check if it redirects on error
|
||||
expect(res.status).toBe(302);
|
||||
|
||||
// Extract the redirect URL from the location header
|
||||
const redirectLocation = res.header.location;
|
||||
expect(redirectLocation).toBeDefined();
|
||||
|
||||
// Create a URL object from the redirect location
|
||||
const redirectUrl = new URL(redirectLocation);
|
||||
|
||||
// Verify that the 'error' search param is set with the encoded error message
|
||||
const errorMessage = redirectUrl.searchParams.get('error');
|
||||
expect(errorMessage).toBe('Auth response is missing cookie nonce');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -164,9 +164,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 +249,20 @@ 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) {
|
||||
const redirectUrl = new URL(state.redirectUrl);
|
||||
redirectUrl.searchParams.set('error', message);
|
||||
|
||||
// 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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user