Merge pull request #15841 from headphonejames/redirect-oauth-flow

Oauth2 redirect flow for authentication (instead of using popup)
This commit is contained in:
Patrik Oldsberg
2023-03-21 11:02:42 +01:00
committed by GitHub
40 changed files with 462 additions and 343 deletions
@@ -83,56 +83,10 @@ describe('OAuthAdapter', () => {
callbackUrl: 'http://domain.org/auth/test-provider/handler/frame',
};
it('sets the correct headers in start', async () => {
const oauthProvider = new OAuthAdapter(
providerInstance,
oAuthProviderOptions,
);
const mockRequest = {
query: {
scope: 'user',
env: 'development',
},
} as unknown as express.Request;
const defaultState = { nonce: 'nonce', env: 'development' };
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.start(mockRequest, mockResponse);
// nonce cookie checks
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining({
httpOnly: true,
path: '/auth/test-provider/handler',
maxAge: TEN_MINUTES_MS,
domain: 'domain.org',
sameSite: 'lax',
secure: false,
}),
);
// redirect checks
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
expect(mockResponse.statusCode).toEqual(301);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
});
it('sets the refresh cookie if refresh is enabled', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
const state = { nonce: 'nonce', env: 'development' };
const mockRequest = {
const createEncodedQueryMockRequest = (state: any) => {
return {
cookies: {
'test-provider-nonce': 'nonce',
},
@@ -140,12 +94,69 @@ describe('OAuthAdapter', () => {
state: encodeState(state),
},
} as unknown as express.Request;
};
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
redirect: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const mockStartRequest = {
query: {
scope: 'user',
env: 'development',
},
} as unknown as express.Request;
const expectedStartAuthCookieData = {
httpOnly: true,
path: '/auth/test-provider/handler',
maxAge: TEN_MINUTES_MS,
domain: 'domain.org',
sameSite: 'lax',
secure: false,
};
it('sets the correct headers in start', async () => {
const oauthProvider = new OAuthAdapter(
providerInstance,
oAuthProviderOptions,
);
await oauthProvider.start(mockStartRequest, mockResponse);
// nonce cookie checks
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining(expectedStartAuthCookieData),
);
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
expect(mockResponse.statusCode).toEqual(301);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const refreshCookieData = {
...expectedStartAuthCookieData,
path: '/auth/test-provider',
maxAge: THOUSAND_DAYS_MS,
};
it('sets the refresh cookie if refresh is enabled', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
@@ -153,15 +164,26 @@ describe('OAuthAdapter', () => {
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining({
httpOnly: true,
path: '/auth/test-provider',
maxAge: THOUSAND_DAYS_MS,
domain: 'domain.org',
secure: false,
sameSite: 'lax',
}),
expect.objectContaining(refreshCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the refresh cookie if refresh is enabled with redirect', async () => {
const oauthProvider = new OAuthAdapter(providerInstance, {
...oAuthProviderOptions,
isOriginAllowed: () => false,
});
const state = {
...defaultState,
redirectUrl: 'http://localhost:3000',
flow: 'redirect',
};
const mockRequest = createEncodedQueryMockRequest(state);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
});
it('persists scope through cookie if enabled', async () => {
@@ -179,32 +201,15 @@ describe('OAuthAdapter', () => {
});
// First we test the /start request, making sure state is set
const mockStartReq = {
query: {
scope: 'user',
env: 'development',
},
} as unknown as express.Request;
const mockStartRes = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.start(mockStartReq, mockStartRes);
await oauthProvider.start(mockStartRequest, mockResponse);
expect(handlers.start).toHaveBeenCalledTimes(1);
expect(handlers.start).toHaveBeenCalledWith({
query: {
scope: 'user',
env: 'development',
},
...mockStartRequest,
scope: 'user',
state: {
nonce: expect.any(String),
env: 'development',
origin: undefined,
scope: 'user',
},
});
@@ -223,6 +228,7 @@ describe('OAuthAdapter', () => {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
redirect: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.frameHandler(mockHandleReq, mockHandleRes);
@@ -230,17 +236,11 @@ describe('OAuthAdapter', () => {
expect(mockHandleRes.cookie).toHaveBeenCalledWith(
'test-provider-granted-scope',
'user',
expect.objectContaining({
httpOnly: true,
path: '/auth/test-provider',
maxAge: THOUSAND_DAYS_MS,
domain: 'domain.org',
secure: false,
sameSite: 'lax',
}),
expect.objectContaining(refreshCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
// Them make sure scopes are forwarded correctly during refresh
// Then make sure scopes are forwarded correctly during refresh
const mockRefreshReq = {
query: { scope: 'ignore-me' },
cookies: {
@@ -252,6 +252,7 @@ describe('OAuthAdapter', () => {
const mockRefreshRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
redirect: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.refresh(mockRefreshReq, mockRefreshRes);
expect(handlers.refresh).toHaveBeenCalledTimes(1);
@@ -261,8 +262,18 @@ describe('OAuthAdapter', () => {
refreshToken: 'refresh-token',
}),
);
expect(mockRefreshRes.redirect).not.toHaveBeenCalled();
});
const mockRequestWithHeader = {
header: () => 'XMLHttpRequest',
cookies: {
'test-provider-refresh-token': 'token',
},
query: {},
get: jest.fn(),
} as unknown as express.Request;
it('removes refresh cookie and calls logout handler when logging out', async () => {
const logoutSpy = jest.spyOn(providerInstance, 'logout');
const oauthProvider = new OAuthAdapter(providerInstance, {
@@ -270,22 +281,8 @@ describe('OAuthAdapter', () => {
isOriginAllowed: () => false,
});
const mockRequest = {
cookies: {
'test-provider-refresh-token': 'token',
},
header: () => 'XMLHttpRequest',
get: jest.fn(),
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.logout(mockRequest, mockResponse);
expect(mockRequest.get).toHaveBeenCalledTimes(1);
await oauthProvider.logout(mockRequestWithHeader, mockResponse);
expect(mockRequestWithHeader.get).toHaveBeenCalledTimes(1);
expect(logoutSpy).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
@@ -294,6 +291,7 @@ describe('OAuthAdapter', () => {
expect.objectContaining({ path: '/auth/test-provider' }),
);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('gets new access-token when refreshing', async () => {
@@ -302,20 +300,7 @@ describe('OAuthAdapter', () => {
isOriginAllowed: () => false,
});
const mockRequest = {
header: () => 'XMLHttpRequest',
cookies: {
'test-provider-refresh-token': 'token',
},
query: {},
} as unknown as express.Request;
const mockResponse = {
json: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.refresh(mockRequest, mockResponse);
await oauthProvider.refresh(mockRequestWithHeader, mockResponse);
expect(mockResponse.json).toHaveBeenCalledTimes(1);
expect(mockResponse.json).toHaveBeenCalledWith({
...mockResponseData,
@@ -328,6 +313,7 @@ describe('OAuthAdapter', () => {
},
},
});
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets new access-token when old cookie exists', async () => {
@@ -337,20 +323,12 @@ describe('OAuthAdapter', () => {
});
const mockRequest = {
header: () => 'XMLHttpRequest',
...mockRequestWithHeader,
cookies: {
'test-provider-refresh-token': 'old-token',
},
query: {},
get: jest.fn(),
} as unknown as express.Request;
const mockResponse = {
json: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
cookie: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.refresh(mockRequest, mockResponse);
expect(mockRequest.get).toHaveBeenCalledTimes(1);
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
@@ -358,15 +336,9 @@ describe('OAuthAdapter', () => {
expect(mockResponse.cookie).toHaveBeenCalledWith(
'test-provider-refresh-token',
'token',
expect.objectContaining({
httpOnly: true,
path: '/auth/test-provider',
maxAge: THOUSAND_DAYS_MS,
domain: 'domain.org',
secure: false,
sameSite: 'lax',
}),
expect.objectContaining(refreshCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct nonce cookie configuration', async () => {
@@ -380,162 +352,89 @@ describe('OAuthAdapter', () => {
...oAuthProviderOptions,
});
const mockRequest = {
query: {
scope: 'user',
env: 'development',
origin: 'http://domain.org',
},
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.start(mockRequest, mockResponse);
await oauthProvider.start(mockStartRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining({
httpOnly: true,
domain: 'domain.org',
maxAge: TEN_MINUTES_MS,
path: '/auth/test-provider/handler',
secure: false,
sameSite: 'lax',
}),
expect.objectContaining(expectedStartAuthCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct nonce cookie configuration using origin from request', async () => {
const config = {
baseUrl: 'http://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const config = {
baseUrl: 'http://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const mockStartRequestWithOrigin = {
query: {
scope: 'user',
env: 'development',
origin: 'http://other.domain',
},
} as unknown as express.Request;
it('sets the correct nonce cookie configuration using origin from request', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
const mockRequest = {
query: {
scope: 'user',
env: 'development',
origin: 'http://other.domain',
},
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.start(mockRequest, mockResponse);
await oauthProvider.start(mockStartRequestWithOrigin, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining({
httpOnly: true,
domain: 'domain.org',
maxAge: TEN_MINUTES_MS,
path: '/auth/test-provider/handler',
...expectedStartAuthCookieData,
secure: true,
sameSite: 'none',
}),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct cookie configuration using an secure callbackUrl', async () => {
const config = {
baseUrl: 'https://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const secureCookieData = {
...refreshCookieData,
secure: true,
sameSite: 'lax',
maxAge: THOUSAND_DAYS_MS,
};
it('sets the correct cookie configuration using an secure callbackUrl', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
const state = {
nonce: 'nonce',
env: 'development',
};
const mockRequest = {
cookies: {
'test-provider-nonce': 'nonce',
},
query: {
state: encodeState(state),
},
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining({
httpOnly: true,
domain: 'domain.org',
path: '/auth/test-provider',
secure: true,
sameSite: 'lax',
}),
expect.objectContaining(secureCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct cookie configuration when on different domains and secure', async () => {
const config = {
baseUrl: 'https://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const secureSameSiteNoneCookieData = {
...secureCookieData,
sameSite: 'none',
};
it('sets the correct cookie configuration when on different domains and secure', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://authdomain.org/auth/test-provider/handler/frame',
});
const state = {
nonce: 'nonce',
env: 'development',
};
const mockRequest = {
cookies: {
'test-provider-nonce': 'nonce',
},
query: {
state: encodeState(state),
},
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
} as unknown as express.Response;
const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
@@ -543,106 +442,108 @@ describe('OAuthAdapter', () => {
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining({
httpOnly: true,
...secureSameSiteNoneCookieData,
domain: 'authdomain.org',
path: '/auth/test-provider',
secure: true,
sameSite: 'none',
}),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
const configOriginAllowed = {
...config,
isOriginAllowed: () => true,
};
it('sets the correct cookie configuration using origin from state', async () => {
const config = {
baseUrl: 'https://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => true,
};
const oauthProvider = OAuthAdapter.fromConfig(
configOriginAllowed,
providerInstance,
{
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
},
);
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
const state = {
nonce: 'nonce',
env: 'development',
const mockRequest = createEncodedQueryMockRequest({
...defaultState,
origin: 'http://other.domain',
};
const mockRequest = {
cookies: {
'test-provider-nonce': 'nonce',
},
query: {
state: encodeState(state),
},
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
} as unknown as express.Response;
});
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining({
httpOnly: true,
domain: 'domain.org',
path: '/auth/test-provider',
secure: true,
sameSite: 'none',
}),
expect.objectContaining(secureSameSiteNoneCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct cookie configuration using origin from header', async () => {
const config = {
baseUrl: 'https://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const mockRequestWithGetMockReturn = {
header: () => 'XMLHttpRequest',
cookies: {
'test-provider-refresh-token': 'old-token',
},
query: {},
get: jest.fn().mockReturnValue('http://other.domain'),
} as unknown as express.Request;
it('sets the correct cookie configuration using origin from header', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
const mockRequest = {
header: () => 'XMLHttpRequest',
cookies: {
'test-provider-refresh-token': 'old-token',
},
query: {},
get: jest.fn().mockReturnValue('http://other.domain'),
} as unknown as express.Request;
const mockResponse = {
json: jest.fn().mockReturnThis(),
status: jest.fn().mockReturnThis(),
cookie: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.refresh(mockRequest, mockResponse);
expect(mockRequest.get).toHaveBeenCalledTimes(1);
await oauthProvider.refresh(mockRequestWithGetMockReturn, mockResponse);
expect(mockRequestWithGetMockReturn.get).toHaveBeenCalledTimes(1);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
'test-provider-refresh-token',
'token',
expect.objectContaining({
httpOnly: true,
path: '/auth/test-provider',
maxAge: THOUSAND_DAYS_MS,
domain: 'domain.org',
secure: true,
sameSite: 'none',
}),
expect.objectContaining(secureSameSiteNoneCookieData),
);
expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('executed a response redirect when flow query string is set to "redirect"', async () => {
const handlers = {
start: jest.fn(async (_req: { state: OAuthState }) => ({
url: '/url',
status: 301,
})),
handler: jest.fn(async () => ({ response: mockResponseData })),
refresh: jest.fn(async () => ({ response: mockResponseData })),
};
const configWithNoPopupEnabled = {
...configOriginAllowed,
};
const oauthProvider = OAuthAdapter.fromConfig(
configWithNoPopupEnabled,
handlers,
{
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
},
);
const state = {
...defaultState,
origin: 'http://other.domain',
redirectUrl: 'http://domain.org',
flow: 'redirect',
};
const mockRequest = {
...createEncodedQueryMockRequest(state),
get: jest.fn().mockReturnValue('http://other.domain'),
} as unknown as express.Request;
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockRequest.get).not.toHaveBeenCalled();
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).not.toHaveBeenCalled();
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
expect(mockResponse.redirect).toHaveBeenCalledWith('http://domain.org');
});
});
@@ -33,7 +33,11 @@ import {
NotAllowedError,
} from '@backstage/errors';
import { defaultCookieConfigurer, readState, verifyNonce } from './helpers';
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
import {
postMessageResponse,
ensuresXRequestedWith,
WebMessageResponse,
} from '../flow';
import {
OAuthHandlers,
OAuthStartRequest,
@@ -98,6 +102,8 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
const scope = req.query.scope?.toString() ?? '';
const env = req.query.env?.toString();
const origin = req.query.origin?.toString();
const redirectUrl = req.query.redirectUrl?.toString();
const flow = req.query.flow?.toString();
if (!env) {
throw new InputError('No env provided in request query parameters');
@@ -109,7 +115,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
// set a nonce cookie before redirecting to oauth provider
this.setNonceCookie(res, nonce, cookieConfig);
const state: OAuthState = { nonce, env, origin };
const state: OAuthState = { nonce, env, origin, redirectUrl, flow };
// If scopes are persisted then we pass them through the state so that we
// can set the cookie on successful auth
@@ -169,11 +175,21 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
const identity = await this.populateIdentity(response.backstageIdentity);
// post message back to popup if successful
return postMessageResponse(res, appOrigin, {
const responseObj: WebMessageResponse = {
type: 'authorization_response',
response: { ...response, backstageIdentity: identity },
});
};
if (state.flow === 'redirect') {
if (!state.redirectUrl) {
throw new InputError(
'No redirectUrl provided in request query parameters',
);
}
res.redirect(state.redirectUrl);
}
// post message back to popup if successful
return postMessageResponse(res, appOrigin, responseObj);
} catch (error) {
const { name, message } = isError(error)
? error
@@ -90,6 +90,8 @@ export type OAuthState = {
env: string;
origin?: string;
scope?: string;
redirectUrl?: string;
flow?: string;
};
/** @public */