diff --git a/index.ts b/index.ts
index 7cdb5393..0e0e6107 100644
--- a/index.ts
+++ b/index.ts
@@ -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,
diff --git a/src/binding-post.ts b/src/binding-post.ts
index 06d333aa..193f62cc 100644
--- a/src/binding-post.ts
+++ b/src/binding-post.ts
@@ -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 = '';
@@ -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);
}
diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts
index 4ff402d9..0d8ae7e6 100644
--- a/src/binding-redirect.ts
+++ b/src/binding-redirect.ts
@@ -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;
@@ -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;
@@ -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 {
diff --git a/src/binding-simplesign.ts b/src/binding-simplesign.ts
index e40cc0d1..3bd1b371 100644
--- a/src/binding-simplesign.ts
+++ b/src/binding-simplesign.ts
@@ -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;
@@ -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 = '';
@@ -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);
}
diff --git a/src/entity-sp.ts b/src/entity-sp.ts
index 454d5664..64ffbfad 100644
--- a/src/entity-sp.ts
+++ b/src/entity-sp.ts
@@ -13,6 +13,7 @@ import {
IdentityProviderConstructor as IdentityProvider,
ServiceProviderMetadata,
ServiceProviderSettings,
+ LoginRequestOptions,
} from './types';
import { namespace } from './urn';
import redirectBinding from './binding-redirect';
@@ -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];
@@ -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,
diff --git a/src/extractor.ts b/src/extractor.ts
index 2cbf8c24..740f8c5c 100644
--- a/src/extractor.ts
+++ b/src/extractor.ts
@@ -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',
diff --git a/src/libsaml.ts b/src/libsaml.ts
index bdb48356..8eb3bba4 100644
--- a/src/libsaml.ts
+++ b/src/libsaml.ts
@@ -143,7 +143,7 @@ const libSaml = () => {
* @type {LoginRequestTemplate}
*/
const defaultLoginRequestTemplate = {
- context: '{Issuer}',
+ context: '{Issuer}',
};
/**
* @desc Default logout request template
diff --git a/src/types.ts b/src/types.ts
index af8d3414..ca048bdc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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;
diff --git a/src/urn.ts b/src/urn.ts
index df71255a..f22b2465 100644
--- a/src/urn.ts
+++ b/src/urn.ts
@@ -104,6 +104,7 @@ const namespace = {
const tags = {
request: {
AllowCreate: '{AllowCreate}',
+ ForceAuthn: '{ForceAuthn}',
AssertionConsumerServiceURL: '{AssertionConsumerServiceURL}',
AuthnContextClassRef: '{AuthnContextClassRef}',
AssertionID: '{AssertionID}',
diff --git a/test/flow.ts b/test/flow.ts
index 3764fa97..5336a648 100644
--- a/test/flow.ts
+++ b/test/flow.ts
@@ -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 {