@bostonuniversity/shibboleth-sp 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/cjs/index.js +63195 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/esm/Config.js +25 -0
- package/dist/esm/Config.js.map +1 -0
- package/dist/esm/HandlerSp.js +229 -0
- package/dist/esm/HandlerSp.js.map +1 -0
- package/dist/esm/Http.js +83 -0
- package/dist/esm/Http.js.map +1 -0
- package/dist/esm/Jwt.js +186 -0
- package/dist/esm/Jwt.js.map +1 -0
- package/dist/esm/Keys.js +82 -0
- package/dist/esm/Keys.js.map +1 -0
- package/dist/esm/Saml.js +235 -0
- package/dist/esm/Saml.js.map +1 -0
- package/dist/esm/Utils.js +138 -0
- package/dist/esm/Utils.js.map +1 -0
- package/dist/esm/index.js +63177 -0
- package/dist/esm/index.js.map +7 -0
- package/dist/types/Config.d.ts +28 -0
- package/dist/types/HandlerSp.d.ts +20 -0
- package/dist/types/Http.d.ts +72 -0
- package/dist/types/Jwt.d.ts +59 -0
- package/dist/types/Keys.d.ts +13 -0
- package/dist/types/Saml.d.ts +73 -0
- package/dist/types/Utils.d.ts +40 -0
- package/dist/types/index.d.ts +8 -0
- package/package.json +49 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { KeyValuePair } from "./Http";
|
|
2
|
+
import { SamlParms } from "./Saml";
|
|
3
|
+
/**
|
|
4
|
+
* A type that accounts for ALL configurable values.
|
|
5
|
+
*
|
|
6
|
+
* @property domain - The domain for app requests (browser access domain)
|
|
7
|
+
* @property appLoginHeader - Header name for login redirects
|
|
8
|
+
* @property appLogoutHeader - Header name for logout redirects
|
|
9
|
+
* @property appAuthorization - True for standard mode, false for basic mode
|
|
10
|
+
* @property samlParms - SAML configuration parameters
|
|
11
|
+
* @property jwtPrivateKeyPEM - Optional private key for JWT generation
|
|
12
|
+
* @property jwtPublicKeyPEM - Optional public key for JWT generation
|
|
13
|
+
* @property customHeaders - Optional custom headers to append to requests
|
|
14
|
+
*/
|
|
15
|
+
export interface IConfig {
|
|
16
|
+
domain: string;
|
|
17
|
+
appLoginHeader: string;
|
|
18
|
+
appLogoutHeader: string;
|
|
19
|
+
appAuthorization: boolean;
|
|
20
|
+
samlParms: SamlParms;
|
|
21
|
+
jwtPrivateKeyPEM?: string;
|
|
22
|
+
jwtPublicKeyPEM?: string;
|
|
23
|
+
customHeaders?: KeyValuePair[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* @returns An instance of IConfig whose values are ALL obtained from the environment.
|
|
27
|
+
*/
|
|
28
|
+
export declare const getConfigFromEnvironment: () => IConfig;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IConfig } from './Config';
|
|
2
|
+
import { IRequest, IResponse } from './Http';
|
|
3
|
+
export declare enum AUTH_PATHS {
|
|
4
|
+
LOGIN = "/login",
|
|
5
|
+
LOGOUT = "/logout",
|
|
6
|
+
ASSERT = "/assert",
|
|
7
|
+
METADATA = "/metadata",
|
|
8
|
+
FAVICON = "/favicon.ico"
|
|
9
|
+
}
|
|
10
|
+
export declare const APP_AUTHORIZATION_HEADER_NAME = "app_authorization";
|
|
11
|
+
/**
|
|
12
|
+
* This is the sp "switchboard operator" for origin request traffic. It will perform all saml SP operations for ensuring
|
|
13
|
+
* that the user bears JWT proof of saml authentication, else it drives the authentication flow with the IDP.
|
|
14
|
+
* @param event
|
|
15
|
+
* @returns Promise<IRequest|IResponse>
|
|
16
|
+
* IRequest: Returning the original request (possibly with headers added) - indicates the request is to be
|
|
17
|
+
* forwarded on to the target app.
|
|
18
|
+
* IResponse: Returning an object that indicates an error or redirection - not passing through to the target app.
|
|
19
|
+
*/
|
|
20
|
+
export declare const handler: (originRequest: IRequest, config: IConfig) => Promise<IRequest | IResponse>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for an incoming request.
|
|
3
|
+
*
|
|
4
|
+
* @remarks This was modelled after a Lambda@Edge request event, but that is arbitrary - this app is agnostic
|
|
5
|
+
* as to which environment it will be run in (such as cloudfront edge lambda), docker compose, etc.
|
|
6
|
+
*
|
|
7
|
+
* @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-request
|
|
8
|
+
*/
|
|
9
|
+
export interface IRequest {
|
|
10
|
+
uri: string;
|
|
11
|
+
method: string;
|
|
12
|
+
querystring: string;
|
|
13
|
+
clientIp?: string;
|
|
14
|
+
status?: string;
|
|
15
|
+
body: RequestBody;
|
|
16
|
+
headers: RequestHeaders;
|
|
17
|
+
headerActivity: {
|
|
18
|
+
added: RequestHeaders;
|
|
19
|
+
removed: RequestHeaders;
|
|
20
|
+
modified: RequestHeaders;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Interface for an outgoing response.
|
|
25
|
+
*
|
|
26
|
+
* @remarks This was modelled after a Lambda@Edge response event, but that is arbitrary - this app is agnostic
|
|
27
|
+
* as to which environment it will be run in (such as cloudfront edge lambda), docker compose, etc.
|
|
28
|
+
*
|
|
29
|
+
* @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#lambda-event-structure-response
|
|
30
|
+
*/
|
|
31
|
+
export interface IResponse {
|
|
32
|
+
status: string;
|
|
33
|
+
statusDescription: string;
|
|
34
|
+
body?: string;
|
|
35
|
+
headers?: RequestHeaders;
|
|
36
|
+
}
|
|
37
|
+
export interface RequestBody {
|
|
38
|
+
data: string;
|
|
39
|
+
}
|
|
40
|
+
export type RequestHeaders = {
|
|
41
|
+
[headerName: string]: KeyValuePair[];
|
|
42
|
+
};
|
|
43
|
+
export interface KeyValuePair {
|
|
44
|
+
key: string;
|
|
45
|
+
value: string;
|
|
46
|
+
}
|
|
47
|
+
export type IHeaders = {
|
|
48
|
+
empty: boolean;
|
|
49
|
+
get: (key: string) => string | null;
|
|
50
|
+
join: (joiner: string, except?: string[]) => string;
|
|
51
|
+
isTruthy: (key: string) => boolean;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Header accessor wrapper.
|
|
55
|
+
* @param request
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
export declare const Headers: (request: IRequest | IResponse) => IHeaders;
|
|
59
|
+
/**
|
|
60
|
+
* Utility function for adding headers to a request or response
|
|
61
|
+
* @param obj
|
|
62
|
+
* @param keyname
|
|
63
|
+
* @param value
|
|
64
|
+
* @returns
|
|
65
|
+
*/
|
|
66
|
+
export declare const addHeader: (obj: IRequest | IResponse, keyname: string, value: string) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Extracts the host header value from an HTTP request.
|
|
69
|
+
* @param request The HTTP request object
|
|
70
|
+
* @returns The host header value, or null if not found or invalid request
|
|
71
|
+
*/
|
|
72
|
+
export declare const getHost: (request: IRequest) => string | null;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IRequest } from './Http';
|
|
2
|
+
/**
|
|
3
|
+
* Use the jsonwebtoken library to create and verify jwts.
|
|
4
|
+
*
|
|
5
|
+
* @see https://www.npmjs.com/package/jsonwebtoken
|
|
6
|
+
*/
|
|
7
|
+
export declare class JwtTools {
|
|
8
|
+
private publicKey;
|
|
9
|
+
private privateKey;
|
|
10
|
+
private expiresIn;
|
|
11
|
+
private clockTime;
|
|
12
|
+
static TOKEN_NAME: string;
|
|
13
|
+
static COOKIE_NAME: string;
|
|
14
|
+
resetPrivateKey(privateKey: string): void;
|
|
15
|
+
resetPublicKey(publicKey: string): void;
|
|
16
|
+
setExpiration(expiresIn: string): void;
|
|
17
|
+
setClockTime(clockTime: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* The cookie header is a concatenation of name=value pairs delimited by a ";" character.
|
|
20
|
+
* Get the value of one of these pairs, specified by name.
|
|
21
|
+
* @param request The HTTP request containing cookies
|
|
22
|
+
* @param cookieName The name of the cookie to retrieve
|
|
23
|
+
* @returns The cookie value if found, undefined otherwise
|
|
24
|
+
*/
|
|
25
|
+
parseCookieValue(request: IRequest, cookieName: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Any environment variable starting with "IMPERSONATE_" is treated as an attribute for building a jwt token.
|
|
28
|
+
* This provides an option for mocking a jwt while running locally for testing so as to "pretend" a valid jwt
|
|
29
|
+
* was provided with the http request.
|
|
30
|
+
* @example
|
|
31
|
+
* IMPERSONATE_buPrincipal=wrh
|
|
32
|
+
* IMPERSONATE_eduPersonPrincipalName=wrh@bu.edu
|
|
33
|
+
* IMPERSONATE_eduPersonAffiliation=employee|member|staff
|
|
34
|
+
* @returns JWT token object with impersonation attributes, or null if no IMPERSONATE_ environment variables are found
|
|
35
|
+
*/
|
|
36
|
+
getImpersonationToken(): any;
|
|
37
|
+
/**
|
|
38
|
+
* Extract the decoded JWT from the larger cookie value.
|
|
39
|
+
* @param request
|
|
40
|
+
* @param verify
|
|
41
|
+
* @returns
|
|
42
|
+
*/
|
|
43
|
+
getToken(request: IRequest, verify?: boolean): any;
|
|
44
|
+
/**
|
|
45
|
+
* Determine if a request carries a valid jwt in its cookie header.
|
|
46
|
+
* @param request
|
|
47
|
+
* @returns
|
|
48
|
+
*/
|
|
49
|
+
hasValidToken(request: IRequest): any;
|
|
50
|
+
/**
|
|
51
|
+
* Place a jwt in a "set-cookie" response header.
|
|
52
|
+
* @param response
|
|
53
|
+
* @param payload
|
|
54
|
+
*/
|
|
55
|
+
setCookieInResponse(response: any, payload: any): void;
|
|
56
|
+
setCookieInvalidationInResponse(response: any): void;
|
|
57
|
+
getCookieName(): string;
|
|
58
|
+
getTokenName(): string;
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class Keys {
|
|
2
|
+
private _keys;
|
|
3
|
+
private _certificate;
|
|
4
|
+
constructor();
|
|
5
|
+
private extractValueFromPEM;
|
|
6
|
+
get privateKey(): string;
|
|
7
|
+
get publicKey(): string;
|
|
8
|
+
get certificate(): string;
|
|
9
|
+
get privateKeyPEM(): string;
|
|
10
|
+
get publicKeyPEM(): string;
|
|
11
|
+
get certificatePEM(): string;
|
|
12
|
+
toString: () => string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SAMLAssertResponse } from 'saml2-js';
|
|
2
|
+
import { IRequest } from './Http';
|
|
3
|
+
/**
|
|
4
|
+
* Parameters required to configure the SAML tools.
|
|
5
|
+
*
|
|
6
|
+
* @property entityId - The entity ID of your app known to shibboleth (e.g., 'https://*.myapp.bu.edu/shibboleth')
|
|
7
|
+
* @property entryPoint - The IDP address for SSO (e.g., 'https://shib-test.bu.edu/idp/profile/SAML2/Redirect/SSO')
|
|
8
|
+
* @property logoutUrl - The IDP logout URL (e.g., 'https://shib.bu.edu/idp/logout.jsp')
|
|
9
|
+
* @property idpCert - The public certificate from the IDP's X509Certificate element
|
|
10
|
+
* @property cert - Optional SAML certificate for your service provider metadata
|
|
11
|
+
* @property key - Optional private key for your service provider metadata
|
|
12
|
+
*/
|
|
13
|
+
export interface SamlParms {
|
|
14
|
+
entityId: string;
|
|
15
|
+
entryPoint: string;
|
|
16
|
+
logoutUrl: string;
|
|
17
|
+
idpCert: string;
|
|
18
|
+
cert?: string;
|
|
19
|
+
key?: string;
|
|
20
|
+
}
|
|
21
|
+
export type SamlResponseObject = {
|
|
22
|
+
samlResponseParm: string | undefined;
|
|
23
|
+
xmlData: string | undefined;
|
|
24
|
+
relayState: string | undefined;
|
|
25
|
+
};
|
|
26
|
+
export type SendAssertResult = {
|
|
27
|
+
samlAssertResponse: SAMLAssertResponse | null;
|
|
28
|
+
relayState: string | null;
|
|
29
|
+
};
|
|
30
|
+
export declare class SamlTools {
|
|
31
|
+
private sp_options;
|
|
32
|
+
private idp_options;
|
|
33
|
+
constructor(parms: SamlParms);
|
|
34
|
+
setSpCertificate(cert: string): void;
|
|
35
|
+
setPrivateKey(key: string): void;
|
|
36
|
+
setAssertUrl(assertEndpoint: string): void;
|
|
37
|
+
getMetaData(): string;
|
|
38
|
+
/**
|
|
39
|
+
* The request indicates the user is not authenticated, and so needs to be redirected to a url for signin with the IDP.
|
|
40
|
+
*
|
|
41
|
+
* @see https://www.npmjs.com/package/saml2-js#create_login_request_urlidp-options-cb
|
|
42
|
+
* @param relay_state
|
|
43
|
+
* @returns
|
|
44
|
+
*/
|
|
45
|
+
createLoginRequestUrl(relay_state: string): Promise<string>;
|
|
46
|
+
/**
|
|
47
|
+
* @returns The url to the IDP to logout.
|
|
48
|
+
*/
|
|
49
|
+
createLogoutRequestUrl(): Promise<string>;
|
|
50
|
+
/**
|
|
51
|
+
* The saml response could come from the IDP as either a querystring parameter (GET), or as a
|
|
52
|
+
* form data parameter (POST).
|
|
53
|
+
* @param request
|
|
54
|
+
* @returns
|
|
55
|
+
*/
|
|
56
|
+
getSamlResponseParameter(request: IRequest): SamlResponseObject | null;
|
|
57
|
+
/**
|
|
58
|
+
* The request has a SAMLResponse parameter and so indicates authentication has completed and a followup
|
|
59
|
+
* assert against the SAMLResponse parameter is next.
|
|
60
|
+
*
|
|
61
|
+
* @see https://www.npmjs.com/package/saml2-js#create_login_request_urlidp-options-cb
|
|
62
|
+
* @param requestUrl
|
|
63
|
+
* @returns
|
|
64
|
+
*/
|
|
65
|
+
sendAssert(request: IRequest): Promise<SendAssertResult>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The saml assert response will have attributes that are missing the friendly names.
|
|
69
|
+
* These friendly names can still be found in the xml that was passed to the post_assert
|
|
70
|
+
* to get the response, and can be applied to that response to overwrite the "unfriendly" names.
|
|
71
|
+
* @param saml_assert_response
|
|
72
|
+
*/
|
|
73
|
+
export declare function getFriendlySamlResponse(xmlData: string | undefined, response: SAMLAssertResponse): SAMLAssertResponse | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare const ParameterTester: {
|
|
2
|
+
isBlank: (s: string | null | undefined) => boolean;
|
|
3
|
+
isNotBlank: (s: string | null | undefined) => boolean;
|
|
4
|
+
anyBlank: (...a: any) => boolean;
|
|
5
|
+
anyNotBlank: (...a: any) => boolean;
|
|
6
|
+
allBlank: (...a: any) => boolean;
|
|
7
|
+
noneBlank: (...a: any) => boolean;
|
|
8
|
+
someBlankSomeNot: (...a: any) => boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Prints debug information to console when DEBUG environment variable is set to 'true'.
|
|
12
|
+
* @param value The debug message to print
|
|
13
|
+
*/
|
|
14
|
+
export declare const debugPrint: (value: string) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Find the first field with a value in an object for the list of field names provided.
|
|
17
|
+
* The field is assumed to be an array, and the value is the first element of it.
|
|
18
|
+
* @param parentObj The object to search within
|
|
19
|
+
* @param names The field names to search for
|
|
20
|
+
* @returns The first non-empty field value found, or null if none found
|
|
21
|
+
*/
|
|
22
|
+
export declare const findFirstFieldValue: (parentObj: any, ...names: any) => string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Logs debug messages to console when DEBUG environment variable is set to 'true'.
|
|
25
|
+
* @param msg The message to log
|
|
26
|
+
*/
|
|
27
|
+
export declare const debugLog: (msg: string) => void;
|
|
28
|
+
/**
|
|
29
|
+
* Type guard function to check if a value is of a specific type based on field presence.
|
|
30
|
+
* @param value The value to check
|
|
31
|
+
* @param fieldName The field name that should exist on the type
|
|
32
|
+
* @returns True if the field exists on the value, indicating it's of type T
|
|
33
|
+
*/
|
|
34
|
+
export declare const instanceOf: <T>(value: any, fieldName: string) => value is T;
|
|
35
|
+
type ReplacerFunction = (key: string, value: any) => any;
|
|
36
|
+
type ReplacerArray = (string | number)[];
|
|
37
|
+
type Replacer = ReplacerFunction | ReplacerArray | null;
|
|
38
|
+
type Space = string | number | null;
|
|
39
|
+
export declare const safeStringify: (obj: any, replacer?: Replacer, space?: Space) => string | undefined;
|
|
40
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bostonuniversity/shibboleth-sp",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A nodejs typescript library that provides all service provider functionality for authenticating with a shibboleth IDP",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/bu-ist/shibboleth-sp.git"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "rm -rf dist/ && node src/build.js",
|
|
11
|
+
"build:tsc": "rm -rf dist/ && npm run build:esm && npm run build:cjs",
|
|
12
|
+
"build:esm": "tsc",
|
|
13
|
+
"build:cjs": "tsc --module CommonJS --outdir dist/cjs",
|
|
14
|
+
"pack": "npm run build && npm pack --pack-destination .",
|
|
15
|
+
"publish": "npm run pack && npm publish ./@bostonuniversity-shibboleth-sp-1.0.2.tgz --access public",
|
|
16
|
+
"watch": "tsc -w",
|
|
17
|
+
"test": "jest --silent"
|
|
18
|
+
},
|
|
19
|
+
"module": "dist/esm/index.js",
|
|
20
|
+
"main": "dist/cjs/index.js",
|
|
21
|
+
"types": "dist/types/index.d.ts",
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@types/saml2-js": "^3.0.4",
|
|
27
|
+
"@xmldom/xmldom": "^0.8.10",
|
|
28
|
+
"js-yaml": "^3.14.2",
|
|
29
|
+
"jsonwebtoken": "^9.0.2",
|
|
30
|
+
"ms": "^3.0.0-canary.1",
|
|
31
|
+
"saml2-js": "^4.0.2"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/jest": "^29.5.12",
|
|
35
|
+
"@types/jsonwebtoken": "^9.0.6",
|
|
36
|
+
"@types/node": "20.6.3",
|
|
37
|
+
"@types/node-forge": "^1.3.8",
|
|
38
|
+
"esbuild": "^0.25.11",
|
|
39
|
+
"jest": "^29.7.0",
|
|
40
|
+
"node-forge": "^1.3.1",
|
|
41
|
+
"ts-jest": "^29.2.0",
|
|
42
|
+
"ts-node": "^10.9.1",
|
|
43
|
+
"tsx": "^4.21.0",
|
|
44
|
+
"typescript": "~5.2.2"
|
|
45
|
+
},
|
|
46
|
+
"overrides": {
|
|
47
|
+
"js-yaml": "^3.14.2"
|
|
48
|
+
}
|
|
49
|
+
}
|