Skip to content

Commit a11ad61

Browse files
rob-gijsensRob Gijsensmarkstos
authored
feat(authorize-request): idp scoping provider (#428)
* feat(scoping): add scoping to authnrequest * Correct case. Co-authored-by: Rob Gijsens <[email protected]> Co-authored-by: Mark Stosberg <[email protected]>
1 parent 881208b commit a11ad61

File tree

4 files changed

+540
-4
lines changed

4 files changed

+540
-4
lines changed

README.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Passport-SAML has been tested to work with Onelogin, Okta, Shibboleth, [SimpleSA
1111
## Installation
1212

1313
$ npm install passport-saml
14-
14+
/
1515
## Usage
1616

1717
The examples utilize the [Feide OpenIdp identity provider](https://openidp.feide.no/). You need an account there to log in with this. You also need to [register your site](https://openidp.feide.no/simplesaml/module.php/metaedit/index.php) as a service provider.
@@ -134,6 +134,23 @@ type Profile = {
134134
* `skipRequestCompression`: if set to true, the SAML request from the service provider won't be compressed.
135135
* `authnRequestBinding`: if set to `HTTP-POST`, will request authentication from IDP via HTTP POST binding, otherwise defaults to HTTP Redirect
136136
* `disableRequestACSUrl`: if truthy, SAML AuthnRequest from the service provider will not include the optional AssertionConsumerServiceURL. Default is falsy so it is automatically included.
137+
* `scoping`: An optional configuration which implements the functionality [explained in the SAML spec paragraph "3.4.1.2 Element <Scoping>"](https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf). The config object is structured as following:
138+
```javascript
139+
{
140+
idpList: { // optional
141+
entries: [ // required
142+
{
143+
providerId: 'yourProviderId', // required for each entry
144+
name: 'yourName', // optional
145+
loc: 'yourLoc', // optional
146+
}
147+
],
148+
getComplete: 'URI to your complete IDP list', // optional
149+
},
150+
proxyCount: 2, // optional
151+
requesterId: 'requesterId', // optional
152+
}
153+
```
137154
* **InResponseTo Validation**
138155
* `validateInResponseTo`: if truthy, then InResponseTo will be validated from incoming SAML responses
139156
* `requestIdExpirationPeriodMs`: Defines the expiration time when a Request ID generated for a SAML request will not be valid if seen in a SAML response in the `InResponseTo` field. Default is 8 hours.

src/passport-saml/saml.ts

+51
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {CacheProvider as InMemoryCacheProvider} from './inmemory-cache-provider'
1414
import * as algorithms from './algorithms';
1515
import { signAuthnRequestPost } from './saml-post-signing';
1616
import type { Request } from 'express';
17+
import { SamlIDPEntryConfig, SamlIDPListConfig } from './types';
1718

1819
function processValidlySignedPostRequest(self: SAML, doc, dom, callback) {
1920
const request = doc.LogoutRequest;
@@ -305,6 +306,56 @@ class SAML {
305306
request['samlp:AuthnRequest']['@ProviderName'] = this.options.providerName;
306307
}
307308

309+
if (this.options.scoping) {
310+
const scoping = {
311+
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
312+
};
313+
314+
if (typeof this.options.scoping.proxyCount === 'number') {
315+
scoping['@ProxyCount'] = this.options.scoping.proxyCount;
316+
}
317+
318+
if (this.options.scoping.idpList) {
319+
scoping['samlp:IDPList'] = this.options.scoping.idpList.map((idpListItem: SamlIDPListConfig) => {
320+
const formattedIdpListItem = {
321+
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
322+
};
323+
324+
if (idpListItem.entries) {
325+
formattedIdpListItem['samlp:IDPEntry'] = idpListItem.entries.map((entry: SamlIDPEntryConfig) => {
326+
const formattedEntry = {
327+
'@xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
328+
};
329+
330+
formattedEntry['@ProviderID'] = entry.providerId;
331+
332+
if (entry.name) {
333+
formattedEntry['@Name'] = entry.name;
334+
}
335+
336+
if (entry.loc) {
337+
formattedEntry['@Loc'] = entry.loc;
338+
}
339+
340+
return formattedEntry;
341+
});
342+
}
343+
344+
if (idpListItem.getComplete) {
345+
formattedIdpListItem['samlp:GetComplete'] = idpListItem.getComplete;
346+
}
347+
348+
return formattedIdpListItem;
349+
});
350+
}
351+
352+
if (this.options.scoping.requesterId) {
353+
scoping['samlp:RequesterID'] = this.options.scoping.requesterId;
354+
}
355+
356+
request['samlp:AuthnRequest']['samlp:Scoping'] = scoping;
357+
}
358+
308359
let stringRequest = xmlbuilder.create(request).end();
309360
if (isHttpPostBinding && this.options.privateCert) {
310361
stringRequest = signAuthnRequestPost(stringRequest, this.options);

src/passport-saml/types.ts

+18
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface SamlConfig {
4141
passive?: boolean;
4242
idpIssuer?: string;
4343
audience?: string;
44+
scoping? : SamlScopingConfig;
4445

4546
// InResponseTo Validation
4647
validateInResponseTo?: boolean;
@@ -57,6 +58,23 @@ export interface SamlConfig {
5758
logoutCallbackUrl?: string;
5859
}
5960

61+
export interface SamlScopingConfig {
62+
idpList: SamlIDPListConfig[];
63+
proxyCount?: number;
64+
requesterId?: string[];
65+
}
66+
67+
export interface SamlIDPListConfig {
68+
entries: SamlIDPEntryConfig[];
69+
getComplete?: string;
70+
}
71+
72+
export interface SamlIDPEntryConfig {
73+
providerId: string;
74+
name?: string;
75+
loc?: string;
76+
}
77+
6078
export type Profile = {
6179
issuer?: string;
6280
sessionIndex?: string;

0 commit comments

Comments
 (0)