Merge pull request #921 from spotify/auth-backend-subroutes

Auth backend - provider and routes
This commit is contained in:
Raghunandan Balachandran
2020-05-20 17:42:07 +02:00
committed by GitHub
8 changed files with 532 additions and 137 deletions
+5 -1
View File
@@ -24,7 +24,11 @@
"helmet": "^3.22.0",
"morgan": "^1.10.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
"yn": "^4.0.0",
"passport": "0.4.1",
"passport-google-oauth20": "2.0.0",
"@types/passport": "1.0.3",
"@types/passport-google-oauth20": "2.0.3"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.6",
@@ -0,0 +1,26 @@
/*
* 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.
*/
export const providers = [
{
provider: 'google',
options: {
clientID: process.env.AUTH_GOOGLE_CLIENT_ID!,
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
callbackURL: 'http://localhost:7000/auth/google/handler/frame',
},
},
];
@@ -0,0 +1,81 @@
/*
* 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 passport from 'passport';
import express from 'express';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import {
AuthProvider,
AuthProviderRouteHandlers,
AuthProviderConfig,
} from './../types';
import { postMessageResponse } from './../utils';
export class GoogleAuthProvider
implements AuthProvider, AuthProviderRouteHandlers {
private readonly providerConfig: AuthProviderConfig;
constructor(providerConfig: AuthProviderConfig) {
this.providerConfig = providerConfig;
}
start(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
const scopes = req.query.scopes?.toString().split(',');
return passport.authenticate('google', {
scope: scopes,
accessType: 'offline',
prompt: 'consent',
})(req, res, next);
}
frameHandler(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) {
return passport.authenticate('google', (_, user) => {
postMessageResponse(res, {
type: 'auth-result',
payload: user,
});
})(req, res, next);
}
logout(_req: express.Request, res: express.Response) {
return new Promise((resolve) => {
res.send('logout!');
resolve();
});
}
strategy(): passport.Strategy {
return new GoogleStrategy(
{ ...this.providerConfig.options, passReqToCallback: true },
(
_req: any,
accessToken: any,
refreshToken: any,
profile: any,
cb: any,
) => {
cb(undefined, { profile, accessToken, refreshToken });
},
);
}
}
@@ -0,0 +1,51 @@
/*
* 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 Router from 'express-promise-router';
import {
AuthProviderRouteHandlers,
AuthProviderFactories,
AuthProviderConfig,
} from './types';
import { GoogleAuthProvider } from './google/provider';
const providerFactories: AuthProviderFactories = {
google: GoogleAuthProvider,
};
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
const router = Router();
router.get('/start', provider.start);
router.get('/handler/frame', provider.frameHandler);
router.get('/logout', provider.logout);
if (provider.refresh) {
router.get('/refreshToken', provider.refresh);
}
return router;
};
export const makeProvider = (config: AuthProviderConfig) => {
const providerId = config.provider;
const ProviderImpl = providerFactories[providerId];
if (!ProviderImpl) {
throw Error(`Provider Implementation missing for provider: ${providerId}`);
}
const providerInstance = new ProviderImpl(config);
const strategy = providerInstance.strategy();
const providerRouter = defaultRouter(providerInstance);
return { providerId, strategy, providerRouter };
};
@@ -0,0 +1,73 @@
/*
* 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';
export type AuthProviderConfig = {
provider: string;
options: any;
};
export interface AuthProvider {
strategy(): passport.Strategy;
router?(): express.Router;
}
export interface AuthProviderRouteHandlers {
start(
req: express.Request,
res: express.Response,
next: express.NextFunction,
): Promise<any>;
frameHandler(
req: express.Request,
res: express.Response,
next: express.NextFunction,
): Promise<any>;
refresh?(
req: express.Request,
res: express.Response,
next: express.NextFunction,
): Promise<any>;
logout(
req: express.Request,
res: express.Response,
next: express.NextFunction,
): Promise<any>;
}
export type AuthProviderFactories = {
[key: string]: {
new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers;
};
};
export type AuthInfo = {
profile: passport.Profile;
accessToken: string;
expiresInSeconds?: number;
};
export type AuthResponse =
| {
type: 'auth-result';
payload: AuthInfo;
}
| {
type: 'auth-result';
error: Error;
};
@@ -0,0 +1,37 @@
/*
* 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('X-Frame-Options', 'sameorigin');
res.end(`
<html>
<body>
<script>
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), location.origin)
</script>
</body>
</html>
`);
};
+31 -7
View File
@@ -16,7 +16,10 @@
import express from 'express';
import Router from 'express-promise-router';
import passport from 'passport';
import { Logger } from 'winston';
import { providers } from './../providers/config';
import { makeProvider } from '../providers';
export interface RouterOptions {
logger: Logger;
@@ -25,16 +28,37 @@ export interface RouterOptions {
export async function createRouter(
options: RouterOptions,
): Promise<express.Router> {
const logger = options.logger.child({ plugin: 'auth' });
const router = Router();
const logger = options.logger.child({ plugin: 'auth' });
const providerRouters: { [key: string]: express.Router } = {};
router.get('/ping', async (_req, res) => {
res.status(200).send('pong');
// configure all the providers
for (const providerConfig of providers) {
const { providerId, strategy, providerRouter } = makeProvider(
providerConfig,
);
logger.info(`Configuring provider: ${providerId}`);
passport.use(strategy);
providerRouters[providerId] = providerRouter;
}
passport.serializeUser((user, done) => {
done(null, user);
});
const app = express();
app.set('logger', logger);
app.use(router);
passport.deserializeUser((user, done) => {
done(null, user);
});
return app;
router.use(passport.initialize());
router.use(passport.session());
for (const providerId in providerRouters) {
if (providerRouters.hasOwnProperty(providerId)) {
const providerRouter = providerRouters[providerId];
router.use(`/${providerId}`, providerRouter);
}
}
return router;
}