Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
532 lines
16 KiB
TypeScript
532 lines
16 KiB
TypeScript
import xmldom from '@xmldom/xmldom';
|
|
import { SignedXml } from 'xml-crypto';
|
|
import type { Reference } from 'xml-crypto';
|
|
import xmlenc from 'xml-encryption';
|
|
|
|
import type { ISAMLAssertion } from '../../definition/ISAMLAssertion';
|
|
import type { IServiceProviderOptions } from '../../definition/IServiceProviderOptions';
|
|
import type { SAMLPOSTEnvelope } from '../../definition/SAMLEnvelope';
|
|
import type { IResponseValidateCallback } from '../../definition/callbacks';
|
|
import { SAMLUtils } from '../Utils';
|
|
import { StatusCode } from '../constants';
|
|
|
|
type XmlParent = Element | Document;
|
|
|
|
export class ResponseParser {
|
|
serviceProviderOptions: IServiceProviderOptions;
|
|
|
|
constructor(serviceProviderOptions: IServiceProviderOptions) {
|
|
this.serviceProviderOptions = serviceProviderOptions;
|
|
}
|
|
|
|
public validate(envelope: SAMLPOSTEnvelope<'SAMLResponse'>, callback: IResponseValidateCallback): void {
|
|
const { decodedDocument: xml } = envelope;
|
|
// We currently use RelayState to save SAML provider
|
|
SAMLUtils.log({ msg: 'Validating SAML Response', xml });
|
|
|
|
let error: Error | null = null;
|
|
|
|
const doc = new xmldom.DOMParser({
|
|
errorHandler: {
|
|
fatalError: (e: any) => {
|
|
if (e instanceof Error) {
|
|
error = e;
|
|
return;
|
|
}
|
|
|
|
if (typeof e === 'string') {
|
|
error = new Error(e);
|
|
return;
|
|
}
|
|
|
|
error = new Error();
|
|
},
|
|
error: (e: Error) => {
|
|
if (e instanceof Error) {
|
|
error = e;
|
|
return;
|
|
}
|
|
|
|
if (typeof e === 'string') {
|
|
error = new Error(e);
|
|
return;
|
|
}
|
|
|
|
error = new Error();
|
|
},
|
|
},
|
|
}).parseFromString(xml, 'text/xml');
|
|
if (!doc) {
|
|
return callback('No Doc Found');
|
|
}
|
|
|
|
if (error) {
|
|
return callback(error, null, false);
|
|
}
|
|
|
|
const allResponses = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response');
|
|
if (allResponses.length === 0) {
|
|
return this._checkLogoutResponse(doc, callback);
|
|
}
|
|
|
|
if (allResponses.length !== 1) {
|
|
return callback(new Error('Too many SAML responses'), null, false);
|
|
}
|
|
const response = allResponses[0];
|
|
|
|
if (response !== doc.documentElement) {
|
|
return callback(new Error('SAML Response must be the document root'), null, false);
|
|
}
|
|
|
|
SAMLUtils.log('Got response');
|
|
|
|
SAMLUtils.log('Verify status');
|
|
const statusValidateObj = SAMLUtils.validateStatus(doc);
|
|
if (!statusValidateObj.success) {
|
|
if (!statusValidateObj.statusCode) {
|
|
return callback(new Error('Missing StatusCode'), null, false);
|
|
}
|
|
|
|
if (statusValidateObj.statusCode === StatusCode.responder && statusValidateObj.message) {
|
|
return callback(new Error(statusValidateObj.message), null, false);
|
|
}
|
|
|
|
return callback(new Error(`Status is: ${statusValidateObj.statusCode}`), null, false);
|
|
}
|
|
SAMLUtils.log('Status ok');
|
|
|
|
let assertion: Element;
|
|
let assertionData: ISAMLAssertion;
|
|
let issuer;
|
|
|
|
try {
|
|
assertionData = this.getAssertion(response, xml);
|
|
assertion = assertionData.assertion;
|
|
|
|
this.verifySignatures(response, assertionData, xml);
|
|
} catch (e) {
|
|
return callback(e instanceof Error ? e : String(e), null, false);
|
|
}
|
|
|
|
const profile: Record<string, any> = {};
|
|
|
|
if (response.hasAttribute('InResponseTo')) {
|
|
profile.inResponseToId = response.getAttribute('InResponseTo');
|
|
}
|
|
|
|
try {
|
|
issuer = this.getIssuer(assertion);
|
|
} catch (e) {
|
|
return callback(e instanceof Error ? e : String(e), null, false);
|
|
}
|
|
|
|
if (issuer) {
|
|
profile.issuer = issuer.textContent;
|
|
}
|
|
|
|
if (assertion.hasAttribute('ID')) {
|
|
profile.assertionId = assertion.getAttribute('ID');
|
|
}
|
|
|
|
const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0];
|
|
if (conditions?.hasAttribute('NotOnOrAfter')) {
|
|
const notOnOrAfter = conditions.getAttribute('NotOnOrAfter');
|
|
if (notOnOrAfter) {
|
|
profile.expireAt = new Date(notOnOrAfter);
|
|
}
|
|
}
|
|
|
|
const subject = this.getSubject(assertion);
|
|
if (subject) {
|
|
const nameID = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'NameID')[0];
|
|
if (nameID) {
|
|
profile.nameID = nameID.textContent;
|
|
|
|
if (nameID.hasAttribute('Format')) {
|
|
profile.nameIDFormat = nameID.getAttribute('Format');
|
|
}
|
|
}
|
|
|
|
try {
|
|
this.validateSubjectConditions(subject);
|
|
} catch (e) {
|
|
return callback(e instanceof Error ? e : String(e), null, false);
|
|
}
|
|
}
|
|
|
|
try {
|
|
this.validateAssertionConditions(assertion);
|
|
} catch (e) {
|
|
return callback(e instanceof Error ? e : String(e), null, false);
|
|
}
|
|
|
|
const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0];
|
|
|
|
if (authnStatement) {
|
|
if (authnStatement.hasAttribute('SessionIndex')) {
|
|
profile.sessionIndex = authnStatement.getAttribute('SessionIndex');
|
|
SAMLUtils.log({ msg: 'Session Index Found', sessionIndex: profile.sessionIndex });
|
|
} else {
|
|
SAMLUtils.log('No Session Index Found');
|
|
}
|
|
} else {
|
|
SAMLUtils.log('No AuthN Statement found');
|
|
}
|
|
|
|
const attributeStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AttributeStatement')[0];
|
|
if (attributeStatement) {
|
|
this.mapAttributes(attributeStatement, profile);
|
|
} else {
|
|
SAMLUtils.log('No Attribute Statement found in SAML response.');
|
|
}
|
|
|
|
if (!profile.email && profile.nameID && profile.nameIDFormat && profile.nameIDFormat.indexOf('emailAddress') >= 0) {
|
|
profile.email = profile.nameID;
|
|
}
|
|
|
|
const profileKeys = Object.keys(profile);
|
|
for (let i = 0; i < profileKeys.length; i++) {
|
|
const key = profileKeys[i];
|
|
|
|
if (key.match(/\./)) {
|
|
profile[key.replace(/\./g, '-')] = profile[key];
|
|
delete profile[key];
|
|
}
|
|
}
|
|
|
|
SAMLUtils.log({ msg: 'NameID', profile });
|
|
return callback(null, profile, false);
|
|
}
|
|
|
|
private _checkLogoutResponse(doc: Document, callback: IResponseValidateCallback): void {
|
|
const logoutResponse = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse');
|
|
if (!logoutResponse.length) {
|
|
return callback(new Error('Unknown SAML response message'), null, false);
|
|
}
|
|
|
|
SAMLUtils.log('Verify status');
|
|
const statusValidateObj = SAMLUtils.validateStatus(doc);
|
|
if (!statusValidateObj.success) {
|
|
return callback(new Error(`Status is: ${statusValidateObj.statusCode}`), null, false);
|
|
}
|
|
SAMLUtils.log('Status ok');
|
|
|
|
// @ToDo: Check if this situation is still used
|
|
return callback(null, null, true);
|
|
}
|
|
|
|
private getAssertion(response: Element, xml: string): ISAMLAssertion {
|
|
const allAssertions = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion');
|
|
const allEncrypedAssertions = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedAssertion');
|
|
|
|
if (allAssertions.length + allEncrypedAssertions.length > 1) {
|
|
throw new Error('Too many SAML assertions');
|
|
}
|
|
|
|
let assertion: Element = allAssertions[0];
|
|
const encAssertion = allEncrypedAssertions[0];
|
|
let newXml = null;
|
|
|
|
if (typeof encAssertion !== 'undefined') {
|
|
// disallowDecryptionWithInsecureAlgorithm defaults to true in xml-encryption v4, but AES-CBC/3DES
|
|
// are still widely used by SAML IdPs in practice, so we keep the pre-v4 behaviour here.
|
|
const options = { key: this.serviceProviderOptions.privateKey, disallowDecryptionWithInsecureAlgorithm: false };
|
|
const encData = encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0];
|
|
xmlenc.decrypt(encData, options, (err, result) => {
|
|
if (err) {
|
|
SAMLUtils.error(err);
|
|
}
|
|
|
|
const document = new xmldom.DOMParser().parseFromString(result, 'text/xml');
|
|
if (!document) {
|
|
throw new Error('Failed to decrypt SAML assertion');
|
|
}
|
|
|
|
const decryptedAssertions = document.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion');
|
|
if (decryptedAssertions.length) {
|
|
assertion = decryptedAssertions[0];
|
|
}
|
|
|
|
newXml = result;
|
|
});
|
|
}
|
|
|
|
if (!assertion) {
|
|
throw new Error('Missing SAML assertion');
|
|
}
|
|
|
|
return {
|
|
assertion,
|
|
xml: newXml || xml,
|
|
};
|
|
}
|
|
|
|
private verifySignatures(response: Element, assertionData: ISAMLAssertion, xml: string): void {
|
|
const signatureType = this.serviceProviderOptions.signatureValidationType;
|
|
|
|
const checkEither = signatureType === 'Either';
|
|
const checkResponse = signatureType === 'Response' || signatureType === 'All' || checkEither;
|
|
const checkAssertion = signatureType === 'Assertion' || signatureType === 'All' || checkEither;
|
|
let anyValidSignature = false;
|
|
|
|
if (!this.serviceProviderOptions.cert) {
|
|
if (checkResponse || checkAssertion) {
|
|
SAMLUtils.log('Missing Signature validation params');
|
|
throw new Error('Unable to validate signature');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (checkResponse) {
|
|
SAMLUtils.log('Verify Document Signature');
|
|
if (!this.validateResponseSignature(xml, this.serviceProviderOptions.cert, response)) {
|
|
if (!checkEither) {
|
|
SAMLUtils.log('Document Signature WRONG');
|
|
throw new Error('Invalid Signature');
|
|
}
|
|
} else {
|
|
anyValidSignature = true;
|
|
}
|
|
SAMLUtils.log('Document Signature OK');
|
|
}
|
|
|
|
if (checkAssertion) {
|
|
SAMLUtils.log('Verify Assertion Signature');
|
|
if (!this.validateAssertionSignature(assertionData.xml, this.serviceProviderOptions.cert, assertionData.assertion)) {
|
|
if (!checkEither) {
|
|
SAMLUtils.log('Assertion Signature WRONG');
|
|
throw new Error('Invalid Assertion signature');
|
|
}
|
|
} else {
|
|
anyValidSignature = true;
|
|
}
|
|
SAMLUtils.log('Assertion Signature OK');
|
|
}
|
|
|
|
if (checkEither && !anyValidSignature) {
|
|
SAMLUtils.log('No Valid Signature');
|
|
throw new Error('No valid SAML Signature found');
|
|
}
|
|
}
|
|
|
|
private validateResponseSignature(xml: string, cert: string, response: Element): boolean {
|
|
return this.validateSignatureChildren(xml, cert, response);
|
|
}
|
|
|
|
private validateAssertionSignature(xml: string, cert: string, assertion: Element): boolean {
|
|
return this.validateSignatureChildren(xml, cert, assertion);
|
|
}
|
|
|
|
private validateSignatureChildren(xml: string, cert: string, parent: Element): boolean {
|
|
const signatures = parent.getElementsByTagNameNS('http://www.w3.org/2000/09/xmldsig#', 'Signature');
|
|
let signature = null;
|
|
|
|
for (let i = 0; i < signatures.length; i++) {
|
|
const sign = signatures[i];
|
|
if (sign.parentNode !== parent) {
|
|
continue;
|
|
}
|
|
|
|
// Too many signatures
|
|
if (signature) {
|
|
SAMLUtils.log('Failed to validate SAML signature: Too Many Signatures');
|
|
return false;
|
|
}
|
|
|
|
signature = sign;
|
|
}
|
|
|
|
if (!signature) {
|
|
SAMLUtils.log('Failed to validate SAML signature: Signature not found');
|
|
return false;
|
|
}
|
|
|
|
return this.validateSignature(xml, cert, signature, this.getElementId(parent));
|
|
}
|
|
|
|
private getElementId(element: Element): string | null {
|
|
return element.getAttribute('ID') || element.getAttribute('Id') || element.getAttribute('id');
|
|
}
|
|
|
|
private signatureCoversElement(sig: SignedXml, expectedId: string | null): boolean {
|
|
if (!expectedId) {
|
|
return false;
|
|
}
|
|
|
|
const references: Reference[] = sig.getReferences() || [];
|
|
if (references.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return references.every((reference: Reference) => {
|
|
// `signedReference` is only populated for references that were part of the successfully
|
|
// validated signature, so its presence proves this reference was actually covered.
|
|
if (reference.signedReference == null) {
|
|
return false;
|
|
}
|
|
|
|
const uri = reference.uri || '';
|
|
const referencedId = uri.charAt(0) === '#' ? uri.substring(1) : uri;
|
|
return referencedId === expectedId;
|
|
});
|
|
}
|
|
|
|
private validateSignature(xml: string, cert: string, signature: Element, expectedId: string | null): boolean {
|
|
const sig = new SignedXml({ publicCert: Buffer.from(SAMLUtils.certToPEM(cert)) });
|
|
|
|
sig.loadSignature(signature);
|
|
|
|
try {
|
|
if (!sig.checkSignature(xml)) {
|
|
SAMLUtils.log(sig.getReferences().map((reference) => reference.validationError?.message));
|
|
return false;
|
|
}
|
|
} catch (e) {
|
|
SAMLUtils.log(e instanceof Error ? e.message : String(e));
|
|
return false;
|
|
}
|
|
|
|
if (!this.signatureCoversElement(sig, expectedId)) {
|
|
SAMLUtils.log('Failed to validate SAML signature: signed reference does not match the processed element');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private getIssuer(assertion: Element): any {
|
|
const issuers = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer');
|
|
if (issuers.length > 1) {
|
|
throw new Error('Too many Issuers');
|
|
}
|
|
|
|
return issuers[0];
|
|
}
|
|
|
|
private getSubject(assertion: Element): XmlParent {
|
|
let subject: XmlParent = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0];
|
|
const encSubject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedID')[0];
|
|
|
|
if (typeof encSubject !== 'undefined') {
|
|
const options = { key: this.serviceProviderOptions.privateKey, disallowDecryptionWithInsecureAlgorithm: false };
|
|
xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, (err, result) => {
|
|
if (err) {
|
|
SAMLUtils.error({ err });
|
|
}
|
|
subject = new xmldom.DOMParser().parseFromString(result, 'text/xml');
|
|
});
|
|
}
|
|
|
|
return subject;
|
|
}
|
|
|
|
private validateSubjectConditions(subject: XmlParent): void {
|
|
const subjectConfirmation = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmation')[0];
|
|
if (subjectConfirmation) {
|
|
const subjectConfirmationData = subjectConfirmation.getElementsByTagNameNS(
|
|
'urn:oasis:names:tc:SAML:2.0:assertion',
|
|
'SubjectConfirmationData',
|
|
)[0];
|
|
if (subjectConfirmationData && !this.validateNotBeforeNotOnOrAfterAssertions(subjectConfirmationData)) {
|
|
throw new Error('NotBefore / NotOnOrAfter assertion failed');
|
|
}
|
|
}
|
|
}
|
|
|
|
private validateNotBeforeNotOnOrAfterAssertions(element: Element): boolean {
|
|
const sysnow = new Date();
|
|
const allowedclockdrift = this.serviceProviderOptions.allowedClockDrift || 0;
|
|
|
|
const now = new Date(sysnow.getTime() + allowedclockdrift);
|
|
|
|
if (element.hasAttribute('NotBefore')) {
|
|
const notBefore: string | null = element.getAttribute('NotBefore');
|
|
|
|
if (!notBefore) {
|
|
return false;
|
|
}
|
|
|
|
const date = new Date(notBefore);
|
|
if (now < date) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (element.hasAttribute('NotOnOrAfter')) {
|
|
const notOnOrAfter: string | null = element.getAttribute('NotOnOrAfter');
|
|
if (!notOnOrAfter) {
|
|
return false;
|
|
}
|
|
|
|
const date = new Date(notOnOrAfter);
|
|
|
|
if (now >= date) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private validateAssertionConditions(assertion: Element): void {
|
|
const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0];
|
|
if (conditions && !this.validateNotBeforeNotOnOrAfterAssertions(conditions)) {
|
|
throw new Error('NotBefore / NotOnOrAfter assertion failed');
|
|
}
|
|
}
|
|
|
|
private mapAttributes(attributeStatement: Element, profile: Record<string, any>): void {
|
|
SAMLUtils.log({ msg: 'Attribute Statement found, mapping attributes to profile.', attributeStatement });
|
|
const attributes = attributeStatement.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Attribute');
|
|
SAMLUtils.log({ msg: 'Attributes will be processed', count: attributes.length });
|
|
|
|
if (attributes) {
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
const values = attributes[i].getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AttributeValue');
|
|
let value;
|
|
if (values.length === 1) {
|
|
value = values[0].textContent;
|
|
} else {
|
|
value = [];
|
|
for (let j = 0; j < values.length; j++) {
|
|
value.push(values[j].textContent);
|
|
}
|
|
}
|
|
|
|
const key = attributes[i].getAttribute('Name');
|
|
if (key) {
|
|
SAMLUtils.log({ msg: 'Mapping attribute to profile', attribute: key, value });
|
|
profile[key] = value;
|
|
}
|
|
}
|
|
} else {
|
|
SAMLUtils.log('No Attributes found in SAML attribute statement.');
|
|
}
|
|
|
|
if (!profile.mail && profile['urn:oid:0.9.2342.19200300.100.1.3']) {
|
|
// See http://www.incommonfederation.org/attributesummary.html for definition of attribute OIDs
|
|
profile.mail = profile['urn:oid:0.9.2342.19200300.100.1.3'];
|
|
}
|
|
|
|
if (!profile.email && profile['urn:oid:1.2.840.113549.1.9.1']) {
|
|
profile.email = profile['urn:oid:1.2.840.113549.1.9.1'];
|
|
}
|
|
|
|
if (!profile.displayName && profile['urn:oid:2.16.840.1.113730.3.1.241']) {
|
|
profile.displayName = profile['urn:oid:2.16.840.1.113730.3.1.241'];
|
|
}
|
|
|
|
if (!profile.eppn && profile['urn:oid:1.3.6.1.4.1.5923.1.1.1.6']) {
|
|
profile.eppn = profile['urn:oid:1.3.6.1.4.1.5923.1.1.1.6'];
|
|
}
|
|
|
|
if (!profile.email && profile.mail) {
|
|
profile.email = profile.mail;
|
|
}
|
|
|
|
if (!profile.cn && profile['urn:oid:2.5.4.3']) {
|
|
profile.cn = profile['urn:oid:2.5.4.3'];
|
|
}
|
|
}
|
|
}
|