Skip to content
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
3 changes: 3 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import * as Extractor from './src/extractor';
// exposed methods for customizing samlify
import { setSchemaValidator, setDOMParserOptions } from './src/api';

// export types
export type { LoginRequestOptions } from './src/types';

export {
Constants,
Extractor,
Expand Down
20 changes: 16 additions & 4 deletions src/binding-post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,27 @@ import { wording, namespace, StatusCode } from './urn';
import { BindingContext } from './entity';
import libsaml from './libsaml';
import utility, { get } from './utility';
import { LoginRequestOptions } from './types';

const binding = wording.binding;

/**
* @desc Generate a base64 encoded login request
* @param {string} referenceTagXPath reference uri
* @param {object} entity object includes both idp and sp
* @param {function} customTagReplacement used when developers have their own login response template
* @param {string} referenceTagXPath reference uri
* @param {object} entity object includes both idp and sp
* @param {function | LoginRequestOptions} options options for this specific request
*/
function base64LoginRequest(referenceTagXPath: string, entity: any, customTagReplacement?: (template: string) => BindingContext): BindingContext {
function base64LoginRequest(referenceTagXPath: string, entity: any, options?: ((template: string) => BindingContext) | LoginRequestOptions): BindingContext {
let customTagReplacement: ((template: string) => BindingContext) | undefined;
let requestOptions: LoginRequestOptions | undefined;

if (typeof options === 'function') {
customTagReplacement = options;
} else if (options) {
requestOptions = options;
customTagReplacement = options.customTagReplacement;
}

const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
const spSetting = entity.sp.entitySetting;
let id: string = '';
Expand All @@ -41,6 +52,7 @@ function base64LoginRequest(referenceTagXPath: string, entity: any, customTagRep
AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.post),
EntityID: metadata.sp.getEntityID(),
AllowCreate: spSetting.allowCreate,
ForceAuthn: requestOptions?.forceAuthn ?? false,
NameIDFormat: selectedNameIDFormat
} as any);
}
Expand Down
17 changes: 14 additions & 3 deletions src/binding-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { IdentityProvider as Idp } from './entity-idp';
import { ServiceProvider as Sp } from './entity-sp';
import * as url from 'url';
import { wording, namespace } from './urn';
import { LoginRequestOptions } from './types';

