diff --git a/.changeset/violet-dots-relate.md b/.changeset/violet-dots-relate.md
new file mode 100644
index 0000000000..3e22cdd1c7
--- /dev/null
+++ b/.changeset/violet-dots-relate.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-components': patch
+---
+
+Added option to pass additional headers to ``, which are passed along with the request to the underlying provider
diff --git a/docs/auth/index.md b/docs/auth/index.md
index 5702e96cee..fa185c0c23 100644
--- a/docs/auth/index.md
+++ b/docs/auth/index.md
@@ -150,6 +150,31 @@ const app = createApp({
});
```
+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 `headers` prop.
+
+Example:
+
+```tsx
+
+```
+
+Headers can also be returned in an async manner:
+
+```tsx
+ {
+ const someValue = await someFn();
+ return { 'x-some-key': someValue };
+ }}
+/>
+```
+
A downside of this method is that it can be cumbersome to set up for local development.
As a workaround for this, it's possible to dynamically select the sign-in page based on
what environment the app is running in, and then use a different sign-in method for local
diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md
index d8ea9d2bbe..c46b3637cb 100644
--- a/packages/core-components/api-report.md
+++ b/packages/core-components/api-report.md
@@ -807,6 +807,7 @@ export const ProxiedSignInPage: (
// @public
export type ProxiedSignInPageProps = SignInPageProps & {
provider: string;
+ headers?: HeadersInit | (() => HeadersInit) | (() => Promise);
};
// Warning: (ae-missing-release-tag) "Ranker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
index 3a40f5bb4b..b8c2b84085 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.test.ts
@@ -103,7 +103,6 @@ describe('ProxiedSignInIdentity', () => {
},
};
}
-
worker.events.on('request:match', serverCalled);
worker.use(
rest.get('http://example.com/api/auth/foo/refresh', (_, res, ctx) =>
@@ -164,5 +163,137 @@ describe('ProxiedSignInIdentity', () => {
await identity.getSessionAsync(); // now the expiry has passed
expect(serverCalled).toHaveBeenCalledTimes(2);
});
+
+ // 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 => {
+ 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().mockResolvedValue({ 'x-foo': 'bars' });
+ const identity = new ProxiedSignInIdentity({
+ provider: 'foo',
+ discoveryApi: { getBaseUrl },
+ 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');
+
+ 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');
+ });
});
});
diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
index 7633fc43c3..bd1fc1bf39 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInIdentity.ts
@@ -51,6 +51,7 @@ export function tokenToExpiry(jwtToken: string | undefined): Date {
type ProxiedSignInIdentityOptions = {
provider: string;
discoveryApi: typeof discoveryApiRef.T;
+ headers?: HeadersInit | (() => HeadersInit) | (() => Promise);
};
type State =
@@ -193,6 +194,13 @@ export class ProxiedSignInIdentity implements IdentityApi {
async fetchSession(): Promise {
const baseUrl = await this.options.discoveryApi.getBaseUrl('auth');
+ 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
// similar.
@@ -200,7 +208,7 @@ export class ProxiedSignInIdentity implements IdentityApi {
`${baseUrl}/${this.options.provider}/refresh`,
{
signal: this.abortController.signal,
- 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 965c603004..ea095cd8ab 100644
--- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
+++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.tsx
@@ -36,6 +36,12 @@ export type ProxiedSignInPageProps = SignInPageProps & {
* a properly configured auth provider ID in the auth backend.
*/
provider: string;
+
+ /**
+ * Optional headers which are passed along with the request to the
+ * underlying provider
+ */
+ headers?: HeadersInit | (() => HeadersInit) | (() => Promise);
};
/**
@@ -60,6 +66,7 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
const identity = new ProxiedSignInIdentity({
provider: props.provider,
discoveryApi,
+ headers: props.headers,
});
await identity.start();