Merge pull request #1097 from spotify/rugvip/idp
plugins/auth-backend: added basic SAML auth
This commit is contained in:
@@ -19,6 +19,14 @@ read -r AUTH_GOOGLE_CLIENT_SECRET
|
||||
export AUTH_GOOGLE_CLIENT_SECRET
|
||||
run `yarn start` in packages/backend folder
|
||||
|
||||
### SAML
|
||||
|
||||
To try out SAML, you can use the mock identity provider:
|
||||
|
||||
```bash
|
||||
./scripts/start-saml-idp.sh
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- (The Backstage homepage)[https://backstage.io]
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@types/passport": "^1.0.3",
|
||||
"@types/passport-github2": "^1.2.4",
|
||||
"@types/passport-google-oauth20": "^2.0.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
@@ -31,11 +32,14 @@
|
||||
"passport": "^0.4.1",
|
||||
"passport-github2": "^0.1.12",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-saml": "^1.3.3",
|
||||
"winston": "^3.2.1",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.6",
|
||||
"@types/body-parser": "^1.19.0",
|
||||
"@types/passport-saml": "^1.1.2",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"tsc-watch": "^4.2.3"
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
*.pem
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
|
||||
cd "$DIR"
|
||||
|
||||
if [[ ! -f idp-public-cert.pem ]]; then
|
||||
echo "Generating new SAML Certificates"
|
||||
openssl req \
|
||||
-x509 \
|
||||
-newkey rsa:1024 \
|
||||
-days 3650 \
|
||||
-nodes \
|
||||
-subj '/CN=localhost' \
|
||||
-keyout "idp-private-key.pem" \
|
||||
-out "idp-public-cert.pem"
|
||||
fi
|
||||
|
||||
echo "Downloading and starting SAML-IdP"
|
||||
export NPM_CONFIG_REGISTRY=https://registry.npmjs.org
|
||||
exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001
|
||||
@@ -55,7 +55,7 @@ export const executeFrameHandlerStrategy = async (
|
||||
reject(new Error('Unexpected redirect'));
|
||||
};
|
||||
|
||||
strategy.authenticate(req);
|
||||
strategy.authenticate(req, {});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -32,4 +32,12 @@ export const providers = [
|
||||
},
|
||||
disableRefresh: true,
|
||||
},
|
||||
{
|
||||
provider: 'saml',
|
||||
options: {
|
||||
path: '/auth/saml/handler/frame',
|
||||
entryPoint: 'http://localhost:7001/',
|
||||
issuer: 'passport-saml',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,37 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
AuthProviderFactories,
|
||||
AuthProviderRouteHandlers,
|
||||
AuthProviderConfig,
|
||||
} from './types';
|
||||
import { GoogleAuthProvider } from './google';
|
||||
import { GithubAuthProvider } from './github';
|
||||
import { OAuthProvider } from './OAuthProvider';
|
||||
import Router from 'express-promise-router';
|
||||
import { createGithubProvider } from './github';
|
||||
import { createGoogleProvider } from './google';
|
||||
import { createSamlProvider } from './saml';
|
||||
import { AuthProviderFactory, AuthProviderConfig } from './types';
|
||||
|
||||
export class ProviderFactories {
|
||||
private static readonly providerFactories: AuthProviderFactories = {
|
||||
google: GoogleAuthProvider,
|
||||
github: GithubAuthProvider,
|
||||
};
|
||||
const factories: { [providerId: string]: AuthProviderFactory } = {
|
||||
google: createGoogleProvider,
|
||||
github: createGithubProvider,
|
||||
saml: createSamlProvider,
|
||||
};
|
||||
|
||||
public static getProviderFactory(
|
||||
config: AuthProviderConfig,
|
||||
): AuthProviderRouteHandlers {
|
||||
const providerId = config.provider;
|
||||
const ProviderImpl = ProviderFactories.providerFactories[providerId];
|
||||
if (!ProviderImpl) {
|
||||
throw Error(
|
||||
`Provider Implementation missing for : ${providerId} auth provider`,
|
||||
);
|
||||
}
|
||||
const providerInstance = new ProviderImpl(config);
|
||||
const oauthProvider = new OAuthProvider(
|
||||
providerInstance,
|
||||
providerId,
|
||||
config.disableRefresh,
|
||||
);
|
||||
return oauthProvider;
|
||||
export function createAuthProvider(providerId: string, config: any) {
|
||||
const factory = factories[providerId];
|
||||
if (!factory) {
|
||||
throw Error(`No auth provider available for '${providerId}'`);
|
||||
}
|
||||
return factory(config);
|
||||
}
|
||||
|
||||
export const createAuthProviderRouter = (config: AuthProviderConfig) => {
|
||||
const providerId = config.provider;
|
||||
const provider = createAuthProvider(providerId, config);
|
||||
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.post('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.get('/logout', provider.logout.bind(provider));
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
}
|
||||
return router;
|
||||
};
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { GithubAuthProvider } from './provider';
|
||||
export { createGithubProvider } from './provider';
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
AuthInfoBase,
|
||||
AuthInfoPrivate,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../OAuthProvider';
|
||||
|
||||
export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
@@ -57,3 +58,9 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
|
||||
return await executeFrameHandlerStrategy(req, this._strategy);
|
||||
}
|
||||
}
|
||||
|
||||
export function createGithubProvider(config: AuthProviderConfig) {
|
||||
const provider = new GithubAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider, true);
|
||||
return oauthProvider;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { GoogleAuthProvider } from './provider';
|
||||
export { createGoogleProvider } from './provider';
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
RedirectInfo,
|
||||
AuthProviderConfig,
|
||||
} from '../types';
|
||||
import { OAuthProvider } from '../OAuthProvider';
|
||||
|
||||
export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
private readonly providerConfig: AuthProviderConfig;
|
||||
@@ -87,3 +88,9 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createGoogleProvider(config: AuthProviderConfig) {
|
||||
const provider = new GoogleAuthProvider(config);
|
||||
const oauthProvider = new OAuthProvider(provider, config.provider);
|
||||
return oauthProvider;
|
||||
}
|
||||
|
||||
@@ -14,24 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Router from 'express-promise-router';
|
||||
import { AuthProviderRouteHandlers, AuthProviderConfig } from './types';
|
||||
import { ProviderFactories } from './factories';
|
||||
|
||||
export const defaultRouter = (provider: AuthProviderRouteHandlers) => {
|
||||
const router = Router();
|
||||
router.get('/start', provider.start.bind(provider));
|
||||
router.get('/handler/frame', provider.frameHandler.bind(provider));
|
||||
router.get('/logout', provider.logout.bind(provider));
|
||||
if (provider.refresh) {
|
||||
router.get('/refresh', provider.refresh.bind(provider));
|
||||
}
|
||||
return router;
|
||||
};
|
||||
|
||||
export const makeProvider = (config: AuthProviderConfig) => {
|
||||
const providerId = config.provider;
|
||||
const oauthProvider = ProviderFactories.getProviderFactory(config);
|
||||
const providerRouter = defaultRouter(oauthProvider);
|
||||
return { providerId, providerRouter };
|
||||
};
|
||||
export { createAuthProviderRouter } from './factories';
|
||||
|
||||
+1
-7
@@ -14,10 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defaultRouter } from '.';
|
||||
|
||||
describe('test', () => {
|
||||
it('unbreaks the test runner', () => {
|
||||
expect(defaultRouter).toBeDefined();
|
||||
});
|
||||
});
|
||||
export { createSamlProvider } from './provider';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 { Strategy as SamlStrategy } from 'passport-saml';
|
||||
import {
|
||||
executeFrameHandlerStrategy,
|
||||
executeRedirectStrategy,
|
||||
} from '../PassportStrategyHelper';
|
||||
import { AuthProviderConfig, AuthProviderRouteHandlers } from '../types';
|
||||
import { postMessageResponse } from '../OAuthProvider';
|
||||
|
||||
export class SamlAuthProvider implements AuthProviderRouteHandlers {
|
||||
private readonly strategy: SamlStrategy;
|
||||
|
||||
constructor(providerConfig: AuthProviderConfig) {
|
||||
this.strategy = new SamlStrategy(
|
||||
{ ...providerConfig.options },
|
||||
(profile: any, done: any) => {
|
||||
// TODO: There's plenty more validation and profile handling to do here,
|
||||
// this provider is currently only intended to validate the provider pattern
|
||||
// for non-oauth auth flows.
|
||||
// TODO: This flow doesn't issue an identity token that can be used to validate
|
||||
// the identity of the user in other backends, which we need in some form.
|
||||
done(undefined, {
|
||||
email: profile.email,
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
displayName: profile.displayName,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async start(req: express.Request, res: express.Response): Promise<any> {
|
||||
const { url } = await executeRedirectStrategy(req, this.strategy, {});
|
||||
res.redirect(url);
|
||||
}
|
||||
|
||||
async frameHandler(
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
): Promise<any> {
|
||||
try {
|
||||
const { user } = await executeFrameHandlerStrategy(req, this.strategy);
|
||||
|
||||
return postMessageResponse(res, {
|
||||
type: 'auth-result',
|
||||
payload: user,
|
||||
});
|
||||
} catch (error) {
|
||||
return postMessageResponse(res, {
|
||||
type: 'auth-result',
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async logout(_req: express.Request, res: express.Response): Promise<any> {
|
||||
res.send('noop');
|
||||
}
|
||||
}
|
||||
|
||||
export function createSamlProvider(config: AuthProviderConfig) {
|
||||
return new SamlAuthProvider(config);
|
||||
}
|
||||
@@ -37,13 +37,9 @@ export interface AuthProviderRouteHandlers {
|
||||
logout(req: express.Request, res: express.Response): Promise<any>;
|
||||
}
|
||||
|
||||
export type AuthProviderFactories = {
|
||||
[key: string]: AuthProviderFactory;
|
||||
};
|
||||
|
||||
export type AuthProviderFactory = {
|
||||
new (providerConfig: any): OAuthProviderHandlers;
|
||||
};
|
||||
export type AuthProviderFactory = (
|
||||
config: AuthProviderConfig,
|
||||
) => AuthProviderRouteHandlers;
|
||||
|
||||
export type AuthInfoBase = {
|
||||
accessToken: string;
|
||||
|
||||
@@ -17,9 +17,10 @@
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import bodyParser from 'body-parser';
|
||||
import { Logger } from 'winston';
|
||||
import { providers } from './../providers/config';
|
||||
import { makeProvider } from '../providers';
|
||||
import { createAuthProviderRouter } from '../providers';
|
||||
|
||||
export interface RouterOptions {
|
||||
logger: Logger;
|
||||
@@ -32,12 +33,15 @@ export async function createRouter(
|
||||
const logger = options.logger.child({ plugin: 'auth' });
|
||||
|
||||
router.use(cookieParser());
|
||||
router.use(bodyParser.urlencoded({ extended: false }));
|
||||
router.use(bodyParser.json());
|
||||
|
||||
// configure all the providers
|
||||
for (const providerConfig of providers) {
|
||||
const { providerId, providerRouter } = makeProvider(providerConfig);
|
||||
logger.info(`Configuring provider, ${providerId}`);
|
||||
router.use(`/${providerId}`, providerRouter);
|
||||
const { provider } = providerConfig;
|
||||
const providerRouter = createAuthProviderRouter(providerConfig);
|
||||
logger.info(`Configuring provider, ${provider}`);
|
||||
router.use(`/${provider}`, providerRouter);
|
||||
}
|
||||
|
||||
return router;
|
||||
|
||||
Reference in New Issue
Block a user