accept objects, functions and promises for headers
Signed-off-by: Abhinav Rastogi <abhinav.rastogi@harness.io>
This commit is contained in:
@@ -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
|
||||
|
||||
+95
-15
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user