Encode env in OAuth flow into the state parameter. refs #1775

`Env` passed as a separate parameter in the request might break the flow
of the authorization flow. Spotify considers the `env` as a first class
abstraction and use it across their application stack.
To ensure both the existing flow and newer flows are supported, use the
`state` parameter to encode an object, consisting of the `nonce` and the
`env` which is then passed to the authorization server. This parameter
is returned to the application in the callbackURL
This commit is contained in:
Govindarajan Nagarajan
2020-08-03 15:33:32 +02:00
parent d15cc81c75
commit 685f76d913
6 changed files with 83 additions and 5 deletions
@@ -25,13 +25,28 @@ export class EnvironmentHandler implements AuthProviderRouteHandlers {
constructor(
private readonly providerId: string,
private readonly providers: EnvironmentHandlers,
private readonly envIdentifier: (req: express.Request) => string,
) {}
/* Return the value of `env` key, if it is exists, encoded within
the `state` parameter in the request
*/
private getEnv(stateParams: Array<string>): string {
const envParams = stateParams.filter(
param => param.split('=')[0] === 'env',
);
if (envParams.length > 0) {
return envParams[0].split('=')[1];
}
return '';
}
private getProviderForEnv(
req: express.Request,
res: express.Response,
): AuthProviderRouteHandlers | undefined {
const env = req.query.env?.toString();
const env: string = this.envIdentifier(req);
if (env && this.providers.hasOwnProperty(env)) {
return this.providers[env];
+42 -1
View File
@@ -39,6 +39,21 @@ export type Options = {
tokenIssuer: TokenIssuer;
};
/* Return the value of `env` key, if it is exists, encoded within
the `state` parameter in the request
*/
const getEnv = (stateParams: Array<string>): string => {
const envParams = stateParams.filter(param => param.split('=')[0] === 'env');
if (envParams.length > 0) {
return envParams[0].split('=')[1];
}
return '';
};
const readState = (stateString: string): Array<string> => {
return decodeURIComponent(stateString).split('&');
};
export const verifyNonce = (req: express.Request, providerId: string) => {
const cookieNonce = req.cookies[`${providerId}-nonce`];
const stateNonce = req.query.state;
@@ -103,6 +118,7 @@ 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 (this.options.persistScopes) {
this.setScopesCookie(res, scope);
@@ -112,9 +128,21 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
// set a nonce cookie before redirecting to oauth provider
this.setNonceCookie(res, nonce);
// const stateObject: {nonce: string,
// env: string}
const stateObject = { nonce: nonce, env: env };
const state = Object.keys(stateObject)
.map(
key =>
`${encodeURIComponent(key)}=${encodeURIComponent(stateObject[key])}`,
)
.join('&');
const queryParameters = {
scope,
state: nonce,
state: state,
};
const { url, status } = await this.providerHandlers.start(
@@ -224,6 +252,19 @@ export class OAuthProvider implements AuthProviderRouteHandlers {
}
}
identifyEnv(req: express.Request): string {
const reqEnv = req.query.env?.toString();
if (reqEnv) {
return reqEnv;
}
const stateParam = req.query.state?.toString();
if (!stateParam) {
return '';
}
const env = getEnv(readState(stateParam));
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.
@@ -15,6 +15,7 @@
*/
import Router from 'express-promise-router';
import express from 'express';
import { Logger } from 'winston';
import { TokenIssuer } from '../identity';
import { createGithubProvider } from './github';
@@ -54,16 +55,24 @@ export const createAuthProviderRouter = (
const router = Router();
const envs = providerConfig.keys();
const envProviders: EnvironmentHandlers = {};
let envIdentifier: (req: express.Request) => string;
for (const env of envs) {
const envConfig = providerConfig.getConfig(env);
const provider = factory(globalConfig, env, envConfig, logger, issuer);
if (provider) {
envProviders[env] = provider;
if (envIdentifier === undefined) {
envIdentifier = provider.identifyEnv;
}
}
}
const handler = new EnvironmentHandler(providerId, envProviders);
const handler = new EnvironmentHandler(
providerId,
envProviders,
envIdentifier,
);
router.get('/start', handler.start.bind(handler));
router.get('/handler/frame', handler.frameHandler.bind(handler));
@@ -143,7 +143,7 @@ export class OAuth2AuthProvider implements OAuthProviderHandlers {
export function createOAuth2Provider(
{ baseUrl }: AuthProviderConfig,
env: string,
_: string,
envConfig: Config,
logger: Logger,
tokenIssuer: TokenIssuer,
@@ -153,7 +153,7 @@ export function createOAuth2Provider(
const appOrigin = envConfig.getString('appOrigin');
const clientID = envConfig.getString('clientId');
const clientSecret = envConfig.getString('clientSecret');
const callbackURL = `${baseUrl}/${providerId}/handler/frame?env=${env}`;
const callbackURL = `${baseUrl}/${providerId}/handler/frame`;
const authorizationURL = envConfig.getString('authorizationURL');
const tokenURL = envConfig.getString('tokenURL');
@@ -106,6 +106,10 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers {
async logout(_req: express.Request, res: express.Response): Promise<void> {
res.send('noop');
}
identifyEnv(_req: express.Request): string {
return '';
}
}
type SAMLProviderOptions = {
@@ -203,6 +203,15 @@ 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;
}
export type AuthProviderFactory = (