const binding = wording.binding;
const urlParams = wording.urlParams;
Expand Down Expand Up @@ -79,11 +80,20 @@ function buildRedirectURL(opts: BuildRedirectConfig) {
}
/**
* @desc Redirect URL for login request
* @param {object} entity object includes both idp and sp
* @param {function} customTagReplacement used when developers have their own login response template
* @param {object} entity object includes both idp and sp
* @param {function | LoginRequestOptions} options options for this specific request
* @return {string} redirect URL
*/
function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacement?: (template: string) => BindingContext): BindingContext {
function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, options?: ((template: string) => BindingContext) | LoginRequestOptions): BindingContext {
let customTagReplacement: ((template: string) => BindingContext) | undefined;
let requestOptions: LoginRequestOptions | undefined;

if (typeof options === 'function') {
customTagReplacement = options;
} else if (options) {
requestOptions = options;
customTagReplacement = options.customTagReplacement;
}

const metadata: any = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
const spSetting: any = entity.sp.entitySetting;
Expand All @@ -109,6 +119,7 @@ function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacem
AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.post),
EntityID: metadata.sp.getEntityID(),
AllowCreate: spSetting.allowCreate,
ForceAuthn: requestOptions?.forceAuthn ?? false,
} as any);
}
return {
Expand Down
19 changes: 15 additions & 4 deletions src/binding-simplesign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { wording, StatusCode } from './urn';
import { BindingContext, SimpleSignComputedContext } from './entity';
import libsaml from './libsaml';
import utility, { get } from './utility';
import { LoginRequestOptions } from './types';

const binding = wording.binding;
const urlParams = wording.urlParams;
Expand Down Expand Up @@ -71,11 +72,20 @@ function buildSimpleSignature(opts: BuildSimpleSignConfig) : string {

/**
* @desc Generate a base64 encoded login request
* @param {string} referenceTagXPath reference uri
* @param {object} entity object includes both idp and sp
* @param {function} customTagReplacement used when developers have their own login response template
* @param {object} entity object includes both idp and sp
* @param {function | LoginRequestOptions} options options for this specific request
*/
function base64LoginRequest(entity: any, customTagReplacement?: (template: string) => BindingContext): SimpleSignComputedContext {
function base64LoginRequest(entity: any, options?: ((template: string) => BindingContext) | LoginRequestOptions): SimpleSignComputedContext {
let customTagReplacement: ((template: string) => BindingContext) | undefined;
let requestOptions: LoginRequestOptions | undefined;

if (typeof options === 'function') {
customTagReplacement = options;
} else if (options) {
requestOptions = options;
customTagReplacement = options.customTagReplacement;
}

const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
const spSetting = entity.sp.entitySetting;
let id: string = '';
Expand All @@ -99,6 +109,7 @@ function base64LoginRequest(entity: any, customTagReplacement?: (template: strin
AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.simpleSign),
EntityID: metadata.sp.getEntityID(),
AllowCreate: spSetting.allowCreate,
ForceAuthn: requestOptions?.forceAuthn ?? false,
NameIDFormat: selectedNameIDFormat
} as any);
}
Expand Down
17 changes: 9 additions & 8 deletions src/entity-sp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
IdentityProviderConstructor as IdentityProvider,
ServiceProviderMetadata,
ServiceProviderSettings,
LoginRequestOptions,
} from './types';
import { namespace } from './urn';
import redirectBinding from './binding-redirect';
Expand Down Expand Up @@ -50,14 +51,14 @@ export class ServiceProvider extends Entity {

/**
* @desc Generates the login request for developers to design their own method
* @param {IdentityProvider} idp object of identity provider
* @param {string} binding protocol binding
* @param {function} customTagReplacement used when developers have their own login response template
* @param {IdentityProvider} idp object of identity provider
* @param {string} binding protocol binding
* @param {function | LoginRequestOptions} options options for this specific request
*/
public createLoginRequest(
idp: IdentityProvider,
binding = 'redirect',
customTagReplacement?: (template: string) => BindingContext,
options?: ((template: string) => BindingContext) | LoginRequestOptions,
): BindingContext | PostBindingContext| SimpleSignBindingContext {
const nsBinding = namespace.binding;
const protocol = nsBinding[binding];
Expand All @@ -68,21 +69,21 @@ export class ServiceProvider extends Entity {
let context: any = null;
switch (protocol) {
case nsBinding.redirect:
return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement);
return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, options);

case nsBinding.post:
context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement);
context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, options);
break;

case nsBinding.simpleSign:
// Object context = {id, context, signature, sigAlg}
context = simpleSignBinding.base64LoginRequest( { idp, sp: this }, customTagReplacement);
context = simpleSignBinding.base64LoginRequest({ idp, sp: this }, options);
break;

default:
// Will support artifact in the next release
throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
}
}

