passport-entra 0.0.1
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/dist/passport-entra.d.ts +39 -0
- package/dist/passport-entra.js +82 -0
- package/package.json +20 -0
- package/passport-entra.ts +117 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Strategy } from 'passport';
|
|
2
|
+
import { createRemoteJWKSet } from 'jose';
|
|
3
|
+
import type { Request } from 'express';
|
|
4
|
+
export interface OauthBearerOptions {
|
|
5
|
+
audience?: string | string[];
|
|
6
|
+
clientID: string;
|
|
7
|
+
clockSkew?: string | number;
|
|
8
|
+
identityMetadata: string;
|
|
9
|
+
issuer: string | string[];
|
|
10
|
+
scope?: string[];
|
|
11
|
+
}
|
|
12
|
+
type VerifyCallback = (token: object, done: AuthenticateCallback) => void;
|
|
13
|
+
export type AuthenticateCallback = (err: unknown, user?: Express.User | false | null, info?: object, status?: number | number[]) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Bearerstrategy for password.js, used with Microsoft Entra as identity provider
|
|
16
|
+
*/
|
|
17
|
+
export default class BearerStrategy extends Strategy {
|
|
18
|
+
/** aud claim check */
|
|
19
|
+
audience: string[];
|
|
20
|
+
/** clientID to check */
|
|
21
|
+
clientID: string;
|
|
22
|
+
/** clockSkew allowed when checking nbf and exp */
|
|
23
|
+
clockSkew: string | number;
|
|
24
|
+
/** well known oid-configuration endpoint */
|
|
25
|
+
identityMetadata: string;
|
|
26
|
+
/** iss claim check */
|
|
27
|
+
issuer: string | string[];
|
|
28
|
+
/** strategy name */
|
|
29
|
+
name: string;
|
|
30
|
+
/** scp claim check */
|
|
31
|
+
scope: string[];
|
|
32
|
+
/** call this function to get the user belonging to the token */
|
|
33
|
+
verifyFn: VerifyCallback;
|
|
34
|
+
/** cached JWK set from well known oid-configuration endpoint */
|
|
35
|
+
jwks: ReturnType<typeof createRemoteJWKSet> | undefined;
|
|
36
|
+
constructor(options: OauthBearerOptions, verifyFn: VerifyCallback);
|
|
37
|
+
authenticate(req: Request): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const passport_1 = require("passport");
|
|
13
|
+
const jose_1 = require("jose");
|
|
14
|
+
/**
|
|
15
|
+
* Bearerstrategy for password.js, used with Microsoft Entra as identity provider
|
|
16
|
+
*/
|
|
17
|
+
class BearerStrategy extends passport_1.Strategy {
|
|
18
|
+
constructor(options, verifyFn) {
|
|
19
|
+
var _a, _b, _c;
|
|
20
|
+
super();
|
|
21
|
+
/** strategy name */
|
|
22
|
+
this.name = 'oauth-bearer';
|
|
23
|
+
this.verifyFn = verifyFn;
|
|
24
|
+
let audience = (_a = options.audience) !== null && _a !== void 0 ? _a : options.clientID;
|
|
25
|
+
if (typeof audience === 'string') {
|
|
26
|
+
audience = [audience, `spn:${audience}`];
|
|
27
|
+
}
|
|
28
|
+
this.audience = audience;
|
|
29
|
+
this.clientID = options.clientID;
|
|
30
|
+
this.clockSkew = (_b = options.clockSkew) !== null && _b !== void 0 ? _b : 300;
|
|
31
|
+
this.identityMetadata = options.identityMetadata;
|
|
32
|
+
this.issuer = options.issuer;
|
|
33
|
+
this.scope = (_c = options.scope) !== null && _c !== void 0 ? _c : [];
|
|
34
|
+
}
|
|
35
|
+
authenticate(req) {
|
|
36
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
37
|
+
var _a;
|
|
38
|
+
try {
|
|
39
|
+
if (!this.jwks) {
|
|
40
|
+
const res = yield fetch(this.identityMetadata);
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Error ${String(res.status)} when querying identity metadata`);
|
|
43
|
+
}
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
45
|
+
const { jwks_uri } = yield res.json();
|
|
46
|
+
this.jwks = (0, jose_1.createRemoteJWKSet)(new URL(jwks_uri));
|
|
47
|
+
}
|
|
48
|
+
const header = req.get('authorization');
|
|
49
|
+
const headerParts = header === null || header === void 0 ? void 0 : header.split(' ');
|
|
50
|
+
const jwt = (headerParts === null || headerParts === void 0 ? void 0 : headerParts.length) === 2 && headerParts[0].toLowerCase() === 'bearer' && headerParts[1];
|
|
51
|
+
if (!jwt) {
|
|
52
|
+
throw new Error('Unable to extract Bearer token from Authorization header');
|
|
53
|
+
}
|
|
54
|
+
const { payload } = yield (0, jose_1.jwtVerify)(jwt, this.jwks, {
|
|
55
|
+
audience: this.audience, // aud
|
|
56
|
+
clockTolerance: this.clockSkew, // exp, nbf
|
|
57
|
+
issuer: this.issuer, // iss
|
|
58
|
+
});
|
|
59
|
+
// console.log(JSON.stringify(payload, null, 2));
|
|
60
|
+
const scp = String((_a = payload.scp) !== null && _a !== void 0 ? _a : ''); // scp
|
|
61
|
+
if (this.scope.length > 0
|
|
62
|
+
&& !this.scope.some((scope) => scp.includes(scope))) {
|
|
63
|
+
throw new Error('Scope does not match scp claim');
|
|
64
|
+
}
|
|
65
|
+
this.verifyFn(payload, (err, user, info) => {
|
|
66
|
+
if (err) {
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
if (!user) {
|
|
70
|
+
throw new Error('No user found');
|
|
71
|
+
}
|
|
72
|
+
this.success(user, info);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = JSON.stringify(error);
|
|
77
|
+
this.fail(message);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.default = BearerStrategy;
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "passport-entra",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Microft Entra authentication strategy for passport",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"author": "Jan Bakker",
|
|
11
|
+
"license": "ISC",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"jose": "^6.0.11",
|
|
14
|
+
"passport": "^0.7.0",
|
|
15
|
+
"typescript": "^5.8.3"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/passport": "^1.0.17"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Strategy } from 'passport';
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
3
|
+
|
|
4
|
+
import type { Request } from 'express';
|
|
5
|
+
|
|
6
|
+
export interface OauthBearerOptions {
|
|
7
|
+
audience?: string | string[];
|
|
8
|
+
clientID: string;
|
|
9
|
+
clockSkew?: string | number;
|
|
10
|
+
identityMetadata: string;
|
|
11
|
+
issuer: string | string[];
|
|
12
|
+
scope?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type VerifyCallback = (token: object, done: AuthenticateCallback) => void;
|
|
16
|
+
|
|
17
|
+
export type AuthenticateCallback = (
|
|
18
|
+
err: unknown,
|
|
19
|
+
user?: Express.User | false | null,
|
|
20
|
+
info?: object,
|
|
21
|
+
status?: number | number[]
|
|
22
|
+
) => void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Bearerstrategy for password.js, used with Microsoft Entra as identity provider
|
|
26
|
+
*/
|
|
27
|
+
export default class BearerStrategy extends Strategy {
|
|
28
|
+
/** aud claim check */
|
|
29
|
+
audience;
|
|
30
|
+
|
|
31
|
+
/** clientID to check */
|
|
32
|
+
clientID;
|
|
33
|
+
|
|
34
|
+
/** clockSkew allowed when checking nbf and exp */
|
|
35
|
+
clockSkew;
|
|
36
|
+
|
|
37
|
+
/** well known oid-configuration endpoint */
|
|
38
|
+
identityMetadata: string;
|
|
39
|
+
|
|
40
|
+
/** iss claim check */
|
|
41
|
+
issuer;
|
|
42
|
+
|
|
43
|
+
/** strategy name */
|
|
44
|
+
name = 'oauth-bearer';
|
|
45
|
+
|
|
46
|
+
/** scp claim check */
|
|
47
|
+
scope;
|
|
48
|
+
|
|
49
|
+
/** call this function to get the user belonging to the token */
|
|
50
|
+
verifyFn;
|
|
51
|
+
|
|
52
|
+
/** cached JWK set from well known oid-configuration endpoint */
|
|
53
|
+
jwks: ReturnType<typeof createRemoteJWKSet> | undefined;
|
|
54
|
+
|
|
55
|
+
constructor(options: OauthBearerOptions, verifyFn: VerifyCallback) {
|
|
56
|
+
super();
|
|
57
|
+
|
|
58
|
+
this.verifyFn = verifyFn;
|
|
59
|
+
|
|
60
|
+
let audience = options.audience ?? options.clientID;
|
|
61
|
+
if (typeof audience === 'string') {
|
|
62
|
+
audience = [audience, `spn:${audience}`];
|
|
63
|
+
}
|
|
64
|
+
this.audience = audience;
|
|
65
|
+
|
|
66
|
+
this.clientID = options.clientID;
|
|
67
|
+
this.clockSkew = options.clockSkew ?? 300;
|
|
68
|
+
this.identityMetadata = options.identityMetadata;
|
|
69
|
+
this.issuer = options.issuer;
|
|
70
|
+
this.scope = options.scope ?? [];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async authenticate(req: Request) {
|
|
74
|
+
try {
|
|
75
|
+
if (!this.jwks) {
|
|
76
|
+
const res = await fetch(this.identityMetadata);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new Error(`Error ${String(res.status)} when querying identity metadata`);
|
|
79
|
+
}
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
81
|
+
const { jwks_uri } = await res.json() as { jwks_uri: string };
|
|
82
|
+
this.jwks = createRemoteJWKSet(new URL(jwks_uri));
|
|
83
|
+
}
|
|
84
|
+
const header = req.get('authorization');
|
|
85
|
+
const headerParts = header?.split(' ');
|
|
86
|
+
const jwt = headerParts?.length === 2 && headerParts[0].toLowerCase() === 'bearer' && headerParts[1];
|
|
87
|
+
if (!jwt) {
|
|
88
|
+
throw new Error('Unable to extract Bearer token from Authorization header');
|
|
89
|
+
}
|
|
90
|
+
const { payload } = await jwtVerify(jwt, this.jwks, {
|
|
91
|
+
audience: this.audience, // aud
|
|
92
|
+
clockTolerance: this.clockSkew, // exp, nbf
|
|
93
|
+
issuer: this.issuer, // iss
|
|
94
|
+
});
|
|
95
|
+
// console.log(JSON.stringify(payload, null, 2));
|
|
96
|
+
|
|
97
|
+
const scp = String(payload.scp ?? ''); // scp
|
|
98
|
+
if (this.scope.length > 0
|
|
99
|
+
&& !this.scope.some((scope) => scp.includes(scope))) {
|
|
100
|
+
throw new Error('Scope does not match scp claim');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.verifyFn(payload, (err: unknown, user: unknown, info?: object) => {
|
|
104
|
+
if (err) {
|
|
105
|
+
throw err as Error;
|
|
106
|
+
}
|
|
107
|
+
if (!user) {
|
|
108
|
+
throw new Error('No user found');
|
|
109
|
+
}
|
|
110
|
+
this.success(user, info);
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
const message = JSON.stringify(error);
|
|
114
|
+
this.fail(message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
|
|
6
|
+
"target": "es2016",
|
|
7
|
+
"module": "commonjs",
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
|
|
11
|
+
"strict": true,
|
|
12
|
+
"skipLibCheck": true
|
|
13
|
+
},
|
|
14
|
+
"exclude": [
|
|
15
|
+
"node_modules",
|
|
16
|
+
"dist"
|
|
17
|
+
]
|
|
18
|
+
}
|