Merge pull request #921 from spotify/auth-backend-subroutes
Auth backend - provider and routes
This commit is contained in:
@@ -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>
|
||||
`);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user