return {
...context,
Expand Down
2 changes: 1 addition & 1 deletion src/extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const loginRequestFields: ExtractorFields = [
{
key: 'request',
localPath: ['AuthnRequest'],
attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL']
attributes: ['ID', 'IssueInstant', 'Destination', 'AssertionConsumerServiceURL', 'ForceAuthn']
},
{
key: 'issuer',
Expand Down
2 changes: 1 addition & 1 deletion src/libsaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ const libSaml = () => {
* @type {LoginRequestTemplate}
*/
const defaultLoginRequestTemplate = {
context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
context: '<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="{ID}" Version="2.0" IssueInstant="{IssueInstant}" Destination="{Destination}" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="{AssertionConsumerServiceURL}" ForceAuthn="{ForceAuthn}"><saml:Issuer>{Issuer}</saml:Issuer><samlp:NameIDPolicy Format="{NameIDFormat}" AllowCreate="{AllowCreate}"/></samlp:AuthnRequest>',
};
/**
* @desc Default logout request template
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ export type ServiceProviderSettings = {
clockDrifts?: [number, number];
};

export type LoginRequestOptions = {
/** Force re-authentication at the IdP */
forceAuthn?: boolean;
/** Custom tag replacement for login request template */
customTagReplacement?: (template: string) => any;
};

export type IdentityProviderSettings = {
metadata?: string | Buffer;

Expand Down
1 change: 1 addition & 0 deletions src/urn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const namespace = {
const tags = {
request: {
AllowCreate: '{AllowCreate}',
ForceAuthn: '{ForceAuthn}',
AssertionConsumerServiceURL: '{AssertionConsumerServiceURL}',
AuthnContextClassRef: '{AuthnContextClassRef}',
AssertionID: '{AssertionID}',
Expand Down
44 changes: 44 additions & 0 deletions test/flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,50 @@ test('create login request with post binding using default template and parse it
expect(typeof extract.signature).toBe('string');
});

test('create login request with redirect binding and per-request forceAuthn=true', async () => {
const { id, context } = sp.createLoginRequest(idp, 'redirect', { forceAuthn: true });
expect(typeof id).toBe('string');
expect(typeof context).toBe('string');
const originalURL = url.parse(context, true);
const SAMLRequest = originalURL.query.SAMLRequest;
const Signature = originalURL.query.Signature;
const SigAlg = originalURL.query.SigAlg;
delete originalURL.query.Signature;
const octetString = Object.keys(originalURL.query).map(q => q + '=' + encodeURIComponent(originalURL.query[q] as string)).join('&');
const { extract } = await idp.parseLoginRequest(sp, 'redirect', { query: { SAMLRequest, Signature, SigAlg }, octetString});
expect(extract.request.forceAuthn).toBe('true');
});

test('create login request with post binding and per-request forceAuthn=true', async () => {
const { id, context: SAMLRequest } = sp.createLoginRequest(idp, 'post', { forceAuthn: true }) as PostBindingContext;
expect(typeof id).toBe('string');
expect(typeof SAMLRequest).toBe('string');
const { extract } = await idp.parseLoginRequest(sp, 'post', { body: { SAMLRequest }});
expect(extract.request.forceAuthn).toBe('true');
});

test('create login request with simpleSign binding and per-request forceAuthn=true', async () => {
const { id, context: SAMLRequest, type, sigAlg, signature, relayState } = sp.createLoginRequest(idp, 'simpleSign', { forceAuthn: true }) as SimpleSignBindingContext;
expect(typeof id).toBe('string');
expect(typeof SAMLRequest).toBe('string');
const octetString = buildSimpleSignOctetString(type, SAMLRequest, sigAlg, relayState, signature);
const { extract } = await idp.parseLoginRequest(sp, 'simpleSign', { body: { SAMLRequest, Signature: signature, SigAlg: sigAlg }, octetString});
expect(extract.request.forceAuthn).toBe('true');
});

test('create login request with default forceAuthn=false', async () => {
const { id, context } = sp.createLoginRequest(idp, 'redirect');
const originalURL = url.parse(context, true);
const SAMLRequest = originalURL.query.SAMLRequest;
const Signature = originalURL.query.Signature;
const SigAlg = originalURL.query.SigAlg;
delete originalURL.query.Signature;
const octetString = Object.keys(originalURL.query).map(q => q + '=' + encodeURIComponent(originalURL.query[q] as string)).join('&');
const { extract } = await idp.parseLoginRequest(sp, 'redirect', { query: { SAMLRequest, Signature, SigAlg }, octetString});
expect(extract.request.forceAuthn).toBe('false');
});


test('signed in sp is not matched with the signed notation in idp with post request', () => {
const _idp = identityProvider({ ...defaultIdpConfig, metadata: noSignedIdpMetadata });
try {
Expand Down