diff --git a/.changeset/violet-dots-relate.md b/.changeset/violet-dots-relate.md
index bc6b75caf9..3e22cdd1c7 100644
--- a/.changeset/violet-dots-relate.md
+++ b/.changeset/violet-dots-relate.md
@@ -2,29 +2,4 @@
'@backstage/core-components': patch
---
-Added option to pass additional headers to ``
-
-The supports signing into Backstage with a Sign Page coming from an external provider e.g. Google IAP, AWS ALB, etc. The component makes requests to a single endpoint `/refresh` on the auth backend to fetch the logged in user session.
-
-If the provider in auth backend expects additional headers such as `x-provider-token`, there is now a way to configure that in `ProxiedSignInPage` using the optional `getHeaders` prop.
-
-Example -
-
-```tsx
-const app = createApp({
- // ...
- components: {
- SignInPage: props => (
- {
- const someValue = await someFn();
- return { 'x-some-key': someValue };
- }}
- />
- ),
- },
- // ...
-});
-```
+Added option to pass additional headers to ``, which are passed along with the request to the underlying provider
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
index ed3a1d73a9..b8c2b84085 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
@@ -164,7 +164,21 @@ describe('ProxiedSignInIdentity', () => {
expect(serverCalled).toHaveBeenCalledTimes(2);
});
- it('handles optional headers correctly', async () => {
+ // dummy response for tests which are only testing the request behaviour
+ const dummySessionResponse = {
+ providerInfo: {},
+ profile: {},
+ backstageIdentity: {
+ token: '',
+ identity: {
+ ownershipEntityRefs: [''],
+ userEntityRef: '',
+ type: 'user',
+ },
+ },
+ };
+
+ it('handles headers passed as a promise', async () => {
let req1: Request;
const getBaseUrl = jest.fn();
const serverCalled = jest.fn().mockImplementation(req => {
@@ -177,19 +191,7 @@ describe('ProxiedSignInIdentity', () => {
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
- // dummy response as we are only testing the request in this test
- ctx.json({
- providerInfo: {},
- profile: {},
- backstageIdentity: {
- token: '',
- identity: {
- ownershipEntityRefs: [''],
- userEntityRef: '',
- type: 'user',
- },
- },
- }),
+ ctx.json(dummySessionResponse),
),
),
);
@@ -198,7 +200,85 @@ describe('ProxiedSignInIdentity', () => {
const identity = new ProxiedSignInIdentity({
provider: 'foo',
discoveryApi: { getBaseUrl },
- getHeaders: getHeaders,
+ headers: getHeaders,
+ });
+
+ getBaseUrl.mockResolvedValue('http://example.com/api/auth');
+
+ await identity.start(); // should not throw
+ expect(getBaseUrl).toHaveBeenCalledTimes(1);
+ expect(getBaseUrl).toHaveBeenLastCalledWith('auth');
+ expect(getHeaders).toHaveBeenCalledTimes(1);
+ expect(serverCalled).toHaveBeenCalledTimes(1);
+
+ expect(req1!).not.toBeUndefined();
+ // required header should be present
+ expect(req1!.headers.get('x-requested-with')).toEqual('XMLHttpRequest');
+ // optional header should be present when passed
+ expect(req1!.headers.get('x-foo')).toEqual('bars');
+ });
+
+ it('handles headers passed as an object', async () => {
+ let req1: Request;
+ const getBaseUrl = jest.fn();
+ const serverCalled = jest.fn().mockImplementation(req => {
+ req1 = req;
+ });
+
+ worker.events.on('request:match', serverCalled);
+ worker.use(
+ rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(dummySessionResponse),
+ ),
+ ),
+ );
+
+ const identity = new ProxiedSignInIdentity({
+ provider: 'foo',
+ discoveryApi: { getBaseUrl },
+ headers: { 'x-foo': 'bars' },
+ });
+
+ getBaseUrl.mockResolvedValue('http://example.com/api/auth');
+
+ await identity.start(); // should not throw
+ expect(getBaseUrl).toHaveBeenCalledTimes(1);
+ expect(getBaseUrl).toHaveBeenLastCalledWith('auth');
+ expect(serverCalled).toHaveBeenCalledTimes(1);
+
+ expect(req1!).not.toBeUndefined();
+ // required header should be present
+ expect(req1!.headers.get('x-requested-with')).toEqual('XMLHttpRequest');
+ // optional header should be present when passed
+ expect(req1!.headers.get('x-foo')).toEqual('bars');
+ });
+
+ it('handles headers passed as a function', async () => {
+ let req1: Request;
+ const getBaseUrl = jest.fn();
+ const serverCalled = jest.fn().mockImplementation(req => {
+ req1 = req;
+ });
+
+ worker.events.on('request:match', serverCalled);
+ worker.use(
+ rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
+ res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(dummySessionResponse),
+ ),
+ ),
+ );
+
+ const getHeaders = jest.fn().mockReturnValue({ 'x-foo': 'bars' });
+ const identity = new ProxiedSignInIdentity({
+ provider: 'foo',
+ discoveryApi: { getBaseUrl },
+ headers: getHeaders,
});
getBaseUrl.mockResolvedValue('http://example.com/api/auth');
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
index c8b4d66837..828e9ab1a0 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
@@ -48,10 +48,15 @@ export function tokenToExpiry(jwtToken: string | undefined): Date {
return new Date(payload.exp * 1000 - DEFAULTS.tokenExpiryMarginMillis);
}
+export type RefreshHeaders =
+ | HeadersInit
+ | (() => HeadersInit)
+ | (() => Promise);
+
type ProxiedSignInIdentityOptions = {
provider: string;
discoveryApi: typeof discoveryApiRef.T;
- getHeaders?: () => Promise;
+ headers?: RefreshHeaders;
};
type State =
@@ -193,7 +198,13 @@ export class ProxiedSignInIdentity implements IdentityApi {
async fetchSession(): Promise {
const baseUrl = await this.options.discoveryApi.getBaseUrl('auth');
- const headers = await this.options.getHeaders?.();
+
+ const headers =
+ typeof this.options.headers === 'function'
+ ? await this.options.headers()
+ : this.options.headers;
+ const mergedHeaders = new Headers(headers);
+ mergedHeaders.set('X-Requested-With', 'XMLHttpRequest');
// Note that we do not use the fetchApi here, since this all happens before
// sign-in completes so there can be no automatic token injection and
@@ -202,7 +213,7 @@ export class ProxiedSignInIdentity implements IdentityApi {
`${baseUrl}/${this.options.provider}/refresh`,
{
signal: this.abortController.signal,
- headers: { ...headers, 'x-requested-with': 'XMLHttpRequest' },
+ headers: mergedHeaders,
credentials: 'include',
},
);
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
index 78ca5a8e84..9d91a31462 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
@@ -23,7 +23,7 @@ import React from 'react';
import { useAsync, useMountEffect } from '@react-hookz/web';
import { ErrorPanel } from '../../components/ErrorPanel';
import { Progress } from '../../components/Progress';
-import { ProxiedSignInIdentity } from './ProxiedSignInIdentity';
+import { ProxiedSignInIdentity, RefreshHeaders } from './ProxiedSignInIdentity';
/**
* Props for {@link ProxiedSignInPage}.
@@ -38,10 +38,10 @@ export type ProxiedSignInPageProps = SignInPageProps & {
provider: string;
/**
- * An optional function which returns a promise resolving with any headers
- * that need to be added to the call made to /refresh endpoint.
+ * Optional headers which are passed along with the request to the
+ * underlying provider
*/
- getHeaders?: () => Promise;
+ headers?: RefreshHeaders;
};
/**
@@ -66,7 +66,7 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
const identity = new ProxiedSignInIdentity({
provider: props.provider,
discoveryApi,
- getHeaders: props.getHeaders,
+ headers: props.headers,
});
await identity.start();