diff --git a/docs/idp-configuration.md b/docs/idp-configuration.md
index 5a5891c3..e6ec6dde 100644
--- a/docs/idp-configuration.md
+++ b/docs/idp-configuration.md
@@ -57,7 +57,7 @@ const idp = new IdentityProvider({
- **tagPrefix: {[key: TagPrefixKey]: string}**
Declare the tag of specific xml document node. `TagPrefixKey` currently supports `encryptedAssertion` only. (See more [#220](https://github.com/tngan/samlify/issues/220))
-- **loginResponseTemplate: {context: String, attributes: Attributes}**
+- **loginResponseTemplate: {context: String, attributes: Attributes, additionalTemplates: LoginResponseAdditionalTemplates}**
Customize the login response template, and user can reuse it in the callback function to do runtime interpolation. (See [more](/template))
- **wantLogoutResponseSigned: Boolean**
diff --git a/src/binding-redirect.ts b/src/binding-redirect.ts
index 1ac37ac0..c4adc01b 100644
--- a/src/binding-redirect.ts
+++ b/src/binding-redirect.ts
@@ -114,6 +114,105 @@ function loginRequestRedirectURL(entity: { idp: Idp, sp: Sp }, customTagReplacem
}
throw new Error('ERR_GENERATE_REDIRECT_LOGIN_REQUEST_MISSING_METADATA');
}
+
+/**
+* @desc Redirect URL for login response
+* @param {object} requestInfo corresponding request, used to obtain the id
+* @param {object} entity object includes both idp and sp
+* @param {object} user current logged user (e.g. req.user)
+* @param {String} relayState the relaystate sent by sp corresponding request
+* @param {function} customTagReplacement used when developers have their own login response template
+*/
+function loginResponseRedirectURL(requestInfo: any, entity: any, user: any = {}, relayState?: string, customTagReplacement?: (template: string) => BindingContext): BindingContext {
+ const idpSetting = entity.idp.entitySetting;
+ const spSetting = entity.sp.entitySetting;
+ const metadata = {
+ idp: entity.idp.entityMeta,
+ sp: entity.sp.entityMeta,
+ };
+
+ let id: string = idpSetting.generateID();
+ if (metadata && metadata.idp && metadata.sp) {
+ const base = metadata.sp.getAssertionConsumerService(binding.redirect);
+ let rawSamlResponse: string;
+ //
+ const nameIDFormat = idpSetting.nameIDFormat;
+ const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
+ const nowTime = new Date();
+ // Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds)
+ const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000 );
+ const tvalue: any = {
+ ID: id,
+ AssertionID: idpSetting.generateID(),
+ Destination: base,
+ SubjectRecipient: base,
+ Issuer: metadata.idp.getEntityID(),
+ Audience: metadata.sp.getEntityID(),
+ EntityID: metadata.sp.getEntityID(),
+ IssueInstant: nowTime.toISOString(),
+ AssertionConsumerServiceURL: base,
+ StatusCode: namespace.statusCode.success,
+ // can be customized
+ ConditionsNotBefore: nowTime.toISOString(),
+ ConditionsNotOnOrAfter: fiveMinutesLaterTime.toISOString(),
+ SubjectConfirmationDataNotOnOrAfter: fiveMinutesLaterTime.toISOString(),
+ NameIDFormat: selectedNameIDFormat,
+ NameID: user.email || '',
+ InResponseTo: get(requestInfo, 'extract.request.id', ''),
+ AuthnStatement: '',
+ AttributeStatement: '',
+ };
+
+ if (idpSetting.loginResponseTemplate && customTagReplacement) {
+ const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
+ id = get(template, 'id', null);
+ rawSamlResponse = get(template, 'context', null);
+ } else {
+
+ if (requestInfo !== null) {
+ tvalue.InResponseTo = requestInfo.extract.request.id;
+ }
+ rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
+ }
+
+ const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting;
+ const config = {
+ privateKey,
+ privateKeyPass,
+ signatureAlgorithm,
+ signingCert: metadata.idp.getX509Certificate('signing'),
+ isBase64Output: false,
+ };
+ // step: sign assertion ? -> encrypted ? -> sign message ?
+ if (metadata.sp.isWantAssertionsSigned()) {
+ rawSamlResponse = libsaml.constructSAMLSignature({
+ ...config,
+ rawSamlMessage: rawSamlResponse,
+ transformationAlgorithms: spSetting.transformationAlgorithms,
+ referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
+ signatureConfig: {
+ prefix: 'ds',
+ location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", action: 'after' },
+ },
+ });
+ }
+
+ // Like in post binding, SAML response is always signed
+ return {
+ id,
+ context: buildRedirectURL({
+ baseUrl: base,
+ type: urlParams.samlResponse,
+ isSigned: true,
+ context: rawSamlResponse,
+ entitySetting: idpSetting,
+ relayState,
+ }),
+ };
+ }
+ throw new Error('ERR_GENERATE_REDIRECT_LOGIN_RESPONSE_MISSING_METADATA');
+}
+
/**
* @desc Redirect URL for logout request
* @param {object} user current logged user (e.g. req.user)
@@ -127,7 +226,7 @@ function logoutRequestRedirectURL(user, entity, relayState?: string, customTagRe
let id: string = initSetting.generateID();
const nameIDFormat = initSetting.nameIDFormat;
const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
-
+
if (metadata && metadata.init && metadata.target) {
const base = metadata.target.getSingleLogoutService(binding.redirect);
let rawSamlRequest: string = '';
@@ -213,6 +312,7 @@ function logoutResponseRedirectURL(requestInfo: any, entity: any, relayState?: s
const redirectBinding = {
loginRequestRedirectURL,
+ loginResponseRedirectURL,
logoutRequestRedirectURL,
logoutResponseRedirectURL,
};
diff --git a/src/binding-simplesign.ts b/src/binding-simplesign.ts
new file mode 100644
index 00000000..a5af5535
--- /dev/null
+++ b/src/binding-simplesign.ts
@@ -0,0 +1,225 @@
+/**
+* @file binding-simplesign.ts
+* @author Orange
+* @desc Binding-level API, declare the functions using POST SimpleSign binding
+*/
+
+import { wording, StatusCode } from './urn';
+import { BindingContext, SimpleSignComputedContext } from './entity';
+import libsaml from './libsaml';
+import utility, { get } from './utility';
+
+const binding = wording.binding;
+const urlParams = wording.urlParams;
+
+export interface BuildSimpleSignConfig {
+ type: string;
+ context: string;
+ entitySetting: any;
+ relayState?: string;
+}
+
+export interface BindingSimpleSignContext {
+ id: string;
+ context: string;
+ signature: any;
+ sigAlg: string;
+}
+
+/**
+* @private
+* @desc Helper of generating URL param/value pair
+* @param {string} param key
+* @param {string} value value of key
+* @param {boolean} first determine whether the param is the starting one in order to add query header '?'
+* @return {string}
+*/
+function pvPair(param: string, value: string, first?: boolean): string {
+ return (first === true ? '?' : '&') + param + '=' + value;
+}
+/**
+* @private
+* @desc Refractored part of simple signature generation for login/logout request
+* @param {string} type
+* @param {string} rawSamlRequest
+* @param {object} entitySetting
+* @return {string}
+*/
+function buildSimpleSignature(opts: BuildSimpleSignConfig) : string {
+ const {
+ type,
+ context,
+ entitySetting,
+ } = opts;
+ let { relayState = '' } = opts;
+ const queryParam = libsaml.getQueryParamByType(type);
+
+ if (relayState !== '') {
+ relayState = pvPair(urlParams.relayState, relayState);
+ }
+
+ const sigAlg = pvPair(urlParams.sigAlg, entitySetting.requestSignatureAlgorithm);
+ const octetString = context + relayState + sigAlg;
+ return libsaml.constructMessageSignature(queryParam + '=' + octetString, entitySetting.privateKey, entitySetting.privateKeyPass, undefined, entitySetting.requestSignatureAlgorithm);
+}
+
+/**
+* @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
+*/
+function base64LoginRequest(entity: any, customTagReplacement?: (template: string) => BindingContext): SimpleSignComputedContext {
+ const metadata = { idp: entity.idp.entityMeta, sp: entity.sp.entityMeta };
+ const spSetting = entity.sp.entitySetting;
+ let id: string = '';
+
+ if (metadata && metadata.idp && metadata.sp) {
+ const base = metadata.idp.getSingleSignOnService(binding.simpleSign);
+ let rawSamlRequest: string;
+ if (spSetting.loginRequestTemplate && customTagReplacement) {
+ const info = customTagReplacement(spSetting.loginRequestTemplate.context);
+ id = get(info, 'id', null);
+ rawSamlRequest = get(info, 'context', null);
+ } else {
+ const nameIDFormat = spSetting.nameIDFormat;
+ const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
+ id = spSetting.generateID();
+ rawSamlRequest = libsaml.replaceTagsByValue(libsaml.defaultLoginRequestTemplate.context, {
+ ID: id,
+ Destination: base,
+ Issuer: metadata.sp.getEntityID(),
+ IssueInstant: new Date().toISOString(),
+ AssertionConsumerServiceURL: metadata.sp.getAssertionConsumerService(binding.simpleSign),
+ EntityID: metadata.sp.getEntityID(),
+ AllowCreate: spSetting.allowCreate,
+ NameIDFormat: selectedNameIDFormat
+ } as any);
+ }
+
+ let simpleSignatureContext : any = null;
+ if (metadata.idp.isWantAuthnRequestsSigned()) {
+ const simpleSignature = buildSimpleSignature({
+ type: urlParams.samlRequest,
+ context: rawSamlRequest,
+ entitySetting: spSetting,
+ relayState: spSetting.relayState,
+ });
+
+ simpleSignatureContext = {
+ signature: simpleSignature,
+ sigAlg: spSetting.requestSignatureAlgorithm,
+ };
+ }
+ // No need to embeded XML signature
+ return {
+ id,
+ context: utility.base64Encode(rawSamlRequest),
+ ...simpleSignatureContext,
+ };
+ }
+ throw new Error('ERR_GENERATE_POST_SIMPLESIGN_LOGIN_REQUEST_MISSING_METADATA');
+}
+/**
+* @desc Generate a base64 encoded login response
+* @param {object} requestInfo corresponding request, used to obtain the id
+* @param {object} entity object includes both idp and sp
+* @param {object} user current logged user (e.g. req.user)
+* @param {string} relayState the relay state
+* @param {function} customTagReplacement used when developers have their own login response template
+*/
+async function base64LoginResponse(requestInfo: any = {}, entity: any, user: any = {}, relayState?: string, customTagReplacement?: (template: string) => BindingContext): Promise {
+ const idpSetting = entity.idp.entitySetting;
+ const spSetting = entity.sp.entitySetting;
+ const id = idpSetting.generateID();
+ const metadata = {
+ idp: entity.idp.entityMeta,
+ sp: entity.sp.entityMeta,
+ };
+ const nameIDFormat = idpSetting.nameIDFormat;
+ const selectedNameIDFormat = Array.isArray(nameIDFormat) ? nameIDFormat[0] : nameIDFormat;
+ if (metadata && metadata.idp && metadata.sp) {
+ const base = metadata.sp.getAssertionConsumerService(binding.simpleSign);
+ let rawSamlResponse: string;
+ const nowTime = new Date();
+ // Five minutes later : nowtime + 5 * 60 * 1000 (in milliseconds)
+ const fiveMinutesLaterTime = new Date(nowTime.getTime() + 300_000 );
+ const tvalue: any = {
+ ID: id,
+ AssertionID: idpSetting.generateID(),
+ Destination: base,
+ Audience: metadata.sp.getEntityID(),
+ EntityID: metadata.sp.getEntityID(),
+ SubjectRecipient: base,
+ Issuer: metadata.idp.getEntityID(),
+ IssueInstant: nowTime.toISOString(),
+ AssertionConsumerServiceURL: base,
+ StatusCode: StatusCode.Success,
+ // can be customized
+ ConditionsNotBefore: nowTime.toISOString(),
+ ConditionsNotOnOrAfter: fiveMinutesLaterTime.toISOString(),
+ SubjectConfirmationDataNotOnOrAfter: fiveMinutesLaterTime.toISOString(),
+ NameIDFormat: selectedNameIDFormat,
+ NameID: user.email || '',
+ InResponseTo: get(requestInfo, 'extract.request.id', ''),
+ AuthnStatement: '',
+ AttributeStatement: '',
+ };
+ if (idpSetting.loginResponseTemplate && customTagReplacement) {
+ const template = customTagReplacement(idpSetting.loginResponseTemplate.context);
+ rawSamlResponse = get(template, 'context', null);
+ } else {
+ if (requestInfo !== null) {
+ tvalue.InResponseTo = requestInfo.extract.request.id;
+ }
+ rawSamlResponse = libsaml.replaceTagsByValue(libsaml.defaultLoginResponseTemplate.context, tvalue);
+ }
+ const { privateKey, privateKeyPass, requestSignatureAlgorithm: signatureAlgorithm } = idpSetting;
+ const config = {
+ privateKey,
+ privateKeyPass,
+ signatureAlgorithm,
+ signingCert: metadata.idp.getX509Certificate('signing'),
+ isBase64Output: false,
+ };
+ // step: sign assertion ? -> encrypted ? -> sign message ?
+ if (metadata.sp.isWantAssertionsSigned()) {
+ rawSamlResponse = libsaml.constructSAMLSignature({
+ ...config,
+ rawSamlMessage: rawSamlResponse,
+ transformationAlgorithms: spSetting.transformationAlgorithms,
+ referenceTagXPath: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']",
+ signatureConfig: {
+ prefix: 'ds',
+ location: { reference: "/*[local-name(.)='Response']/*[local-name(.)='Assertion']/*[local-name(.)='Issuer']", action: 'after' },
+ },
+ });
+ }
+
+ // SAML response must be signed sign message first, then encrypt
+ let simpleSignature: string = '';
+ // like in post and redirect bindings, login response is always signed.
+ simpleSignature = buildSimpleSignature({
+ type: urlParams.samlResponse,
+ context: rawSamlResponse,
+ entitySetting: idpSetting,
+ relayState: relayState,
+ } );
+
+ return Promise.resolve({
+ id,
+ context: utility.base64Encode(rawSamlResponse),
+ signature: simpleSignature,
+ sigAlg: idpSetting.requestSignatureAlgorithm,
+ });
+
+ }
+ throw new Error('ERR_GENERATE_POST_SIMPLESIGN_LOGIN_RESPONSE_MISSING_METADATA');
+}
+
+const simpleSignBinding = {
+ base64LoginRequest,
+ base64LoginResponse,
+ };
+
+export default simpleSignBinding;
diff --git a/src/entity-idp.ts b/src/entity-idp.ts
index 01634caf..f4705939 100644
--- a/src/entity-idp.ts
+++ b/src/entity-idp.ts
@@ -13,6 +13,8 @@ import {
import libsaml from './libsaml';
import { namespace } from './urn';
import postBinding from './binding-post';
+import redirectBinding from './binding-redirect';
+import simpleSignBinding from './binding-simplesign';
import { flow, FlowResult } from './flow';
import { isString } from './utility';
import { BindingContext } from './entity';
@@ -28,7 +30,7 @@ export default function(props: IdentityProviderSettings) {
* Identity prvider can be configured using either metadata importing or idpSetting
*/
export class IdentityProvider extends Entity {
-
+
entityMeta: IdentityProviderMetadata;
constructor(idpSetting: IdentityProviderSettings) {
@@ -42,8 +44,20 @@ export class IdentityProvider extends Entity {
// build attribute part
if (idpSetting.loginResponseTemplate) {
if (isString(idpSetting.loginResponseTemplate.context) && Array.isArray(idpSetting.loginResponseTemplate.attributes)) {
+ let attributeStatementTemplate;
+ let attributeTemplate;
+ if (!idpSetting.loginResponseTemplate.additionalTemplates || !idpSetting.loginResponseTemplate.additionalTemplates!.attributeStatementTemplate) {
+ attributeStatementTemplate = libsaml.defaultAttributeStatementTemplate;
+ } else {
+ attributeStatementTemplate = idpSetting.loginResponseTemplate.additionalTemplates!.attributeStatementTemplate!;
+ }
+ if (!idpSetting.loginResponseTemplate.additionalTemplates || !idpSetting.loginResponseTemplate.additionalTemplates!.attributeTemplate) {
+ attributeTemplate = libsaml.defaultAttributeTemplate;
+ } else {
+ attributeTemplate = idpSetting.loginResponseTemplate.additionalTemplates!.attributeTemplate!;
+ }
const replacement = {
- AttributeStatement: libsaml.attributeStatementBuilder(idpSetting.loginResponseTemplate.attributes),
+ AttributeStatement: libsaml.attributeStatementBuilder(idpSetting.loginResponseTemplate.attributes, attributeTemplate, attributeStatementTemplate),
};
entitySetting.loginResponseTemplate = {
...entitySetting.loginResponseTemplate,
@@ -64,6 +78,7 @@ export class IdentityProvider extends Entity {
* @param user current logged user (e.g. req.user)
* @param customTagReplacement used when developers have their own login response template
* @param encryptThenSign whether or not to encrypt then sign first (if signing)
+ * @param relayState the relayState from corresponding request
*/
public async createLoginResponse(
sp: ServiceProvider,
@@ -72,21 +87,41 @@ export class IdentityProvider extends Entity {
user: { [key: string]: any },
customTagReplacement?: (template: string) => BindingContext,
encryptThenSign?: boolean,
+ relayState?: string,
) {
const protocol = namespace.binding[binding];
- // can only support post binding for login response
- if (protocol === namespace.binding.post) {
- const context = await postBinding.base64LoginResponse(requestInfo, {
- idp: this,
- sp,
- }, user, customTagReplacement, encryptThenSign);
- return {
- ...context,
- entityEndpoint: (sp.entityMeta as ServiceProviderMetadata).getAssertionConsumerService(binding),
- type: 'SAMLResponse'
- };
+ // can support post, redirect and post simple sign bindings for login response
+ let context: any = null;
+ switch (protocol) {
+ case namespace.binding.post:
+ context = await postBinding.base64LoginResponse(requestInfo, {
+ idp: this,
+ sp,
+ }, user, customTagReplacement, encryptThenSign);
+ break;
+
+ case namespace.binding.simpleSign:
+ context = await simpleSignBinding.base64LoginResponse( requestInfo, {
+ idp: this, sp,
+ }, user, relayState, customTagReplacement);
+ break;
+
+ case namespace.binding.redirect:
+ return redirectBinding.loginResponseRedirectURL(requestInfo, {
+ idp: this,
+ sp,
+ }, user, relayState, customTagReplacement);
+
+ default:
+ throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
}
- throw new Error('ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
+
+ return {
+ ...context,
+ relayState,
+ entityEndpoint: (sp.entityMeta as ServiceProviderMetadata).getAssertionConsumerService(binding) as string,
+ type: 'SAMLResponse'
+ };
}
/**
@@ -104,7 +139,7 @@ export class IdentityProvider extends Entity {
parserType: 'SAMLRequest',
type: 'login',
binding: binding,
- request: req
+ request: req
});
}
}
diff --git a/src/entity-sp.ts b/src/entity-sp.ts
index c89e33bc..454d5664 100644
--- a/src/entity-sp.ts
+++ b/src/entity-sp.ts
@@ -7,6 +7,7 @@ import Entity, {
BindingContext,
PostBindingContext,
ESamlHttpRequest,
+ SimpleSignBindingContext,
} from './entity';
import {
IdentityProviderConstructor as IdentityProvider,
@@ -16,6 +17,7 @@ import {
import { namespace } from './urn';
import redirectBinding from './binding-redirect';
import postBinding from './binding-post';
+import simpleSignBinding from './binding-simplesign';
import { flow, FlowResult } from './flow';
/*
@@ -56,28 +58,38 @@ export class ServiceProvider extends Entity {
idp: IdentityProvider,
binding = 'redirect',
customTagReplacement?: (template: string) => BindingContext,
- ): BindingContext | PostBindingContext {
+ ): BindingContext | PostBindingContext| SimpleSignBindingContext {
const nsBinding = namespace.binding;
const protocol = nsBinding[binding];
if (this.entityMeta.isAuthnRequestSigned() !== idp.entityMeta.isWantAuthnRequestsSigned()) {
throw new Error('ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG');
}
- if (protocol === nsBinding.redirect) {
- return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement);
- }
+ let context: any = null;
+ switch (protocol) {
+ case nsBinding.redirect:
+ return redirectBinding.loginRequestRedirectURL({ idp, sp: this }, customTagReplacement);
- if (protocol === nsBinding.post) {
- const context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement);
- return {
- ...context,
- relayState: this.entitySetting.relayState,
- entityEndpoint: idp.entityMeta.getSingleSignOnService(binding) as string,
- type: 'SAMLRequest',
- };
- }
- // Will support artifact in the next release
- throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
+ case nsBinding.post:
+ context = postBinding.base64LoginRequest("/*[local-name(.)='AuthnRequest']", { idp, sp: this }, customTagReplacement);
+ break;
+
+ case nsBinding.simpleSign:
+ // Object context = {id, context, signature, sigAlg}
+ context = simpleSignBinding.base64LoginRequest( { idp, sp: this }, customTagReplacement);
+ break;
+
+ default:
+ // Will support artifact in the next release
+ throw new Error('ERR_SP_LOGIN_REQUEST_UNDEFINED_BINDING');
+ }
+
+ return {
+ ...context,
+ relayState: this.entitySetting.relayState,
+ entityEndpoint: idp.entityMeta.getSingleSignOnService(binding) as string,
+ type: 'SAMLRequest',
+ };
}
/**
diff --git a/src/entity.ts b/src/entity.ts
index 28da535f..5f7436ca 100644
--- a/src/entity.ts
+++ b/src/entity.ts
@@ -48,6 +48,17 @@ export interface PostBindingContext extends BindingContext {
type: string;
}
+export interface SimpleSignBindingContext extends PostBindingContext {
+ sigAlg?: string;
+ signature?: string;
+ keyInfo?: string;
+}
+
+export interface SimpleSignComputedContext extends BindingContext {
+ sigAlg?: string;
+ signature?: string;
+}
+
export interface ParseResult {
samlContent: string;
extract: any;
diff --git a/src/flow.ts b/src/flow.ts
index 7750563b..4b016741 100644
--- a/src/flow.ts
+++ b/src/flow.ts
@@ -1,4 +1,4 @@
-import { inflateString, base64Decode } from './utility';
+import { inflateString, base64Decode, isNonEmptyArray } from './utility';
import { verifyTime } from './validator';
import libsaml from './libsaml';
import {
@@ -19,6 +19,7 @@ import {
MessageSignatureOrder,
StatusCode
} from './urn';
+import simpleSignBinding from './binding-simplesign';
const bindDict = wording.binding;
const urlParams = wording.urlParams;
@@ -26,6 +27,7 @@ const urlParams = wording.urlParams;
export interface FlowResult {
samlContent: string;
extract: any;
+ sigAlg?: string|null ;
}
// get the default extractor fields based on the parserType
@@ -49,9 +51,9 @@ function getDefaultExtractorFields(parserType: ParserType, assertion?: any): Ext
}
// proceed the redirect binding flow
-async function redirectFlow(options) {
+async function redirectFlow(options): Promise {
- const { request, parserType, checkSignature = true, from } = options;
+ const { request, parserType, self, checkSignature = true, from } = options;
const { query, octetString } = request;
const { SigAlg: sigAlg, Signature: signature } = query;
@@ -68,20 +70,32 @@ async function redirectFlow(options) {
const xmlString = inflateString(decodeURIComponent(content));
- // validate the xml (remarks: login response must be gone through post flow)
- if (
- parserType === urlParams.samlRequest ||
- parserType === urlParams.logoutRequest ||
- parserType === urlParams.logoutResponse
- ) {
- try {
- await libsaml.isValidXml(xmlString);
- } catch (e) {
- return Promise.reject('ERR_INVALID_XML');
+ // validate the xml
+ try {
+ await libsaml.isValidXml(xmlString);
+ } catch (e) {
+ return Promise.reject('ERR_INVALID_XML');
+ }
+
+ // check status based on different scenarios
+ await checkStatus(xmlString, parserType);
+
+ let assertion: string = '';
+
+ if (parserType === urlParams.samlResponse){
+ // Extract assertion shortcut
+ const verifiedDoc = extract(xmlString, [{
+ key: 'assertion',
+ localPath: ['~Response', 'Assertion'],
+ attributes: [],
+ context: true
+ }]);
+ if (verifiedDoc && verifiedDoc.assertion){
+ assertion = verifiedDoc.assertion as string;
}
}
- const extractorFields = getDefaultExtractorFields(parserType);
+ const extractorFields = getDefaultExtractorFields(parserType, assertion.length > 0 ? assertion : null);
const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = {
samlContent: xmlString,
@@ -89,9 +103,6 @@ async function redirectFlow(options) {
extract: extract(xmlString, extractorFields),
};
- // check status based on different scenarios
- await checkStatus(xmlString, parserType);
-
// see if signature check is required
// only verify message signature is enough
if (checkSignature) {
@@ -113,6 +124,49 @@ async function redirectFlow(options) {
parseResult.sigAlg = decodeSigAlg;
}
+ /**
+ * Validation part: validate the context of response after signature is verified and decrpyted (optional)
+ */
+ const issuer = targetEntityMetadata.getEntityID();
+ const extractedProperties = parseResult.extract;
+
+ // unmatched issuer
+ if (
+ (parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
+ && extractedProperties
+ && extractedProperties.issuer !== issuer
+ ) {
+ return Promise.reject('ERR_UNMATCH_ISSUER');
+ }
+
+ // invalid session time
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
+ if (
+ parserType === 'SAMLResponse'
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
+ && !verifyTime(
+ undefined,
+ extractedProperties.sessionIndex.sessionNotOnOrAfter,
+ self.entitySetting.clockDrifts
+ )
+ ) {
+ return Promise.reject('ERR_EXPIRED_SESSION');
+ }
+
+ // invalid time
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+ if (
+ parserType === 'SAMLResponse'
+ && extractedProperties.conditions
+ && !verifyTime(
+ extractedProperties.conditions.notBefore,
+ extractedProperties.conditions.notOnOrAfter,
+ self.entitySetting.clockDrifts
+ )
+ ) {
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
+ }
+
return Promise.resolve(parseResult);
}
@@ -149,7 +203,7 @@ async function postFlow(options): Promise {
if (parserType !== urlParams.samlResponse) {
extractorFields = getDefaultExtractorFields(parserType, null);
}
-
+
// check status based on different scenarios
await checkStatus(samlContent, parserType);
@@ -238,6 +292,129 @@ async function postFlow(options): Promise {
return Promise.resolve(parseResult);
}
+
+// proceed the post simple sign binding flow
+async function postSimpleSignFlow(options): Promise {
+
+ const { request, parserType, self, checkSignature = true, from } = options;
+
+ const { body, octetString } = request;
+
+ const targetEntityMetadata = from.entityMeta;
+
+ // ?SAMLRequest= or ?SAMLResponse=
+ const direction = libsaml.getQueryParamByType(parserType);
+ const encodedRequest: string = body[direction];
+ const sigAlg: string = body['SigAlg'];
+ const signature: string = body['Signature'];
+
+ // query must contain the saml content
+ if (encodedRequest === undefined) {
+ return Promise.reject('ERR_SIMPLESIGN_FLOW_BAD_ARGS');
+ }
+
+ const xmlString = String(base64Decode(encodedRequest));
+
+ // validate the xml
+ try {
+ await libsaml.isValidXml(xmlString);
+ } catch (e) {
+ return Promise.reject('ERR_INVALID_XML');
+ }
+
+ // check status based on different scenarios
+ await checkStatus(xmlString, parserType);
+
+ let assertion: string = '';
+
+ if (parserType === urlParams.samlResponse){
+ // Extract assertion shortcut
+ const verifiedDoc = extract(xmlString, [{
+ key: 'assertion',
+ localPath: ['~Response', 'Assertion'],
+ attributes: [],
+ context: true
+ }]);
+ if (verifiedDoc && verifiedDoc.assertion){
+ assertion = verifiedDoc.assertion as string;
+ }
+ }
+
+ const extractorFields = getDefaultExtractorFields(parserType, assertion.length > 0 ? assertion : null);
+
+ const parseResult: { samlContent: string, extract: any, sigAlg: (string | null) } = {
+ samlContent: xmlString,
+ sigAlg: null,
+ extract: extract(xmlString, extractorFields),
+ };
+
+ // see if signature check is required
+ // only verify message signature is enough
+ if (checkSignature) {
+ if (!signature || !sigAlg) {
+ return Promise.reject('ERR_MISSING_SIG_ALG');
+ }
+
+ // put the below two assignemnts into verifyMessageSignature function
+ const base64Signature = Buffer.from(signature, 'base64');
+
+ const verified = libsaml.verifyMessageSignature(targetEntityMetadata, octetString, base64Signature, sigAlg);
+
+ if (!verified) {
+ // Fail to verify message signature
+ return Promise.reject('ERR_FAILED_MESSAGE_SIGNATURE_VERIFICATION');
+ }
+
+ parseResult.sigAlg = sigAlg;
+ }
+
+ /**
+ * Validation part: validate the context of response after signature is verified and decrpyted (optional)
+ */
+ const issuer = targetEntityMetadata.getEntityID();
+ const extractedProperties = parseResult.extract;
+
+ // unmatched issuer
+ if (
+ (parserType === 'LogoutResponse' || parserType === 'SAMLResponse')
+ && extractedProperties
+ && extractedProperties.issuer !== issuer
+ ) {
+ return Promise.reject('ERR_UNMATCH_ISSUER');
+ }
+
+ // invalid session time
+ // only run the verifyTime when `SessionNotOnOrAfter` exists
+ if (
+ parserType === 'SAMLResponse'
+ && extractedProperties.sessionIndex.sessionNotOnOrAfter
+ && !verifyTime(
+ undefined,
+ extractedProperties.sessionIndex.sessionNotOnOrAfter,
+ self.entitySetting.clockDrifts
+ )
+ ) {
+ return Promise.reject('ERR_EXPIRED_SESSION');
+ }
+
+ // invalid time
+ // 2.4.1.2 https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
+ if (
+ parserType === 'SAMLResponse'
+ && extractedProperties.conditions
+ && !verifyTime(
+ extractedProperties.conditions.notBefore,
+ extractedProperties.conditions.notOnOrAfter,
+ self.entitySetting.clockDrifts
+ )
+ ) {
+ return Promise.reject('ERR_SUBJECT_UNCONFIRMED');
+ }
+
+ return Promise.resolve(parseResult);
+}
+
+
function checkStatus(content: string, parserType: string): Promise {
// only check response parser
@@ -269,10 +446,10 @@ export function flow(options): Promise {
const binding = options.binding;
const parserType = options.parserType;
- options.supportBindings = [BindingNamespace.Redirect, BindingNamespace.Post];
- // saml response only allows POST
+ options.supportBindings = [BindingNamespace.Redirect, BindingNamespace.Post, BindingNamespace.SimpleSign];
+ // saml response allows POST, REDIRECT
if (parserType === ParserType.SAMLResponse) {
- options.supportBindings = [BindingNamespace.Post];
+ options.supportBindings = [BindingNamespace.Post, BindingNamespace.Redirect, BindingNamespace.SimpleSign];
}
if (binding === bindDict.post) {
@@ -283,6 +460,10 @@ export function flow(options): Promise {
return redirectFlow(options);
}
+ if (binding === bindDict.simpleSign) {
+ return postSimpleSignFlow(options);
+ }
+
return Promise.reject('ERR_UNEXPECTED_FLOW');
}
diff --git a/src/libsaml.ts b/src/libsaml.ts
index 4159cbd2..2cf67cec 100644
--- a/src/libsaml.ts
+++ b/src/libsaml.ts
@@ -58,13 +58,23 @@ export interface LoginResponseAttribute {
valueXmlnsXsi?: string;
}
+export interface LoginResponseAdditionalTemplates {
+ attributeStatementTemplate?: AttributeStatementTemplate;
+ attributeTemplate?:AttributeTemplate;
+}
+
export interface BaseSamlTemplate {
context: string;
}
export interface LoginResponseTemplate extends BaseSamlTemplate {
attributes?: LoginResponseAttribute[];
+ additionalTemplates?: LoginResponseAdditionalTemplates;
}
+export interface AttributeStatementTemplate extends BaseSamlTemplate { }
+
+export interface AttributeTemplate extends BaseSamlTemplate { }
+
export interface LoginRequestTemplate extends BaseSamlTemplate { }
export interface LogoutRequestTemplate extends BaseSamlTemplate { }
@@ -81,7 +91,7 @@ export interface LibSamlInterface {
getQueryParamByType: (type: string) => string;
createXPath: (local, isExtractAll?: boolean) => string;
replaceTagsByValue: (rawXML: string, tagValues: any) => string;
- attributeStatementBuilder: (attributes: LoginResponseAttribute[]) => string;
+ attributeStatementBuilder: (attributes: LoginResponseAttribute[], attributeTemplate : AttributeTemplate, attributeStatementTemplate : AttributeStatementTemplate) => string;
constructSAMLSignature: (opts: SignatureConstructor) => string;
verifySignature: (xml: string, opts) => [boolean, any];
createKeySection: (use: KeyUse, cert: string | Buffer) => {};
@@ -97,6 +107,8 @@ export interface LibSamlInterface {
nrsaAliasMapping: any;
defaultLoginRequestTemplate: LoginRequestTemplate;
defaultLoginResponseTemplate: LoginResponseTemplate;
+ defaultAttributeStatementTemplate: AttributeStatementTemplate;
+ defaultAttributeTemplate: AttributeTemplate;
defaultLogoutRequestTemplate: LogoutRequestTemplate;
defaultLogoutResponseTemplate: LogoutResponseTemplate;
}
@@ -138,6 +150,23 @@ const libSaml = () => {
const defaultLogoutRequestTemplate = {
context: '{Issuer}{NameID}',
};
+
+ /**
+ * @desc Default AttributeStatement template
+ * @type {AttributeStatementTemplate}
+ */
+ const defaultAttributeStatementTemplate = {
+ context: '{Attributes}',
+ };
+
+ /**
+ * @desc Default Attribute template
+ * @type {AttributeTemplate}
+ */
+ const defaultAttributeTemplate = {
+ context: '{Value}',
+ };
+
/**
* @desc Default login response template
* @type {LoginResponseTemplate}
@@ -145,6 +174,10 @@ const libSaml = () => {
const defaultLoginResponseTemplate = {
context: '{Issuer}{Issuer}{NameID}{Audience}{AuthnStatement}{AttributeStatement}',
attributes: [],
+ additionalTemplates:{
+ "attributeStatementTemplate": defaultAttributeStatementTemplate,
+ "attributeTemplate": defaultAttributeTemplate
+ }
};
/**
* @desc Default logout response template
@@ -213,6 +246,8 @@ const libSaml = () => {
getQueryParamByType,
defaultLoginRequestTemplate,
defaultLoginResponseTemplate,
+ defaultAttributeStatementTemplate,
+ defaultAttributeTemplate,
defaultLogoutRequestTemplate,
defaultLogoutResponseTemplate,
@@ -231,16 +266,32 @@ const libSaml = () => {
/**
* @desc Helper function to build the AttributeStatement tag
* @param {LoginResponseAttribute} attributes an array of attribute configuration
+ * @param {AttributeTemplate} attributeTemplate the attribut tag template to be used
+ * @param {AttributeStatementTemplate} attributeStatementTemplate the attributStatement tag template to be used
* @return {string}
*/
- attributeStatementBuilder(attributes: LoginResponseAttribute[]): string {
- const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => {
- const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema';
- const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance';
- return `{${tagging('attr', valueTag)}}`;
- }).join('');
- return `${attr}`;
- },
+ attributeStatementBuilder(attributes: LoginResponseAttribute[], attributeTemplate : AttributeTemplate, attributeStatementTemplate : AttributeStatementTemplate): string {
+ if (!attributeStatementTemplate){
+ attributeStatementTemplate = defaultAttributeStatementTemplate;
+ }
+ if (!attributeTemplate){
+ attributeTemplate = defaultAttributeTemplate;
+ }
+ const attr = attributes.map(({ name, nameFormat, valueTag, valueXsiType, valueXmlnsXs, valueXmlnsXsi }) => {
+ const defaultValueXmlnsXs = 'http://www.w3.org/2001/XMLSchema';
+ const defaultValueXmlnsXsi = 'http://www.w3.org/2001/XMLSchema-instance';
+ let attributeLine = attributeTemplate.context;
+ attributeLine = attributeLine.replace('{Name}',name);
+ attributeLine = attributeLine.replace('{NameFormat}',nameFormat);
+ attributeLine = attributeLine.replace('{ValueXmlnsXs}',valueXmlnsXs ? valueXmlnsXs : defaultValueXmlnsXs);
+ attributeLine = attributeLine.replace('{ValueXmlnsXsi}',valueXmlnsXsi ? valueXmlnsXsi : defaultValueXmlnsXsi);
+ attributeLine = attributeLine.replace('{ValueXsiType}',valueXsiType);
+ attributeLine = attributeLine.replace('{Value}',`{${tagging('attr', valueTag)}}`);
+ return attributeLine;
+ }).join('');
+ return attributeStatementTemplate.context.replace('{Attributes}',attr);
+},
+
/**
* @desc Construct the XML signature for POST binding
* @param {string} rawSamlMessage request/response xml string
@@ -366,11 +417,12 @@ const libSaml = () => {
// normalise the certificate string
metadataCert = metadataCert.map(utility.normalizeCerString);
- if (certificateNode.length === 0) {
+ // no certificate in node response nor metadata
+ if (certificateNode.length === 0 && metadataCert.length === 0) {
throw new Error('NO_SELECTED_CERTIFICATE');
}
- // no certificate node in response
+ // certificate node in response
if (certificateNode.length !== 0) {
const x509CertificateData = certificateNode[0].firstChild.data;
const x509Certificate = utility.normalizeCerString(x509CertificateData);
@@ -386,9 +438,12 @@ const libSaml = () => {
sig.keyInfoProvider = new this.getKeyInfo(x509Certificate);
+ } else {
+ // Select first one from metadata
+ sig.keyInfoProvider = new this.getKeyInfo(metadataCert[0]);
}
- }
+ }
sig.loadSignature(signatureNode);
diff --git a/src/urn.ts b/src/urn.ts
index 7a15a25c..df71255a 100644
--- a/src/urn.ts
+++ b/src/urn.ts
@@ -7,6 +7,7 @@
export enum BindingNamespace {
Redirect = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
Post = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ SimpleSign = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign',
Artifact = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact'
}
@@ -47,6 +48,7 @@ const namespace = {
binding: {
redirect: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
post: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
+ simpleSign: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST-SimpleSign',
artifact: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact',
},
names: {
@@ -184,6 +186,7 @@ const wording = {
binding: {
redirect: 'redirect',
post: 'post',
+ simpleSign: 'simpleSign',
artifact: 'artifact',
},
certUse: {
diff --git a/test/flow.ts b/test/flow.ts
index 70cfc55b..16f94670 100644
--- a/test/flow.ts
+++ b/test/flow.ts
@@ -1,7 +1,7 @@
import esaml2 = require('../index');
import { readFileSync, writeFileSync } from 'fs';
import test from 'ava';
-import { PostBindingContext } from '../src/entity';
+import { PostBindingContext, SimpleSignBindingContext } from '../src/entity';
import * as uuid from 'uuid';
import * as url from 'url';
import util from '../src/utility';
@@ -42,7 +42,7 @@ const loginResponseTemplate = {
const failedResponse: string = String(readFileSync('./test/misc/failed_response.xml'));
-const createTemplateCallback = (_idp, _sp, user) => template => {
+const createTemplateCallback = (_idp, _sp, _binding, user) => template => {
const _id = '_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6';
const now = new Date();
const spEntityID = _sp.entityMeta.getEntityID();
@@ -52,7 +52,7 @@ const createTemplateCallback = (_idp, _sp, user) => template => {
const tvalue = {
ID: _id,
AssertionID: idpSetting.generateID ? idpSetting.generateID() : `${uuid.v4()}`,
- Destination: _sp.entityMeta.getAssertionConsumerService(binding.post),
+ Destination: _sp.entityMeta.getAssertionConsumerService(_binding),
Audience: spEntityID,
SubjectRecipient: spEntityID,
NameIDFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
@@ -62,7 +62,7 @@ const createTemplateCallback = (_idp, _sp, user) => template => {
ConditionsNotBefore: now.toISOString(),
ConditionsNotOnOrAfter: fiveMinutesLater.toISOString(),
SubjectConfirmationDataNotOnOrAfter: fiveMinutesLater.toISOString(),
- AssertionConsumerServiceURL: _sp.entityMeta.getAssertionConsumerService(binding.post),
+ AssertionConsumerServiceURL: _sp.entityMeta.getAssertionConsumerService(_binding),
EntityID: spEntityID,
InResponseTo: '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4',
StatusCode: 'urn:oasis:names:tc:SAML:2.0:status:Success',
@@ -75,6 +75,38 @@ const createTemplateCallback = (_idp, _sp, user) => template => {
};
};
+// Parse Redirect Url context
+
+const parseRedirectUrlContextCallBack = (_context) => {
+ const originalURL = url.parse(_context, true);
+ const _SAMLResponse = originalURL.query.SAMLResponse;
+ 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('&');
+
+ return { query: {
+ SAMLResponse: _SAMLResponse,
+ Signature: _Signature,
+ SigAlg: _SigAlg, },
+ octetString: _octetString,
+ };
+};
+
+// Build SimpleSign octetString
+const buildSimpleSignOctetString = (type:string, context:string, sigAlg:string|undefined, relayState:string|undefined, signature: string|undefined) =>{
+ const rawRequest = String(utility.base64Decode(context, true));
+ let octetString:string = '';
+ octetString += type + '=' + rawRequest;
+ if (relayState !== undefined && relayState.length > 0){
+ octetString += '&RelayState=' + relayState;
+ }
+ if (signature !== undefined && signature.length >0 && sigAlg && sigAlg.length > 0){
+ octetString += '&SigAlg=' + sigAlg;
+ }
+ return octetString;
+};
+
// Define of metadata
const defaultIdpConfig = {
@@ -149,6 +181,18 @@ test('create login request with redirect binding using default template and pars
t.is(extract.nameIDPolicy.allowCreate, 'false');
});
+test('create login request with post simpleSign binding using default template and parse it', async t => {
+ const { relayState, id, context: SAMLRequest, type, sigAlg, signature } = sp.createLoginRequest(idp, 'simpleSign') as SimpleSignBindingContext;
+ t.is(typeof id, 'string');
+ t.is(typeof SAMLRequest, 'string');
+ const octetString = buildSimpleSignOctetString(type, SAMLRequest, sigAlg, relayState,signature);
+ const { samlContent, extract } = await idp.parseLoginRequest(sp, 'simpleSign', { body: { SAMLRequest, Signature: signature, SigAlg:sigAlg }, octetString});
+ t.is(extract.issuer, 'https://sp.example.org/metadata');
+ t.is(typeof extract.request.id, 'string');
+ t.is(extract.nameIDPolicy.format, 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress');
+ t.is(extract.nameIDPolicy.allowCreate, 'false');
+});
+
test('create login request with post binding using default template and parse it', async t => {
const { relayState, type, entityEndpoint, id, context: SAMLRequest } = sp.createLoginRequest(idp, 'post') as PostBindingContext;
t.is(typeof id, 'string');
@@ -183,6 +227,16 @@ test('signed in sp is not matched with the signed notation in idp with redirect
}
});
+test('signed in sp is not matched with the signed notation in idp with post simpleSign request', t => {
+ const _idp = identityProvider({ ...defaultIdpConfig, metadata: noSignedIdpMetadata });
+ try {
+ const { id, context } = sp.createLoginRequest(_idp, 'simpleSign');
+ t.fail();
+ } catch (e) {
+ t.is(e.message, 'ERR_METADATA_CONFLICT_REQUEST_SIGNED_FLAG');
+ }
+});
+
test('create login request with redirect binding using [custom template]', t => {
const _sp = serviceProvider({
...defaultSpConfig, loginRequestTemplate: {
@@ -218,15 +272,53 @@ test('create login request with post binding using [custom template]', t => {
? t.pass() : t.fail();
});
+test('create login request with post simpleSign binding using [custom template]', t => {
+ const _sp = serviceProvider({
+ ...defaultSpConfig, loginRequestTemplate: {
+ context: '{Issuer}',
+ },
+ });
+ const { id, context, entityEndpoint, type, relayState, signature, sigAlg } = _sp.createLoginRequest(idp, 'simpleSign', template => {
+ return {
+ id: 'exposed_testing_id',
+ context: template, // all the tags are supposed to be replaced
+ };
+ }) as SimpleSignBindingContext;
+ id === 'exposed_testing_id' &&
+ isString(context) &&
+ isString(relayState) &&
+ isString(entityEndpoint) &&
+ isString(signature) &&
+ isString(sigAlg) &&
+ type === 'SAMLRequest'
+ ? t.pass() : t.fail();
+});
+
test('create login response with undefined binding', async t => {
const user = { email: 'user@esaml2.com' };
- const error = await t.throwsAsync(() => idp.createLoginResponse(sp, {}, 'undefined', user, createTemplateCallback(idp, sp, user)));
+ const error = await t.throwsAsync(() => idp.createLoginResponse(sp, {}, 'undefined', user, createTemplateCallback(idp, sp, binding.post, user)));
t.is(error.message, 'ERR_CREATE_RESPONSE_UNDEFINED_BINDING');
});
+test('create redirect login response', async t => {
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idp.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idp, sp, binding.redirect, user), undefined, 'relaystate');
+ isString(id) && isString(context) ? t.pass() : t.fail();
+});
+
+test('create post simpleSign login response', async t => {
+ const user = { email: 'user@esaml2.com' };
+ const { id, context, entityEndpoint, type, signature, sigAlg } = await idp.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, sp, binding.simpleSign, user), undefined, 'relaystate') as SimpleSignBindingContext;
+ isString(id) &&
+ isString(context) &&
+ isString(entityEndpoint) &&
+ isString(signature) &&
+ isString(sigAlg) ? t.pass() : t.fail();
+});
+
test('create post login response', async t => {
const user = { email: 'user@esaml2.com' };
- const { id, context } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user));
+ const { id, context } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user));
isString(id) && isString(context) ? t.pass() : t.fail();
});
@@ -248,7 +340,7 @@ test('create logout request when idp only has one binding', t => {
test('create logout response with undefined binding', t => {
try {
- const { id, context } = idp.createLogoutResponse(sp, {}, 'undefined', '', createTemplateCallback(idp, sp, {}));
+ const { id, context } = idp.createLogoutResponse(sp, {}, 'undefined', '', createTemplateCallback(idp, sp, binding.post, {}));
t.fail();
} catch (e) {
t.is(e.message, 'ERR_CREATE_LOGOUT_RESPONSE_UNDEFINED_BINDING');
@@ -256,12 +348,12 @@ test('create logout response with undefined binding', t => {
});
test('create logout response with redirect binding', t => {
- const { id, context } = idp.createLogoutResponse(sp, {}, 'redirect', '', createTemplateCallback(idp, sp, {}));
+ const { id, context } = idp.createLogoutResponse(sp, {}, 'redirect', '', createTemplateCallback(idp, sp, binding.post, {}));
isString(id) && isString(context) ? t.pass() : t.fail();
});
test('create logout response with post binding', t => {
- const { relayState, type, entityEndpoint, id, context } = idp.createLogoutResponse(sp, {}, 'post', '', createTemplateCallback(idp, sp, {})) as PostBindingContext;
+ const { relayState, type, entityEndpoint, id, context } = idp.createLogoutResponse(sp, {}, 'post', '', createTemplateCallback(idp, sp, binding.post, {})) as PostBindingContext;
isString(id) && isString(context) && isString(entityEndpoint) && type === 'SAMLResponse' ? t.pass() : t.fail();
});
@@ -272,7 +364,7 @@ test('create logout response with post binding', t => {
test('send response with signed assertion and parse it', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user));
+ const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
@@ -282,6 +374,49 @@ test('send response with signed assertion and parse it', async t => {
t.is(extract.response.inResponseTo, 'request_id');
});
+// + REDIRECT
+test('send response with signed assertion by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idpNoEncrypt, sp, binding.redirect, user), undefined, 'relaystate');
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) );
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+});
+
+// SimpleSign
+test('send response with signed assertion by post simplesign and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const user = { email: 'user@esaml2.com' };
+ const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(
+ sp,
+ sampleRequestInfo,
+ 'simpleSign',
+ user,
+ createTemplateCallback(idpNoEncrypt, sp, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+});
+
test('send response with signed assertion + custom transformation algorithms and parse it', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const signedAssertionSp = serviceProvider(
@@ -295,7 +430,7 @@ test('send response with signed assertion + custom transformation algorithms and
);
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(signedAssertionSp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user));
+ const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(signedAssertionSp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
@@ -310,6 +445,76 @@ test('send response with signed assertion + custom transformation algorithms and
}
});
+test('send response with signed assertion + custom transformation algorithms by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const signedAssertionSp = serviceProvider(
+ {
+ ...defaultSpConfig,
+ transformationAlgorithms: [
+ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
+ 'http://www.w3.org/2001/10/xml-exc-c14n#'
+ ]
+ }
+ );
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpNoEncrypt.createLoginResponse(signedAssertionSp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idpNoEncrypt, signedAssertionSp, binding.redirect, user), undefined, 'relaystate');
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await signedAssertionSp.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) );
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+
+ // Verify xmldsig#enveloped-signature is included in the response
+ if (samlContent.indexOf('http://www.w3.org/2000/09/xmldsig#enveloped-signature') === -1) {
+ t.fail();
+ }
+});
+
+test('send response with signed assertion + custom transformation algorithms by post simplesign and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const signedAssertionSp = serviceProvider(
+ {
+ ...defaultSpConfig,
+ transformationAlgorithms: [
+ 'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
+ 'http://www.w3.org/2001/10/xml-exc-c14n#'
+ ]
+ }
+ );
+
+ const user = { email: 'user@esaml2.com' }
+ const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(
+ signedAssertionSp,
+ sampleRequestInfo,
+ 'simpleSign',
+ user,
+ createTemplateCallback(idpNoEncrypt, sp, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+
+ // Verify xmldsig#enveloped-signature is included in the response
+ if (samlContent.indexOf('http://www.w3.org/2000/09/xmldsig#enveloped-signature') === -1) {
+ t.fail();
+ }
+});
+
test('send response with [custom template] signed assertion and parse it', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const requestInfo = { extract: { request: { id: 'request_id' } } };
@@ -320,7 +525,7 @@ test('send response with [custom template] signed assertion and parse it', async
'post',
user,
// declare the callback to do custom template replacement
- createTemplateCallback(idpcustomNoEncrypt, sp, user),
+ createTemplateCallback(idpcustomNoEncrypt, sp, binding.post, user),
);
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'post', { body: { SAMLResponse } });
@@ -333,10 +538,67 @@ test('send response with [custom template] signed assertion and parse it', async
t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
});
+test('send response with [custom template] signed assertion by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const requestInfo = { extract: { request: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpcustomNoEncrypt.createLoginResponse(
+ sp,
+ requestInfo,
+ 'redirect',
+ user,
+ createTemplateCallback(idpcustomNoEncrypt, sp, binding.redirect, user),
+ undefined,
+ 'relaystate'
+ );
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context));
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.attributes.name, 'mynameinsp');
+ t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
+ t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
+});
+
+test('send response with [custom template] signed assertion by post simpleSign and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const requestInfo = { extract: { request: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com'};
+ const { id, context: SAMLResponse, type, sigAlg, signature, entityEndpoint, relayState } = await idpcustomNoEncrypt.createLoginResponse(
+ sp,
+ requestInfo,
+ 'simpleSign',
+ user,
+ // declare the callback to do custom template replacement
+ createTemplateCallback(idpcustomNoEncrypt, sp, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await sp.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(entityEndpoint, 'https://sp.example.org/sp/sso');
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.attributes.name, 'mynameinsp');
+ t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
+ t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
+});
+
test('send response with signed message and parse it', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spNoAssertSign, user));
+ const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spNoAssertSign, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
@@ -346,6 +608,56 @@ test('send response with signed message and parse it', async t => {
t.is(extract.response.inResponseTo, 'request_id');
});
+test('send response with signed message by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const requestInfo = { extract: { request: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpNoEncrypt.createLoginResponse(
+ spNoAssertSign,
+ requestInfo,
+ 'redirect',
+ user,
+ createTemplateCallback(idpNoEncrypt, spNoAssertSign, binding.redirect, user),
+ undefined,
+ 'relaystate'
+ );
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) );
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+});
+
+test('send response with signed message by post simplesign and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const user = { email: 'user@esaml2.com' };
+ const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(
+ spNoAssertSign,
+ sampleRequestInfo,
+ 'simpleSign',
+ user,
+ createTemplateCallback(idpNoEncrypt, spNoAssertSign, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+});
+
test('send response with [custom template] and signed message and parse it', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const requestInfo = { extract: { authnrequest: { id: 'request_id' } } };
@@ -354,7 +666,7 @@ test('send response with [custom template] and signed message and parse it', asy
spNoAssertSign,
{ extract: { authnrequest: { id: 'request_id' } } }, 'post',
{ email: 'user@esaml2.com' },
- createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, user),
+ createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, binding.post, user),
);
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'post', { body: { SAMLResponse } });
@@ -367,13 +679,67 @@ test('send response with [custom template] and signed message and parse it', asy
t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
});
+test('send response with [custom template] and signed message by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const requestInfo = { extract: { request: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpcustomNoEncrypt.createLoginResponse(
+ spNoAssertSign,
+ requestInfo,
+ 'redirect',
+ user,
+ createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, binding.redirect, user),
+ undefined,
+ 'relaystate'
+ );
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) );
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.attributes.name, 'mynameinsp');
+ t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
+ t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
+});
+
+test('send response with [custom template] and signed message by post simplesign and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const requestInfo = { extract: { authnrequest: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com'};
+ const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse(
+ spNoAssertSign,
+ { extract: { authnrequest: { id: 'request_id' } } }, 'simpleSign',
+ { email: 'user@esaml2.com' },
+ createTemplateCallback(idpcustomNoEncrypt, spNoAssertSign, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.attributes.name, 'mynameinsp');
+ t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
+ t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
+});
+
test('send login response with signed assertion + signed message and parse it', async t => {
const spWantMessageSign = serviceProvider({
...defaultSpConfig,
wantMessageSigned: true,
});
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spWantMessageSign, user));
+ const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, spWantMessageSign, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spWantMessageSign.parseLoginResponse (idpNoEncrypt, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
@@ -383,6 +749,60 @@ test('send login response with signed assertion + signed message and parse it',
t.is(extract.response.inResponseTo, 'request_id');
});
+test('send response with signed assertion + signed message by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const spWantMessageSign = serviceProvider({
+ ...defaultSpConfig,
+ wantMessageSigned: true,
+ });
+ const requestInfo = { extract: { request: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpNoEncrypt.createLoginResponse(
+ spWantMessageSign,
+ requestInfo,
+ 'redirect',
+ user,
+ createTemplateCallback(idpNoEncrypt, spWantMessageSign, binding.redirect, user),
+ undefined,
+ 'relaystate'
+ );
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) );
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+});
+
+test('send login response with signed assertion + signed message by post simplesign and parse it', async t => {
+ const spWantMessageSign = serviceProvider({
+ ...defaultSpConfig,
+ wantMessageSigned: true,
+ });
+ const user = { email: 'user@esaml2.com' };
+ const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(spWantMessageSign, sampleRequestInfo,
+ 'simpleSign', user,
+ createTemplateCallback(idpNoEncrypt, spWantMessageSign, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await spWantMessageSign.parseLoginResponse (idpNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.response.inResponseTo, 'request_id');
+});
+
test('send login response with [custom template] and signed assertion + signed message and parse it', async t => {
const spWantMessageSign = serviceProvider({
...defaultSpConfig,
@@ -393,7 +813,7 @@ test('send login response with [custom template] and signed assertion + signed m
spWantMessageSign,
{ extract: { authnrequest: { id: 'request_id' } } }, 'post',
{ email: 'user@esaml2.com' },
- createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, user),
+ createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, binding.post, user),
);
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'post', { body: { SAMLResponse } });
@@ -406,9 +826,70 @@ test('send login response with [custom template] and signed assertion + signed m
t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
});
+test('send response with [custom template] and signed assertion + signed message by redirect and parse it', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const spWantMessageSign = serviceProvider({
+ ...defaultSpConfig,
+ wantMessageSigned: true,
+ });
+ const requestInfo = { extract: { request: { id: 'request_id' } } };
+ const user = { email: 'user@esaml2.com' };
+ const { id, context } = await idpcustomNoEncrypt.createLoginResponse(
+ spWantMessageSign,
+ requestInfo,
+ 'redirect',
+ user,
+ createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, binding.redirect, user),
+ undefined,
+ 'relaystate'
+ );
+ const query = url.parse(context).query;
+ t.is(query!.includes('SAMLResponse='), true);
+ t.is(query!.includes('SigAlg='), true);
+ t.is(query!.includes('Signature='), true);
+ t.is(typeof id, 'string');
+ t.is(typeof context, 'string');
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'redirect', parseRedirectUrlContextCallBack(context) );
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.attributes.name, 'mynameinsp');
+ t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
+ t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
+});
+
+test('send login response with [custom template] and signed assertion + signed message by post simplesign and parse it', async t => {
+ const spWantMessageSign = serviceProvider({
+ ...defaultSpConfig,
+ wantMessageSigned: true,
+ });
+ const user = { email: 'user@esaml2.com'};
+ const { id, context: SAMLResponse, type, sigAlg, signature, relayState } = await idpcustomNoEncrypt.createLoginResponse(
+ spWantMessageSign,
+ { extract: { authnrequest: { id: 'request_id' } } },
+ 'simpleSign',
+ { email: 'user@esaml2.com' },
+ createTemplateCallback(idpcustomNoEncrypt, spWantMessageSign, binding.simpleSign, user),
+ undefined,
+ 'relaystate'
+ ) as SimpleSignBindingContext;
+ // receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustomNoEncrypt, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(typeof id, 'string');
+ t.is(samlContent.startsWith(''), true);
+ t.is(extract.nameID, 'user@esaml2.com');
+ t.is(extract.attributes.name, 'mynameinsp');
+ t.is(extract.attributes.mail, 'myemailassociatedwithsp@sp.com');
+ t.is(extract.response.inResponseTo, '_4606cc1f427fa981e6ffd653ee8d6972fc5ce398c4');
+});
+
test('send login response with encrypted non-signed assertion and parse it', async t => {
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idp.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spNoAssertSign, user));
+ const { id, context: SAMLResponse } = await idp.createLoginResponse(spNoAssertSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spNoAssertSign, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spNoAssertSign.parseLoginResponse(idp, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
@@ -420,7 +901,7 @@ test('send login response with encrypted non-signed assertion and parse it', asy
test('send login response with encrypted signed assertion and parse it', async t => {
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user));
+ const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await sp.parseLoginResponse(idp, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
@@ -436,7 +917,7 @@ test('send login response with [custom template] and encrypted signed assertion
sp,
{ extract: { request: { id: 'request_id' } } }, 'post',
{ email: 'user@esaml2.com' },
- createTemplateCallback(idpcustom, sp, user),
+ createTemplateCallback(idpcustom, sp, binding.post, user),
);
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await sp.parseLoginResponse(idpcustom, 'post', { body: { SAMLResponse } });
@@ -455,7 +936,7 @@ test('send login response with encrypted signed assertion + signed message and p
wantMessageSigned: true,
});
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idp.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWantMessageSign, user));
+ const { id, context: SAMLResponse } = await idp.createLoginResponse(spWantMessageSign, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWantMessageSign, binding.post, user));
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idp, 'post', { body: { SAMLResponse } });
@@ -477,7 +958,7 @@ test('send login response with [custom template] encrypted signed assertion + si
spWantMessageSign,
{ extract: { authnrequest: { id: 'request_id' } } }, 'post',
{ email: 'user@esaml2.com' },
- createTemplateCallback(idpcustom, spWantMessageSign, user),
+ createTemplateCallback(idpcustom, spWantMessageSign, binding.post, user),
);
// receiver (caution: only use metadata and public key when declare pair-up in oppoent entity)
const { samlContent, extract } = await spWantMessageSign.parseLoginResponse(idpcustom, 'post', { body: { SAMLResponse } });
@@ -562,7 +1043,7 @@ test('idp sends a post logout request with signature and sp parses it', async t
// simulate init-slo
test('sp sends a post logout response without signature and parse', async t => {
- const { context: SAMLResponse } = sp.createLogoutResponse(idp, null, 'post', '', createTemplateCallback(idp, sp, {})) as PostBindingContext;
+ const { context: SAMLResponse } = sp.createLogoutResponse(idp, null, 'post', '', createTemplateCallback(idp, sp, binding.post, {})) as PostBindingContext;
const { samlContent, extract } = await idp.parseLogoutResponse(sp, 'post', { body: { SAMLResponse }});
t.is(extract.signature, null);
t.is(extract.issuer, 'https://sp.example.org/metadata');
@@ -571,7 +1052,7 @@ test('sp sends a post logout response without signature and parse', async t => {
});
test('sp sends a post logout response with signature and parse', async t => {
- const { relayState, type, entityEndpoint, id, context: SAMLResponse } = sp.createLogoutResponse(idpWantLogoutResSign, sampleRequestInfo, 'post', '', createTemplateCallback(idpWantLogoutResSign, sp, {})) as PostBindingContext;
+ const { relayState, type, entityEndpoint, id, context: SAMLResponse } = sp.createLogoutResponse(idpWantLogoutResSign, sampleRequestInfo, 'post', '', createTemplateCallback(idpWantLogoutResSign, sp, binding.post, {})) as PostBindingContext;
const { samlContent, extract } = await idpWantLogoutResSign.parseLogoutResponse(sp, 'post', { body: { SAMLResponse }});
t.is(typeof extract.signature, 'string');
t.is(extract.issuer, 'https://sp.example.org/metadata');
@@ -581,7 +1062,7 @@ test('sp sends a post logout response with signature and parse', async t => {
test('send login response with encrypted non-signed assertion with EncryptThenSign and parse it', async t => {
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpEncryptThenSign.createLoginResponse(spNoAssertSignCustomConfig, sampleRequestInfo, 'post', user, createTemplateCallback(idpEncryptThenSign, spNoAssertSignCustomConfig, user), true);
+ const { id, context: SAMLResponse } = await idpEncryptThenSign.createLoginResponse(spNoAssertSignCustomConfig, sampleRequestInfo, 'post', user, createTemplateCallback(idpEncryptThenSign, spNoAssertSignCustomConfig, binding.post, user), true);
const { samlContent, extract } = await spNoAssertSignCustomConfig.parseLoginResponse(idpEncryptThenSign, 'post', { body: { SAMLResponse } });
t.is(typeof id, 'string');
t.is(samlContent.startsWith(' {
const idpCustomizePfx = identityProvider(Object.assign(defaultIdpConfig, { tagPrefix: {
encryptedAssertion: 'saml2',
}}));
- const { id, context: SAMLResponse } = await idpCustomizePfx.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpCustomizePfx, sp, user));
+ const { id, context: SAMLResponse } = await idpCustomizePfx.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpCustomizePfx, sp, binding.post, user));
t.is((utility.base64Decode(SAMLResponse) as string).includes('saml2:EncryptedAssertion'), true);
const { samlContent, extract } = await sp.parseLoginResponse(idpCustomizePfx, 'post', { body: { SAMLResponse } });
});
test('Customize prefix (default is saml) for encrypted assertion tag', async t => {
const user = { email: 'test@email.com' };
- const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user));
+ const { id, context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user));
t.is((utility.base64Decode(SAMLResponse) as string).includes('saml:EncryptedAssertion'), true);
const { samlContent, extract } = await sp.parseLoginResponse(idp, 'post', { body: { SAMLResponse } });
});
@@ -609,7 +1090,7 @@ test('Customize prefix (default is saml) for encrypted assertion tag', async t =
test('avoid malformatted response', async t => {
// sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
const user = { email: 'user@email.com' };
- const { context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user));
+ const { context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user));
const rawResponse = String(utility.base64Decode(SAMLResponse, true));
const attackResponse = `evil@evil.com${rawResponse}`;
try {
@@ -620,10 +1101,46 @@ test('avoid malformatted response', async t => {
}
});
+test('avoid malformatted response with redirect binding', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const user = { email: 'user@email.com' };
+ const { id, context } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idpNoEncrypt, sp, binding.redirect, user), undefined, '');
+ const originalURL = url.parse(context, true);
+ const SAMLResponse = originalURL.query.SAMLResponse;
+ const signature = originalURL.query.Signature;
+ const sigAlg = originalURL.query.SigAlg;
+ delete originalURL.query.Signature;
+
+ const rawResponse = utility.inflateString(SAMLResponse as string);
+ const attackResponse = `evil@evil.com${rawResponse}`;
+ const octetString = "SAMLResponse=" + encodeURIComponent(utility.base64Encode(utility.deflateString(attackResponse))) + "&SigAlg=" + encodeURIComponent(sigAlg as string);
+ try {
+ await sp.parseLoginResponse(idpNoEncrypt, 'redirect', { query :{ SAMLResponse, SigAlg: sigAlg, Signature: signature}, octetString });
+ } catch (e) {
+ // it must throw an error
+ t.is(true, true);
+ }
+});
+
+test('avoid malformatted response with simplesign binding', async t => {
+ // sender (caution: only use metadata and public key when declare pair-up in oppoent entity)
+ const user = { email: 'user@email.com' };
+ const { context: SAMLResponse, type, sigAlg, signature, relayState } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idpNoEncrypt, sp, binding.simpleSign, user), undefined, 'relaystate');
+ const rawResponse = String(utility.base64Decode(SAMLResponse, true));
+ const attackResponse = `evil@evil.com${rawResponse}`;
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ try {
+ await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(attackResponse), Signature: signature, SigAlg:sigAlg }, octetString });
+ } catch (e) {
+ // it must throw an error
+ t.is(true, true);
+ }
+});
+
test('should reject signature wrapped response - case 1', async t => {
- //
+ //
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user));
+ const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user));
//Decode
const buffer = Buffer.from(SAMLResponse, 'base64');
const xml = buffer.toString();
@@ -645,9 +1162,9 @@ test('should reject signature wrapped response - case 1', async t => {
});
test('should reject signature wrapped response - case 2', async t => {
- //
+ //
const user = { email: 'user@esaml2.com' };
- const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, user));
+ const { id, context: SAMLResponse } = await idpNoEncrypt.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idpNoEncrypt, sp, binding.post, user));
//Decode
const buffer = Buffer.from(SAMLResponse, 'base64');
const xml = buffer.toString();
@@ -676,6 +1193,26 @@ test('should throw two-tiers code error when the response does not return succes
}
});
+test('should throw two-tiers code error when the response by redirect does not return success status', async t => {
+ try {
+ const SAMLResponse = utility.base64Encode(utility.deflateString(failedResponse));
+ const sigAlg = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
+ const encodedSigAlg = encodeURIComponent(sigAlg);
+ const octetString = "SAMLResponse=" + encodeURIComponent(SAMLResponse) + "&SigAlg=" + encodedSigAlg;
+ const _result = await sp.parseLoginResponse(idpNoEncrypt, 'redirect',{ query :{ SAMLResponse, SigAlg: encodedSigAlg} , octetString} );
+ } catch (e) {
+ t.is(e.message, 'ERR_FAILED_STATUS with top tier code: urn:oasis:names:tc:SAML:2.0:status:Requester, second tier code: urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy');
+ }
+});
+
+test('should throw two-tiers code error when the response over simpleSign does not return success status', async t => {
+ try {
+ const _result = await sp.parseLoginResponse(idpNoEncrypt, 'simpleSign', { body: { SAMLResponse: utility.base64Encode(failedResponse) } });
+ } catch (e) {
+ t.is(e.message, 'ERR_FAILED_STATUS with top tier code: urn:oasis:names:tc:SAML:2.0:status:Requester, second tier code: urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy');
+ }
+});
+
test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response without clock drift setup', async t => {
const now = new Date();
@@ -686,7 +1223,7 @@ test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response
const user = { email: 'user@esaml2.com' };
try {
- const { context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, user));
+ const { context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'post', user, createTemplateCallback(idp, sp, binding.post, user));
// simulate the time on client side when response arrives after 5.1 sec
tk.freeze(fiveMinutesOneSecLater);
await sp.parseLoginResponse(idp, 'post', { body: { SAMLResponse } });
@@ -699,16 +1236,59 @@ test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response
}
});
-test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response with clock drift setup', async t => {
+test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by redirect without clock drift setup', async t => {
const now = new Date();
const fiveMinutesOneSecLater = new Date(now.getTime());
fiveMinutesOneSecLater.setMinutes(fiveMinutesOneSecLater.getMinutes() + 5);
fiveMinutesOneSecLater.setSeconds(fiveMinutesOneSecLater.getSeconds() + 1);
+
+ const user = { email: 'user@esaml2.com' };
+
+ try {
+ const { context: SAMLResponse } = await idp.createLoginResponse(sp, sampleRequestInfo, 'redirect', user, createTemplateCallback(idp, sp, binding.redirect, user), undefined, 'relaystate');
+ // simulate the time on client side when response arrives after 5.1 sec
+ tk.freeze(fiveMinutesOneSecLater);
+ await sp.parseLoginResponse(idp, 'redirect', parseRedirectUrlContextCallBack(SAMLResponse));
+ // test failed, it shouldn't happen
+ t.is(true, false);
+ } catch (e) {
+ t.is(e, 'ERR_SUBJECT_UNCONFIRMED');
+ } finally {
+ tk.reset();
+ }
+});
+
+test.serial('should throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by simpleSign without clock drift setup', async t => {
+
+ const now = new Date();
+ const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000);
+
const user = { email: 'user@esaml2.com' };
try {
- const { context: SAMLResponse } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWithClockDrift, user));
+ const { context: SAMLResponse, type, sigAlg, signature, relayState } = await idp.createLoginResponse(sp, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, sp, binding.simpleSign, user), undefined, 'relaystate');
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ // simulate the time on client side when response arrives after 5.1 sec
+ tk.freeze(fiveMinutesOneSecLater);
+ await sp.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ // test failed, it shouldn't happen
+ t.is(true, false);
+ } catch (e) {
+ t.is(e, 'ERR_SUBJECT_UNCONFIRMED');
+ } finally {
+ tk.reset();
+ }
+});
+
+test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response with clock drift setup', async t => {
+
+ const now = new Date();
+ const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000);
+ const user = { email: 'user@esaml2.com' };
+
+ try {
+ const { context: SAMLResponse } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'post', user, createTemplateCallback(idp, spWithClockDrift, binding.post, user));
// simulate the time on client side when response arrives after 5.1 sec
tk.freeze(fiveMinutesOneSecLater);
await spWithClockDrift.parseLoginResponse(idp, 'post', { body: { SAMLResponse } });
@@ -720,4 +1300,47 @@ test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML respo
tk.reset();
}
-});
\ No newline at end of file
+});
+
+test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by redirect with clock drift setup', async t => {
+
+ const now = new Date();
+ const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000);
+ const user = { email: 'user@esaml2.com' };
+
+ try {
+ const { context: SAMLResponse } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'redirect', user, createTemplateCallback(idp, spWithClockDrift, binding.redirect, user), undefined, '');
+ // simulate the time on client side when response arrives after 5.1 sec
+ tk.freeze(fiveMinutesOneSecLater);
+ await spWithClockDrift.parseLoginResponse(idp, 'redirect', parseRedirectUrlContextCallBack(SAMLResponse));
+ t.is(true, true);
+ } catch (e) {
+ // test failed, it shouldn't happen
+ t.is(e, false);
+ } finally {
+ tk.reset();
+ }
+
+});
+
+test.serial('should not throw ERR_SUBJECT_UNCONFIRMED for the expired SAML response by simpleSign with clock drift setup', async t => {
+
+ const now = new Date();
+ const fiveMinutesOneSecLater = new Date(now.getTime() + 301_000);
+ const user = { email: 'user@esaml2.com' };
+
+ try {
+ const { context: SAMLResponse, type, signature, sigAlg, relayState } = await idp.createLoginResponse(spWithClockDrift, sampleRequestInfo, 'simpleSign', user, createTemplateCallback(idp, spWithClockDrift, binding.simpleSign, user), undefined, 'relaystate');
+ const octetString = buildSimpleSignOctetString(type, SAMLResponse, sigAlg, relayState, signature);
+ // simulate the time on client side when response arrives after 5.1 sec
+ tk.freeze(fiveMinutesOneSecLater);
+ await spWithClockDrift.parseLoginResponse(idp, 'simpleSign', { body: { SAMLResponse, Signature: signature, SigAlg:sigAlg }, octetString });
+ t.is(true, true);
+ } catch (e) {
+ // test failed, it shouldn't happen
+ t.is(e, false);
+ } finally {
+ tk.reset();
+ }
+
+});
diff --git a/test/misc/idpmeta.xml b/test/misc/idpmeta.xml
index e81c9889..af371416 100644
--- a/test/misc/idpmeta.xml
+++ b/test/misc/idpmeta.xml
@@ -28,6 +28,7 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
+
diff --git a/test/misc/idpmeta_nosign.xml b/test/misc/idpmeta_nosign.xml
index a30a4284..457d8b1f 100644
--- a/test/misc/idpmeta_nosign.xml
+++ b/test/misc/idpmeta_nosign.xml
@@ -21,6 +21,7 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
+
diff --git a/test/misc/idpmeta_onelogoutservice.xml b/test/misc/idpmeta_onelogoutservice.xml
index c6d3af95..8b9c18cd 100644
--- a/test/misc/idpmeta_onelogoutservice.xml
+++ b/test/misc/idpmeta_onelogoutservice.xml
@@ -28,6 +28,7 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
+
diff --git a/test/misc/idpmeta_share_cert.xml b/test/misc/idpmeta_share_cert.xml
index 33b0d5b3..74a466c2 100644
--- a/test/misc/idpmeta_share_cert.xml
+++ b/test/misc/idpmeta_share_cert.xml
@@ -21,6 +21,7 @@
urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName
+
diff --git a/test/misc/spmeta.xml b/test/misc/spmeta.xml
index 81068842..13e7c6fc 100644
--- a/test/misc/spmeta.xml
+++ b/test/misc/spmeta.xml
@@ -23,5 +23,7 @@
+
+
diff --git a/test/misc/spmeta_noassertsign.xml b/test/misc/spmeta_noassertsign.xml
index 984917ed..e917b87a 100644
--- a/test/misc/spmeta_noassertsign.xml
+++ b/test/misc/spmeta_noassertsign.xml
@@ -25,8 +25,14 @@
-
+
+
+
diff --git a/test/misc/spmeta_noauthnsign.xml b/test/misc/spmeta_noauthnsign.xml
index 0fc8d2c6..44f88afd 100644
--- a/test/misc/spmeta_noauthnsign.xml
+++ b/test/misc/spmeta_noauthnsign.xml
@@ -18,8 +18,14 @@
-
+
+
+