Merge pull request #9136 from backstage/eide/auth-callback-urls
[auth-backend]: Support callbackUrl when setting cookie configuration
This commit is contained in:
@@ -478,7 +478,11 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
handlers: OAuthHandlers,
|
||||
options: Pick<
|
||||
Options,
|
||||
'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer'
|
||||
| 'providerId'
|
||||
| 'persistScopes'
|
||||
| 'disableRefresh'
|
||||
| 'tokenIssuer'
|
||||
| 'callbackUrl'
|
||||
>,
|
||||
): OAuthAdapter;
|
||||
// (undocumented)
|
||||
|
||||
@@ -347,4 +347,85 @@ describe('OAuthAdapter', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the correct cookie configuration using the base url', async () => {
|
||||
const config = {
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
|
||||
const oauthProvider = OAuthAdapter.fromConfig(
|
||||
config,
|
||||
providerInstance,
|
||||
oAuthProviderOptions,
|
||||
);
|
||||
|
||||
const mockRequest = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
},
|
||||
} 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);
|
||||
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
domain: 'domain.org',
|
||||
path: '/auth/test-provider/handler',
|
||||
secure: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets the correct cookie configuration using a callbackUrl', async () => {
|
||||
const config = {
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://authdomain.org/auth/test-provider/handler/frame',
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
},
|
||||
} 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);
|
||||
|
||||
expect(mockResponse.cookie).toBeCalledTimes(1);
|
||||
expect(mockResponse.cookie).toBeCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
domain: 'authdomain.org',
|
||||
path: '/auth/test-provider/handler',
|
||||
secure: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
NotAllowedError,
|
||||
} from '@backstage/errors';
|
||||
import { TokenIssuer } from '../../identity/types';
|
||||
import { readState, verifyNonce } from './helpers';
|
||||
import { getCookieConfig, readState, verifyNonce } from './helpers';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
|
||||
import {
|
||||
OAuthHandlers,
|
||||
@@ -58,25 +58,32 @@ export type Options = {
|
||||
appOrigin: string;
|
||||
tokenIssuer: TokenIssuer;
|
||||
isOriginAllowed: (origin: string) => boolean;
|
||||
callbackUrl?: string;
|
||||
};
|
||||
|
||||
export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
static fromConfig(
|
||||
config: AuthProviderConfig,
|
||||
handlers: OAuthHandlers,
|
||||
options: Pick<
|
||||
Options,
|
||||
'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer'
|
||||
| 'providerId'
|
||||
| 'persistScopes'
|
||||
| 'disableRefresh'
|
||||
| 'tokenIssuer'
|
||||
| 'callbackUrl'
|
||||
>,
|
||||
): OAuthAdapter {
|
||||
const { origin: appOrigin } = new URL(config.appUrl);
|
||||
const secure = config.baseUrl.startsWith('https://');
|
||||
const url = new URL(config.baseUrl);
|
||||
const cookiePath = `${url.pathname}/${options.providerId}`;
|
||||
const authUrl = new URL(options.callbackUrl ?? config.baseUrl);
|
||||
const { cookieDomain, cookiePath, secure } = getCookieConfig(
|
||||
authUrl,
|
||||
options.providerId,
|
||||
);
|
||||
|
||||
return new OAuthAdapter(handlers, {
|
||||
...options,
|
||||
appOrigin,
|
||||
cookieDomain: url.hostname,
|
||||
cookieDomain,
|
||||
cookiePath,
|
||||
secure,
|
||||
isOriginAllowed: config.isOriginAllowed,
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import { verifyNonce, encodeState, readState } from './helpers';
|
||||
import {
|
||||
verifyNonce,
|
||||
encodeState,
|
||||
readState,
|
||||
getCookieConfig,
|
||||
} from './helpers';
|
||||
|
||||
describe('OAuthProvider Utils', () => {
|
||||
describe('encodeState', () => {
|
||||
@@ -104,4 +109,33 @@ describe('OAuthProvider Utils', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCookieConfig', () => {
|
||||
it('should set the correct domain and path for a base url', () => {
|
||||
const mockAuthUrl = new URL('http://domain.org/auth');
|
||||
expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({
|
||||
cookieDomain: 'domain.org',
|
||||
cookiePath: '/auth/test-provider',
|
||||
secure: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the correct domain and path for a url containing a frame handler', () => {
|
||||
const mockAuthUrl = new URL(
|
||||
'http://domain.org/auth/test-provider/handler/frame',
|
||||
);
|
||||
expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({
|
||||
cookieDomain: 'domain.org',
|
||||
cookiePath: '/auth/test-provider',
|
||||
secure: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set the secure flag if url is using https', () => {
|
||||
const mockAuthUrl = new URL('https://domain.org/auth');
|
||||
expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({
|
||||
secure: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,3 +57,21 @@ export const verifyNonce = (req: express.Request, providerId: string) => {
|
||||
throw new Error('Invalid nonce');
|
||||
}
|
||||
};
|
||||
|
||||
export const getCookieConfig = (authUrl: URL, providerId: string) => {
|
||||
const { hostname: cookieDomain, pathname, protocol } = authUrl;
|
||||
const secure = protocol === 'https:';
|
||||
|
||||
// If the provider supports callbackUrls, the pathname will
|
||||
// contain the complete path to the frame handler so we need
|
||||
// to slice off the trailing part of the path.
|
||||
const cookiePath = pathname.endsWith(`${providerId}/handler/frame`)
|
||||
? pathname.slice(0, -'/handler/frame'.length)
|
||||
: `${pathname}/${providerId}`;
|
||||
|
||||
return {
|
||||
cookieDomain,
|
||||
cookiePath,
|
||||
secure,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -314,6 +314,7 @@ export const createGithubProvider = (
|
||||
persistScopes: true,
|
||||
providerId,
|
||||
tokenIssuer,
|
||||
callbackUrl,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user