accept objects, functions and promises for headers

Signed-off-by: Abhinav Rastogi <abhinav.rastogi@harness.io>
This commit is contained in:
Abhinav Rastogi
2022-12-12 16:29:35 +05:30
parent 6c9fbd1957
commit 0548ef8ed8
4 changed files with 115 additions and 49 deletions
+1 -26
View File
@@ -2,29 +2,4 @@
'@backstage/core-components': patch
---
Added option to pass additional headers to `<ProxiedSignInPage />`
The <ProxiedSignInPage /> 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 => (
<ProxiedSignInPage
{...props}
provider="my-custom-provider"
getHeaders={async () => {
const someValue = await someFn();
return { 'x-some-key': someValue };
}}
/>
),
},
// ...
});
```
Added option to pass additional headers to `<ProxiedSignInPage />`, which are passed along with the request to the underlying provider
@@ -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');
@@ -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<HeadersInit>);
type ProxiedSignInIdentityOptions = {
provider: string;
discoveryApi: typeof discoveryApiRef.T;
getHeaders?: () => Promise<HeadersInit>;
headers?: RefreshHeaders;
};
type State =
@@ -193,7 +198,13 @@ export class ProxiedSignInIdentity implements IdentityApi {
async fetchSession(): Promise<ProxiedSession> {
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',
},
);
@@ -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<HeadersInit>;
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();