Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SAML: allowing idpOptions sso_urls to be parameterized #344

Open
wants to merge 4 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/auth/src/common/generic-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { GrantManager } from './grant-manager';
const ERROR_TIMEOUT = 500; // ms
const EXTERNAL_URL_INTERVAL = 500;
const EXTERNAL_URL_RETRIES = 10;
const ADDITIONAL_AUTHORIZE_OPTION_VALUES_REGEX = /^([a-zA-Z0-9][a-zA-Z0-9-_.]+)$/;

export class GenericOAuth2Router {

Expand Down Expand Up @@ -452,6 +453,14 @@ export class GenericOAuth2Router {
// Support prefilled username
authRequest.prefill_username = givenPrefillUsername;

authRequest.options = {};
Object.keys(req.query).forEach(key => {
let value = req.query[key];
if (key.startsWith("x_") && ADDITIONAL_AUTHORIZE_OPTION_VALUES_REGEX.test(value)) {
authRequest.options[key.substring(2)] = value;
}
})

// Validate parameters first now (TODO: This is pbly feasible centrally,
// it will be the same for all Auth Methods).
let subscriptionInfo: WickedSubscriptionInfo;
Expand Down
4 changes: 3 additions & 1 deletion src/auth/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ export interface AuthRequest extends OAuth2Request {
prefill_username?: string,
validNamespaces?: string[],
// Used in the SAML case
requestId?: string
requestId?: string,
// additional parameters passed to the authorization request
options?: any
}

export interface AuthRequestCallback {
Expand Down
39 changes: 27 additions & 12 deletions src/auth/src/providers/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export class SamlIdP implements IdentityProvider {
private authMethodConfig: SamlIdpConfig;

private serviceProvider: any;
private identityProvider: any;
private identityProviders: any;

constructor(basePath: string, authMethodId: string, authMethodConfig: any, options: IdpOptions) {
debug(`constructor(${basePath}, ${authMethodId},...)`);
Expand Down Expand Up @@ -54,7 +54,7 @@ export class SamlIdP implements IdentityProvider {
this.authMethodConfig.spOptions.entity_id = entityUrl;

this.serviceProvider = new saml2.ServiceProvider(authMethodConfig.spOptions);
this.identityProvider = new saml2.IdentityProvider(authMethodConfig.idpOptions);
this.identityProviders = {};

this.genericFlow.initIdP(this);
}
Expand All @@ -73,6 +73,20 @@ export class SamlIdP implements IdentityProvider {
return this.genericFlow.getRouter();
}

private getIdentityProvider(req): any {
const authRequest = utils.getAuthRequest(req, this.authMethodId);
const key = (authRequest.options && Object.keys(authRequest.options).length > 0) ? JSON.stringify(authRequest.options) : "global";
if (!this.identityProviders[key]) {
const clonedIdpOptions = Object.assign({}, this.authMethodConfig.idpOptions);
clonedIdpOptions.sso_login_url = mustache.render(clonedIdpOptions.sso_login_url, authRequest);
if (clonedIdpOptions.sso_logout_url) {
clonedIdpOptions.sso_logout_url = mustache.render(clonedIdpOptions.sso_logout_url, authRequest);
}
this.identityProviders[key] = new saml2.IdentityProvider(clonedIdpOptions)
}
return this.identityProviders[key];
}

/**
* In case the user isn't already authenticated, this method will
* be called from the generic flow implementation. It is assumed to
Expand Down Expand Up @@ -108,7 +122,7 @@ export class SamlIdP implements IdentityProvider {
}
options.is_passive = true;
}
this.serviceProvider.create_login_request_url(this.identityProvider, options, function (err, loginUrl, requestId) {
this.serviceProvider.create_login_request_url(this.getIdentityProvider(req), options, function (err, loginUrl, requestId) {
if (err)
return failError(500, err, next);
// Remember the request ID
Expand All @@ -134,13 +148,14 @@ export class SamlIdP implements IdentityProvider {
const instance = this;
try {
const authResponse = utils.getAuthResponse(req, instance.authMethodId) as SamlAuthResponse;
const identityProvider = instance.getIdentityProvider(req);
const options: any = {
name_id: authResponse.name_id,
session_index: authResponse.session_index
};

// Check that the identityProvider is correctly configured
if (!instance.identityProvider.sso_logout_url) {
if (!identityProvider.sso_logout_url) {
next(makeError('The SAML configuration does not contain an sso_logout_url.', 500));
return true;
}
Expand All @@ -151,7 +166,7 @@ export class SamlIdP implements IdentityProvider {
if (redirect_uri)
options.relay_state = Buffer.from(redirect_uri).toString('base64');
instance.serviceProvider.create_logout_request_url(
instance.identityProvider,
identityProvider,
options,
function (err, logoutUrl) {
if (err) {
Expand Down Expand Up @@ -273,15 +288,15 @@ export class SamlIdP implements IdentityProvider {
request_body: req.query
};
const relay_state = req.query.RelayState;
instance.serviceProvider.redirect_assert(instance.identityProvider, options, function (err, response) {
instance.serviceProvider.redirect_assert(instance.getIdentityProvider(req), options, function (err, response) {
if (err)
return next(err);
debug(response);
if (response.type === 'logout_request') {
// IdP initiated logout
debug('SAML: logout_request');
const in_response_to = response && response.response_header ? response.response_header.in_response_to : null;
instance.getLogoutResponseUrl(in_response_to, relay_state, function (err, redirectUrl) {
instance.getLogoutResponseUrl(req, in_response_to, relay_state, function (err, redirectUrl) {
if (err)
return next(err);
info(redirectUrl);
Expand Down Expand Up @@ -349,14 +364,14 @@ export class SamlIdP implements IdentityProvider {
});
};

private getLogoutResponseUrl(inResponseTo, relayState, callback) {
private getLogoutResponseUrl(req, inResponseTo, relayState, callback) {
debug('getLogoutResponseUrl');
const instance = this;
if (!instance.identityProvider.sso_logout_url) {
const identityProvider = instance.getIdentityProvider(req);
if (!identityProvider.sso_logout_url) {
return callback(makeError('The SAML configuration (identityProvider) does not contain an sso_logout_url.', 500));
}
this.serviceProvider.create_logout_response_url(
instance.identityProvider,
this.serviceProvider.create_logout_response_url(identityProvider,
{ in_response_to: inResponseTo, relay_state: relayState },
function (err, logoutResponseUrl) {
if (err) {
Expand All @@ -375,7 +390,7 @@ export class SamlIdP implements IdentityProvider {
return callback(new Error('assert needs a requestId to verify the SAML assertion.'));

const options = { request_body: req.body };
this.serviceProvider.post_assert(this.identityProvider, options, function (err, samlResponse) {
this.serviceProvider.post_assert(this.getIdentityProvider(req), options, function (err, samlResponse) {
if (err) {
error('post_assert failed.');
return callback(err);
Expand Down