Merge pull request #1082 from spotify/google-auth-tests

Auth tests: PassportStrategyHelper
This commit is contained in:
Raghunandan Balachandran
2020-06-01 14:32:35 +02:00
committed by GitHub
4 changed files with 272 additions and 52 deletions
@@ -0,0 +1,21 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
describe('OAuthProvider', () => {
it('unbreak test runner', () => {
expect(true).toBeTruthy();
});
});
@@ -16,9 +16,12 @@
import express, { CookieOptions } from 'express';
import crypto from 'crypto';
import { AuthProviderRouteHandlers, OAuthProviderHandlers } from './types';
import {
AuthResponse,
AuthProviderRouteHandlers,
OAuthProviderHandlers,
} from './types';
import { InputError } from '@backstage/backend-common';
import { postMessageResponse, ensuresXRequestedWith } from './utils';
export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000;
export const TEN_MINUTES_MS = 600 * 1000;
@@ -86,6 +89,38 @@ export const removeRefreshTokenCookie = (
res.cookie(`${provider}-refresh-token`, '', options);
};
export const postMessageResponse = (
res: express.Response,
data: AuthResponse,
) => {
const jsonData = JSON.stringify(data);
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Frame-Options', 'sameorigin');
// TODO: Make target app origin configurable globally
res.end(`
<html>
<body>
<script>
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), 'http://localhost:3000')
window.close()
</script>
</body>
</html>
`);
};
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
return false;
}
return true;
};
export class OAuthProvider implements AuthProviderRouteHandlers {
private readonly provider: string;
private readonly providerHandlers: OAuthProviderHandlers;
@@ -0,0 +1,214 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import passport from 'passport';
import {
executeRedirectStrategy,
executeFrameHandlerStrategy,
executeRefreshTokenStrategy,
} from './PassportStrategyHelper';
const mockRequest = ({} as unknown) as express.Request;
describe('PassportStrategyHelper', () => {
class MyCustomRedirectStrategy extends passport.Strategy {
authenticate() {
this.redirect('a', 302);
}
}
describe('executeRedirectStrategy', () => {
it('should call authenticate and resolve with RedirectInfo', async () => {
const mockStrategy = new MyCustomRedirectStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const redirectStrategyPromise = executeRedirectStrategy(
mockRequest,
mockStrategy,
{},
);
expect(spyAuthenticate).toBeCalledTimes(1);
await expect(redirectStrategyPromise).resolves.toStrictEqual(
expect.objectContaining({ url: 'a', status: 302 }),
);
});
});
describe('executeFrameHandlerStrategy', () => {
class MyCustomAuthSuccessStrategy extends passport.Strategy {
authenticate() {
this.success(
{ accessToken: 'ACCESS_TOKEN' },
{ refreshToken: 'REFRESH_TOKEN' },
);
}
}
class MyCustomAuthErrorStrategy extends passport.Strategy {
authenticate() {
this.error(new Error('MyCustomAuth error'));
}
}
class MyCustomAuthRedirectStrategy extends passport.Strategy {
authenticate() {
this.redirect('URL', 302);
}
}
class MyCustomAuthFailStrategy extends passport.Strategy {
authenticate() {
this.fail('challenge', 302);
}
}
it('should resolve with user and info on success', async () => {
const mockStrategy = new MyCustomAuthSuccessStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toBeCalledTimes(1);
await expect(frameHandlerStrategyPromise).resolves.toStrictEqual(
expect.objectContaining({
user: { accessToken: 'ACCESS_TOKEN' },
info: { refreshToken: 'REFRESH_TOKEN' },
}),
);
});
it('should reject on error', async () => {
const mockStrategy = new MyCustomAuthErrorStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toBeCalledTimes(1);
await expect(frameHandlerStrategyPromise).rejects.toThrow(
'Authentication failed, Error: MyCustomAuth error',
);
});
it('should reject on redirect', async () => {
const mockStrategy = new MyCustomAuthRedirectStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toBeCalledTimes(1);
await expect(frameHandlerStrategyPromise).rejects.toThrow(
'Unexpected redirect',
);
});
it('should reject on fail', async () => {
const mockStrategy = new MyCustomAuthFailStrategy();
const spyAuthenticate = jest.spyOn(mockStrategy, 'authenticate');
const frameHandlerStrategyPromise = executeFrameHandlerStrategy(
mockRequest,
mockStrategy,
);
expect(spyAuthenticate).toBeCalledTimes(1);
await expect(frameHandlerStrategyPromise).rejects.toThrow();
});
});
describe('executeRefreshTokenStrategy', () => {
it('should resolve with a new access token, scope and expiry', async () => {
class MyCustomOAuth2Success {
getOAuthAccessToken(
_refreshToken: string,
_options: any,
callback: Function,
) {
callback(null, 'ACCESS_TOKEN', 'REFRESH_TOKEN', {
scope: 'a',
expires_in: 10,
});
}
}
class MyCustomRefreshTokenSuccess extends passport.Strategy {
// @ts-ignore
private _oauth2 = new MyCustomOAuth2Success();
}
const mockStrategy = new MyCustomRefreshTokenSuccess();
const refreshTokenPromise = executeRefreshTokenStrategy(
mockStrategy,
'REFRESH_TOKEN',
'a',
);
await expect(refreshTokenPromise).resolves.toStrictEqual(
expect.objectContaining({
accessToken: 'ACCESS_TOKEN',
params: expect.objectContaining({ scope: 'a', expires_in: 10 }),
}),
);
});
it('should reject with an error if refresh failed', async () => {
class MyCustomOAuth2Error {
getOAuthAccessToken(
_refreshToken: string,
_options: any,
callback: Function,
) {
callback(new Error('Unknown error'));
}
}
class MyCustomRefreshTokenSuccess extends passport.Strategy {
// @ts-ignore
private _oauth2 = new MyCustomOAuth2Error();
}
const mockStrategy = new MyCustomRefreshTokenSuccess();
const refreshTokenPromise = executeRefreshTokenStrategy(
mockStrategy,
'REFRESH_TOKEN',
'a',
);
await expect(refreshTokenPromise).rejects.toThrow(
'Failed to refresh access token Error: Unknown error',
);
});
it('should reject with an error if access token missing in refresh callback', async () => {
class MyCustomOAuth2AccessTokenMissing {
getOAuthAccessToken(
_refreshToken: string,
_options: any,
callback: Function,
) {
callback(null, '');
}
}
class MyCustomRefreshTokenSuccess extends passport.Strategy {
// @ts-ignore
private _oauth2 = new MyCustomOAuth2AccessTokenMissing();
}
const mockStrategy = new MyCustomRefreshTokenSuccess();
const refreshTokenPromise = executeRefreshTokenStrategy(
mockStrategy,
'REFRESH_TOKEN',
'a',
);
await expect(refreshTokenPromise).rejects.toThrow(
'Failed to refresh access token, no access token received',
);
});
});
});
@@ -1,50 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import express from 'express';
import { AuthResponse } from './types';
export const postMessageResponse = (
res: express.Response,
data: AuthResponse,
) => {
const jsonData = JSON.stringify(data);
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Frame-Options', 'sameorigin');
// TODO: Make target app origin configurable globally
res.end(`
<html>
<body>
<script>
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), 'http://localhost:3000')
window.close()
</script>
</body>
</html>
`);
};
export const ensuresXRequestedWith = (req: express.Request) => {
const requiredHeader = req.header('X-Requested-With');
if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') {
return false;
}
return true;
};