Merge pull request #15161 from harness/headers-for-session-refresh

Adding option to pass additional headers in ProxiedSignInPage
This commit is contained in:
Fredrik Adelöw
2022-12-12 13:50:42 +01:00
committed by GitHub
6 changed files with 179 additions and 2 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Added option to pass additional headers to `<ProxiedSignInPage />`, which are passed along with the request to the underlying provider
+25
View File
@@ -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
<ProxiedSignInPage
{...props}
provider="my-custom-provider"
headers={{ 'x-some-key': someValue }}
/>
```
Headers can also be returned in an async manner:
```tsx
<ProxiedSignInPage
{...props}
provider="my-custom-provider"
headers={async () => {
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
+1
View File
@@ -807,6 +807,7 @@ export const ProxiedSignInPage: (
// @public
export type ProxiedSignInPageProps = SignInPageProps & {
provider: string;
headers?: HeadersInit | (() => HeadersInit) | (() => Promise<HeadersInit>);
};
// Warning: (ae-missing-release-tag) "Ranker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -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');
});
});
});
@@ -51,6 +51,7 @@ export function tokenToExpiry(jwtToken: string | undefined): Date {
type ProxiedSignInIdentityOptions = {
provider: string;
discoveryApi: typeof discoveryApiRef.T;
headers?: HeadersInit | (() => HeadersInit) | (() => Promise<HeadersInit>);
};
type State =
@@ -193,6 +194,13 @@ export class ProxiedSignInIdentity implements IdentityApi {
async fetchSession(): Promise<ProxiedSession> {
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',
},
);
@@ -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<HeadersInit>);
};
/**
@@ -60,6 +66,7 @@ export const ProxiedSignInPage = (props: ProxiedSignInPageProps) => {
const identity = new ProxiedSignInIdentity({
provider: props.provider,
discoveryApi,
headers: props.headers,
});
await identity.start();