Merge branch 'master' into adds_auth0

This commit is contained in:
Patrik Oldsberg
2020-08-12 09:05:00 +02:00
committed by GitHub
548 changed files with 31453 additions and 3287 deletions
+18
View File
@@ -27,11 +27,29 @@ export AUTH_GOOGLE_CLIENT_SECRET=x
### Github
#### Creating a GitHub OAuth application
Follow this link, [Create new OAuth App](https://github.com/settings/applications/new).
1. Set Application Name to `backstage-dev` or something along those lines.
1. You can set the Homepage URL to whatever you want to.
1. The Authorization Callback URL should match the redirect URI set in Backstage.
1. Set this to `http://localhost:7000/auth/github` for local development.
1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/auth/github` for non-local deployments.
```bash
export AUTH_GITHUB_CLIENT_ID=x
export AUTH_GITHUB_CLIENT_SECRET=x
```
for github enterprise:
```bash
export AUTH_GITHUB_CLIENT_ID=x
export AUTH_GITHUB_CLIENT_SECRET=x
export AUTH_GITHUB_ENTERPRISE_INSTANCE_URL=https://x
```
### Gitlab
```bash
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-backend",
"version": "0.1.1-alpha.14",
"version": "0.1.1-alpha.18",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -20,9 +20,8 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.1.1-alpha.14",
"@backstage/config": "^0.1.1-alpha.13",
"@backstage/config-loader": "^0.1.1-alpha.13",
"@backstage/backend-common": "^0.1.1-alpha.18",
"@backstage/config": "^0.1.1-alpha.18",
"@types/express": "^4.17.6",
"body-parser": "^1.19.0",
"compression": "^1.7.4",
@@ -31,7 +30,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"helmet": "^3.22.0",
"helmet": "^4.0.0",
"jose": "^1.27.1",
"jwt-decode": "2.2.0",
"knex": "^0.21.1",
@@ -49,7 +48,7 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.1.1-alpha.14",
"@backstage/cli": "^0.1.1-alpha.18",
"@types/body-parser": "^1.19.0",
"@types/cookie-parser": "^1.4.2",
"@types/jwt-decode": "2.2.1",
@@ -15,7 +15,10 @@
*/
import express from 'express';
import { AuthProviderRouteHandlers } from '../providers/types';
import {
AuthProviderRouteHandlers,
EnvironmentIdentifierFn,
} from '../providers/types';
export type EnvironmentHandlers = {
[key: string]: AuthProviderRouteHandlers;
@@ -25,15 +28,16 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers {
constructor(
private readonly providerId: string,
private readonly providers: EnvironmentHandlers,
private readonly envIdentifier: EnvironmentIdentifierFn,
) {}
private getProviderForEnv(
req: express.Request,
res: express.Response,
): AuthProviderRouteHandlers | undefined {
const env = req.query.env?.toString();
const env: string | undefined = this.envIdentifier(req);
if (this.providers.hasOwnProperty(env)) {
if (env && this.providers.hasOwnProperty(env)) {
return this.providers[env];
}
@@ -21,6 +21,7 @@ import {
THOUSAND_DAYS_MS,
TEN_MINUTES_MS,
verifyNonce,
encodeState,
OAuthProvider,
} from './OAuthProvider';
import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types';
@@ -43,10 +44,11 @@ const mockResponseData = {
describe('OAuthProvider Utils', () => {
describe('verifyNonce', () => {
it('should throw error if cookie nonce missing', () => {
const state = { nonce: 'NONCE', env: 'development' };
const mockRequest = ({
cookies: {},
query: {
state: 'NONCE',
state: encodeState(state),
},
} as unknown) as express.Request;
expect(() => {
@@ -63,16 +65,17 @@ describe('OAuthProvider Utils', () => {
} as unknown) as express.Request;
expect(() => {
verifyNonce(mockRequest, 'providera');
}).toThrowError('Auth response is missing state nonce');
}).toThrowError('Invalid state passed via request');
});
it('should throw error if nonce mismatch', () => {
const state = { nonce: 'NONCEB', env: 'development' };
const mockRequest = ({
cookies: {
'providera-nonce': 'NONCEA',
},
query: {
state: 'NONCEB',
state: encodeState(state),
},
} as unknown) as express.Request;
expect(() => {
@@ -81,12 +84,13 @@ describe('OAuthProvider Utils', () => {
});
it('should not throw any error if nonce matches', () => {
const state = { nonce: 'NONCE', env: 'development' };
const mockRequest = ({
cookies: {
'providera-nonce': 'NONCE',
},
query: {
state: 'NONCE',
state: encodeState(state),
},
} as unknown) as express.Request;
expect(() => {
@@ -125,7 +129,7 @@ describe('OAuthProvider Utils', () => {
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toBeCalledTimes(2);
expect(mockResponse.setHeader).toBeCalledTimes(3);
expect(mockResponse.end).toBeCalledTimes(1);
expect(mockResponse.end).toBeCalledWith(
expect.stringContaining(base64Data),
@@ -146,7 +150,7 @@ describe('OAuthProvider Utils', () => {
const base64Data = Buffer.from(jsonData, 'utf8').toString('base64');
postMessageResponse(mockResponse, appOrigin, data);
expect(mockResponse.setHeader).toBeCalledTimes(2);
expect(mockResponse.setHeader).toBeCalledTimes(3);
expect(mockResponse.end).toBeCalledTimes(1);
expect(mockResponse.end).toBeCalledWith(
expect.stringContaining(base64Data),
@@ -217,6 +221,7 @@ describe('OAuthProvider', () => {
const mockRequest = ({
query: {
scope: 'user',
env: 'development',
},
} as unknown) as express.Request;
@@ -249,12 +254,13 @@ describe('OAuthProvider', () => {
disableRefresh: false,
});
const state = { nonce: 'nonce', env: 'development' };
const mockRequest = ({
cookies: {
'test-provider-nonce': 'nonce',
},
query: {
state: 'nonce',
state: encodeState(state),
},
} as unknown) as express.Request;
+62 -13
View File
@@ -22,6 +22,7 @@ import {
OAuthProviderHandlers,
WebMessageResponse,
BackstageIdentity,
OAuthState,
} from '../providers/types';
import { InputError } from '@backstage/backend-common';
import { TokenIssuer } from '../identity';
@@ -39,14 +40,41 @@ export type Options = {
tokenIssuer: TokenIssuer;
};
const readState = (stateString: string): OAuthState => {
const state = Object.fromEntries(
new URLSearchParams(decodeURIComponent(stateString)),
);
if (
!state.nonce ||
!state.env ||
state.nonce?.length === 0 ||
state.env?.length === 0
) {
throw Error(`Invalid state passed via request`);
}
return {
nonce: state.nonce,
env: state.env,
};
};
export const encodeState = (state: OAuthState): string => {
const searchParams = new URLSearchParams();
searchParams.append('nonce', state.nonce);
searchParams.append('env', state.env);
return encodeURIComponent(searchParams.toString());
};
export const verifyNonce = (req: express.Request, providerId: string) => {
const cookieNonce = req.cookies[`${providerId}-nonce`];
const stateNonce = req.query.state;
const state: OAuthState = readState(req.query.state?.toString() ?? '');
const stateNonce = state.nonce;
if (!cookieNonce) {
throw new Error('Auth response is missing cookie nonce');
}
if (!stateNonce) {
if (stateNonce.length === 0) {
throw new Error('Auth response is missing state nonce');
}
if (cookieNonce !== stateNonce) {
@@ -66,13 +94,17 @@ export const postMessageResponse = (
res.setHeader('X-Frame-Options', 'sameorigin');
// TODO: Make target app origin configurable globally
const script = `
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}')
window.close()
`;
const hash = crypto.createHash('sha256').update(script).digest('base64');
res.setHeader('Content-Security-Policy', `script-src 'sha256-${hash}'`);
res.end(`
<html>
<body>
<script>
(window.opener || window.parent).postMessage(JSON.parse(atob('${base64Data}')), '${appOrigin}')
window.close()
</script>
<script>${script}</script>
</body>
</html>
`);
@@ -103,9 +135,10 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
async start(req: express.Request, res: express.Response): Promise<void> {
// retrieve scopes from request
const scope = req.query.scope?.toString() ?? '';
const env = req.query.env?.toString();
if (!scope) {
throw new InputError('missing scope parameter');
if (!env) {
throw new InputError('No env provided in request query parameters');
}
if (this.options.persistScopes) {
@@ -116,9 +149,12 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
// set a nonce cookie before redirecting to oauth provider
this.setNonceCookie(res, nonce);
const stateObject = { nonce: nonce, env: env };
const stateParameter = encodeState(stateObject);
const queryParameters = {
scope,
state: nonce,
state: stateParameter,
};
const { url, status } = await this.providerHandlers.start(
@@ -228,6 +264,19 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
}
}
identifyEnv(req: express.Request): string | undefined {
const reqEnv = req.query.env?.toString();
if (reqEnv) {
return reqEnv;
}
const stateParams = req.query.state?.toString();
if (!stateParams) {
return undefined;
}
const env = readState(stateParams).env;
return env;
}
/**
* If the response from the OAuth provider includes a Backstage identity, we
* make sure it's populated with all the information we can derive from the user ID.
@@ -248,7 +297,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
res.cookie(`${this.options.providerId}-nonce`, nonce, {
maxAge: TEN_MINUTES_MS,
secure: this.options.secure,
sameSite: 'none',
sameSite: 'lax',
domain: this.domain,
path: `${this.basePath}/${this.options.providerId}/handler`,
httpOnly: true,
@@ -259,7 +308,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
res.cookie(`${this.options.providerId}-scope`, scope, {
maxAge: TEN_MINUTES_MS,
secure: this.options.secure,
sameSite: 'none',
sameSite: 'lax',
domain: this.domain,
path: `${this.basePath}/${this.options.providerId}/handler`,
httpOnly: true,
@@ -277,7 +326,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
res.cookie(`${this.options.providerId}-refresh-token`, refreshToken, {
maxAge: THOUSAND_DAYS_MS,
secure: this.options.secure,
sameSite: 'none',
sameSite: 'lax',
domain: this.domain,
path: `${this.basePath}/${this.options.providerId}`,
httpOnly: true,
@@ -288,7 +337,7 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
res.cookie(`${this.options.providerId}-refresh-token`, '', {
maxAge: 0,
secure: false,
sameSite: 'none',
sameSite: 'lax',
domain: `${this.domain}`,
path: `${this.basePath}/${this.options.providerId}`,
httpOnly: true,
+41 -11
View File
@@ -24,7 +24,16 @@ import { createOAuth2Provider } from './oauth2';
import { createOktaProvider } from './okta';
import { createSamlProvider } from './saml';
import { createAuth0Provider } from './auth0'
import { AuthProviderConfig, AuthProviderFactory } from './types';
import {
AuthProviderConfig,
AuthProviderFactory,
EnvironmentIdentifierFn,
} from './types';
import { Config } from '@backstage/config';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../lib/EnvironmentHandler';
const factories: { [providerId: string]: AuthProviderFactory } = {
google: createGoogleProvider,
@@ -39,7 +48,7 @@ const factories: { [providerId: string]: AuthProviderFactory } = {
export const createAuthProviderRouter = (
providerId: string,
globalConfig: AuthProviderConfig,
providerConfig: any, // TODO: make this a config reader object of sorts
providerConfig: Config,
logger: Logger,
issuer: TokenIssuer,
) => {
@@ -48,17 +57,38 @@ export const createAuthProviderRouter = (
throw Error(`No auth provider available for '${providerId}'`);
}
const provider = factory(globalConfig, providerConfig, logger, issuer);
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));
if (provider.logout) {
router.post('/logout', provider.logout.bind(provider));
const envs = providerConfig.keys();
const envProviders: EnvironmentHandlers = {};
let envIdentifier: EnvironmentIdentifierFn | undefined;
for (const env of envs) {
const envConfig = providerConfig.getConfig(env);
const provider = factory(globalConfig, env, envConfig, logger, issuer);
if (provider) {
envProviders[env] = provider;
envIdentifier = provider.identifyEnv;
}
}
if (provider.refresh) {
router.get('/refresh', provider.refresh.bind(provider));
if (typeof envIdentifier === 'undefined') {
throw Error(`No envIdentifier provided for '${providerId}'`);
}
const handler = new EnvironmentHandler(
providerId,
envProviders,
envIdentifier,
);
router.get('/start', handler.start.bind(handler));
router.get('/handler/frame', handler.frameHandler.bind(handler));
router.post('/handler/frame', handler.frameHandler.bind(handler));
if (handler.logout) {
router.post('/logout', handler.logout.bind(handler));
}
if (handler.refresh) {
router.get('/refresh', handler.refresh.bind(handler));
}
return router;
@@ -25,20 +25,15 @@ import {
OAuthProviderHandlers,
AuthProviderConfig,
RedirectInfo,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import passport from 'passport';
import { Config } from '@backstage/config';
export class GithubAuthProvider implements OAuthProviderHandlers {
private readonly _strategy: GithubStrategy;
@@ -131,44 +126,58 @@ export class GithubAuthProvider implements OAuthProviderHandlers {
export function createGithubProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'github';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const enterpriseInstanceUrl = envConfig.getOptionalString(
'enterpriseInstanceUrl',
);
const authorizationURL = enterpriseInstanceUrl
? `${enterpriseInstanceUrl}/login/oauth/authorize`
: undefined;
const tokenURL = enterpriseInstanceUrl
? `${enterpriseInstanceUrl}/login/oauth/access_token`
: undefined;
const userProfileURL = enterpriseInstanceUrl
? `${enterpriseInstanceUrl}/api/v3/user`
: undefined;
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
};
const opts = {
clientID,
clientSecret,
authorizationURL,
tokenURL,
userProfileURL,
callbackURL,
};
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
);
}
logger.warn(
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Github auth provider, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new GithubAuthProvider(opts), {
disableRefresh: true,
persistScopes: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Github auth provider disabled, set AUTH_GITHUB_CLIENT_ID and AUTH_GITHUB_CLIENT_SECRET env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new GithubAuthProvider(opts), {
disableRefresh: true,
persistScopes: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -25,20 +25,15 @@ import {
OAuthProviderHandlers,
AuthProviderConfig,
RedirectInfo,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import passport from 'passport';
import { Config } from '@backstage/config';
export class GitlabAuthProvider implements OAuthProviderHandlers {
private readonly _strategy: GitlabStrategy;
@@ -138,49 +133,45 @@ export class GitlabAuthProvider implements OAuthProviderHandlers {
export function createGitlabProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'gitlab';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const audience = envConfig.getString('audience');
const baseURL = audience || 'https://gitlab.com';
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const {
secure,
appOrigin,
clientId,
clientSecret,
audience,
} = (envConfig as unknown) as OAuthProviderConfig;
const opts = {
clientID: clientId,
clientSecret: clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
baseURL: audience,
};
const opts = {
clientID,
clientSecret,
callbackURL,
baseURL,
};
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
);
}
logger.warn(
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Gitlab auth provider, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new GitlabAuthProvider(opts), {
disableRefresh: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Gitlab auth provider disabled, set AUTH_GITLAB_CLIENT_ID and AUTH_GITLAB_CLIENT_SECRET env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new GitlabAuthProvider(opts), {
disableRefresh: true,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -27,20 +27,15 @@ import {
OAuthProviderHandlers,
RedirectInfo,
AuthProviderConfig,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import { OAuthProvider } from '../../lib/OAuthProvider';
import passport from 'passport';
import {
EnvironmentHandler,
EnvironmentHandlers,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
type PrivateInfo = {
refreshToken: string;
@@ -150,43 +145,42 @@ export class GoogleAuthProvider implements OAuthProviderHandlers {
export function createGoogleProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'google';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
};
const opts = {
clientID,
clientSecret,
callbackURL,
};
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
);
}
logger.warn(
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
if (!opts.clientID || !opts.clientSecret) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Google auth provider, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new GoogleAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Google auth provider disabled, set AUTH_GOOGLE_CLIENT_ID and AUTH_GOOGLE_CLIENT_SECRET env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new GoogleAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -19,10 +19,6 @@ import passport from 'passport';
import { Strategy as OAuth2Strategy } from 'passport-oauth2';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import {
EnvironmentHandler,
EnvironmentHandlers,
} from '../../lib/EnvironmentHandler';
import { OAuthProvider } from '../../lib/OAuthProvider';
import {
executeFetchUserProfileStrategy,
@@ -33,14 +29,13 @@ import {
} from '../../lib/PassportStrategyHelper';
import {
AuthProviderConfig,
EnvironmentProviderConfig,
GenericOAuth2ProviderConfig,
GenericOAuth2ProviderOptions,
OAuthProviderHandlers,
OAuthResponse,
PassportDoneCallback,
RedirectInfo,
} from '../types';
import { Config } from '@backstage/config';
type PrivateInfo = {
refreshToken: string;
@@ -148,51 +143,51 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
export function createOAuth2Provider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'oauth2';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
const authorizationURL = envConfig.getString('authorizationURL');
const tokenURL = envConfig.getString('tokenURL');
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as GenericOAuth2ProviderConfig;
const { secure, appOrigin } = config;
const opts = {
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
authorizationURL: config.authorizationURL,
tokenURL: config.tokenURL,
};
const opts = {
clientID,
clientSecret,
callbackURL,
authorizationURL,
tokenURL,
};
if (
!opts.clientID ||
!opts.clientSecret ||
!opts.authorizationURL ||
!opts.tokenURL
) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
);
}
logger.warn(
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
if (
!opts.clientID ||
!opts.clientSecret ||
!opts.authorizationURL ||
!opts.tokenURL
) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize OAuth2 auth provider, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new OAuth2AuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'OAuth2 auth provider disabled, set AUTH_OAUTH2_CLIENT_ID, AUTH_OAUTH2_CLIENT_SECRET, AUTH_OAUTH2_AUTH_URL, and AUTH_OAUTH2_TOKEN_URL env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new OAuth2AuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -28,19 +28,14 @@ import {
OAuthProviderHandlers,
RedirectInfo,
AuthProviderConfig,
EnvironmentProviderConfig,
OAuthProviderOptions,
OAuthProviderConfig,
OAuthResponse,
PassportDoneCallback,
} from '../types';
import {
EnvironmentHandler,
EnvironmentHandlers,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { StateStore } from 'passport-oauth2';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
type PrivateInfo = {
refreshToken: string;
@@ -170,45 +165,44 @@ export class OktaAuthProvider implements OAuthProviderHandlers {
export function createOktaProvider(
{ baseUrl }: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const providerId = 'okta';
const envProviders: EnvironmentHandlers = {};
const secure = envConfig.getBoolean('secure');
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const audience = envConfig.getString('audience');
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as OAuthProviderConfig;
const { secure, appOrigin } = config;
const opts = {
audience: config.audience,
clientID: config.clientId,
clientSecret: config.clientSecret,
callbackURL: `${baseUrl}/${providerId}/handler/frame?env=${env}`,
};
const opts = {
audience,
clientID,
clientSecret,
callbackURL,
};
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
);
}
logger.warn(
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
if (!opts.clientID || !opts.clientSecret || !opts.audience) {
if (process.env.NODE_ENV !== 'development') {
throw new Error(
'Failed to initialize Okta auth provider, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars',
);
continue;
}
envProviders[env] = new OAuthProvider(new OktaAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
logger.warn(
'Okta auth provider disabled, set AUTH_OKTA_CLIENT_ID, AUTH_OKTA_CLIENT_SECRET, and AUTH_OKTA_AUDIENCE env vars to enable',
);
return undefined;
}
return new EnvironmentHandler(providerId, envProviders);
return new OAuthProvider(new OktaAuthProvider(opts), {
disableRefresh: false,
providerId,
secure,
baseUrl,
appOrigin,
tokenIssuer,
});
}
@@ -27,18 +27,13 @@ import {
import {
AuthProviderConfig,
AuthProviderRouteHandlers,
EnvironmentProviderConfig,
SAMLProviderConfig,
PassportDoneCallback,
ProfileInfo,
} from '../types';
import { postMessageResponse } from '../../lib/OAuthProvider';
import {
EnvironmentHandlers,
EnvironmentHandler,
} from '../../lib/EnvironmentHandler';
import { Logger } from 'winston';
import { TokenIssuer } from '../../identity';
import { Config } from '@backstage/config';
type SamlInfo = {
userId: string;
@@ -111,6 +106,10 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
async logout(_req: express.Request, res: express.Response): Promise<void> {
res.send('noop');
}
identifyEnv(): string | undefined {
return undefined;
}
}
type SAMLProviderOptions = {
@@ -122,30 +121,25 @@ type SAMLProviderOptions = {
export function createSamlProvider(
_authProviderConfig: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
_env: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
) {
const envProviders: EnvironmentHandlers = {};
const entryPoint = envConfig.getString('entryPoint');
const issuer = envConfig.getString('issuer');
const opts = {
entryPoint,
issuer,
path: '/auth/saml/handler/frame',
tokenIssuer,
};
for (const [env, envConfig] of Object.entries(providerConfig)) {
const config = (envConfig as unknown) as SAMLProviderConfig;
const opts = {
entryPoint: config.entryPoint,
issuer: config.issuer,
path: '/auth/saml/handler/frame',
tokenIssuer,
};
if (!opts.entryPoint || !opts.issuer) {
logger.warn(
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
);
continue;
}
envProviders[env] = new SamlAuthProvider(opts);
if (!opts.entryPoint || !opts.issuer) {
logger.warn(
'SAML auth provider disabled, set entryPoint and entryPoint in saml auth config to enable',
);
return undefined;
}
return new EnvironmentHandler('saml', envProviders);
return new SamlAuthProvider(opts);
}
+26 -2
View File
@@ -17,6 +17,9 @@
import express from 'express';
import { Logger } from 'winston';
import { TokenIssuer } from '../identity';
import { Config } from '@backstage/config';
import { OAuthProvider } from '../lib/OAuthProvider';
import { SamlAuthProvider } from './saml/provider';
export type OAuthProviderOptions = {
/**
@@ -200,14 +203,24 @@ export interface AuthProviderRouteHandlers {
* @param {express.Response} res
*/
logout?(req: express.Request, res: express.Response): Promise<void>;
/**
*(Optional) A method to identify the environment Context of the Request
*
*Request
*- contains the environment context information encoded in the request
* @param {express.Request} req
*/
identifyEnv?(req: express.Request): string | undefined;
}
export type AuthProviderFactory = (
globalConfig: AuthProviderConfig,
providerConfig: EnvironmentProviderConfig,
env: string,
envConfig: Config,
logger: Logger,
issuer: TokenIssuer,
) => AuthProviderRouteHandlers;
) => OAuthProvider | SamlAuthProvider | undefined;
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;
@@ -328,3 +341,14 @@ export type SAMLProviderConfig = {
export type SAMLEnvironmentProviderConfig = {
[key: string]: SAMLProviderConfig;
};
export type OAuthState = {
/* A type for the serialized value in the `state` parameter of the OAuth authorization flow
*/
nonce: string;
env: string;
};
export type EnvironmentIdentifierFn = (
req: express.Request,
) => string | undefined;
+4 -73
View File
@@ -36,8 +36,6 @@ export async function createRouter(
const router = Router();
const logger = options.logger.child({ plugin: 'auth' });
const appUrl = new URL(options.config.getString('app.baseUrl'));
const appOrigin = appUrl.origin;
const backendUrl = options.config.getString('backend.baseUrl');
const authUrl = `${backendUrl}/auth`;
@@ -57,80 +55,13 @@ export async function createRouter(
router.use(bodyParser.urlencoded({ extended: false }));
router.use(bodyParser.json());
const config = {
backend: {
baseUrl: backendUrl,
},
auth: {
providers: {
google: {
development: {
appOrigin,
secure: false,
clientId: process.env.AUTH_GOOGLE_CLIENT_ID!,
clientSecret: process.env.AUTH_GOOGLE_CLIENT_SECRET!,
},
},
github: {
development: {
appOrigin,
secure: false,
clientId: process.env.AUTH_GITHUB_CLIENT_ID!,
clientSecret: process.env.AUTH_GITHUB_CLIENT_SECRET!,
},
},
gitlab: {
development: {
appOrigin,
secure: false,
clientId: process.env.AUTH_GITLAB_CLIENT_ID!,
clientSecret: process.env.AUTH_GITLAB_CLIENT_SECRET!,
audience: process.env.GITLAB_BASE_URL! || 'https://gitlab.com',
},
},
saml: {
development: {
entryPoint: 'http://localhost:7001/',
issuer: 'passport-saml',
},
},
okta: {
development: {
appOrigin,
secure: false,
clientId: process.env.AUTH_OKTA_CLIENT_ID!,
clientSecret: process.env.AUTH_OKTA_CLIENT_SECRET!,
audience: process.env.AUTH_OKTA_AUDIENCE,
},
},
oauth2: {
development: {
appOrigin,
secure: false,
clientId: process.env.AUTH_OAUTH2_CLIENT_ID!,
clientSecret: process.env.AUTH_OAUTH2_CLIENT_SECRET!,
authorizationURL: process.env.AUTH_OAUTH2_AUTH_URL!,
tokenURL: process.env.AUTH_OAUTH2_TOKEN_URL!,
},
},
auth0: {
development: {
appOrigin,
secure: false,
domain: process.env.AUTH_AUTH0_DOMAIN!,
clientId: process.env.AUTH_AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH_AUTH0_CLIENT_SECRET!
},
},
},
},
};
const providersConfig = options.config.getConfig('auth.providers');
const providers = providersConfig.keys();
const providerConfigs = config.auth.providers;
for (const [providerId, providerConfig] of Object.entries(providerConfigs)) {
for (const providerId of providers) {
logger.info(`Configuring provider, ${providerId}`);
try {
const providerConfig = providersConfig.getConfig(providerId);
const providerRouter = createAuthProviderRouter(
providerId,
{ baseUrl: authUrl },
@@ -18,9 +18,12 @@ import Knex from 'knex';
import { Server } from 'http';
import { Logger } from 'winston';
import { ConfigReader } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
import { createRouter } from './router';
import { createServiceBuilder, useHotMemoize } from '@backstage/backend-common';
import {
createServiceBuilder,
useHotMemoize,
loadBackendConfig,
} from '@backstage/backend-common';
export interface ServerOptions {
logger: Logger;
@@ -30,7 +33,7 @@ export async function startStandaloneServer(
options: ServerOptions,
): Promise<Server> {
const logger = options.logger.child({ service: 'auth-backend' });
const config = ConfigReader.fromConfigs(await loadConfig());
const config = ConfigReader.fromConfigs(await loadBackendConfig());
const database = useHotMemoize(module, () => {
const knex = Knex({