@rudderjs/passport 1.1.0 → 1.1.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 +96 -15
- package/boost/guidelines.md +190 -0
- package/dist/grants/authorization-code.d.ts.map +1 -1
- package/dist/grants/authorization-code.js +4 -17
- package/dist/grants/authorization-code.js.map +1 -1
- package/dist/grants/client-credentials.d.ts.map +1 -1
- package/dist/grants/client-credentials.js +4 -17
- package/dist/grants/client-credentials.js.map +1 -1
- package/dist/grants/device-code.d.ts.map +1 -1
- package/dist/grants/device-code.js +2 -1
- package/dist/grants/device-code.js.map +1 -1
- package/dist/grants/parse-scopes.d.ts +15 -0
- package/dist/grants/parse-scopes.d.ts.map +1 -0
- package/dist/grants/parse-scopes.js +17 -0
- package/dist/grants/parse-scopes.js.map +1 -0
- package/dist/grants/refresh-token.d.ts.map +1 -1
- package/dist/grants/refresh-token.js +5 -18
- package/dist/grants/refresh-token.js.map +1 -1
- package/dist/grants/verify-client.d.ts +29 -0
- package/dist/grants/verify-client.d.ts.map +1 -0
- package/dist/grants/verify-client.js +43 -0
- package/dist/grants/verify-client.js.map +1 -0
- package/dist/middleware/bearer.d.ts.map +1 -1
- package/dist/middleware/bearer.js +98 -103
- package/dist/middleware/bearer.js.map +1 -1
- package/dist/models/AccessToken.d.ts +3 -3
- package/dist/models/AuthCode.d.ts +3 -3
- package/dist/models/DeviceCode.d.ts +3 -3
- package/dist/models/RefreshToken.d.ts +3 -3
- package/dist/models/helpers.d.ts +27 -9
- package/dist/models/helpers.d.ts.map +1 -1
- package/dist/models/helpers.js +12 -6
- package/dist/models/helpers.js.map +1 -1
- package/dist/personal-access-tokens.d.ts.map +1 -1
- package/dist/personal-access-tokens.js.map +1 -1
- package/dist/routes/authorize.d.ts +17 -0
- package/dist/routes/authorize.d.ts.map +1 -0
- package/dist/routes/authorize.js +107 -0
- package/dist/routes/authorize.js.map +1 -0
- package/dist/routes/device.d.ts +23 -0
- package/dist/routes/device.d.ts.map +1 -0
- package/dist/routes/device.js +69 -0
- package/dist/routes/device.js.map +1 -0
- package/dist/routes/helpers.d.ts +64 -0
- package/dist/routes/helpers.d.ts.map +1 -0
- package/dist/routes/helpers.js +154 -0
- package/dist/routes/helpers.js.map +1 -0
- package/dist/routes/revoke.d.ts +16 -0
- package/dist/routes/revoke.d.ts.map +1 -0
- package/dist/routes/revoke.js +33 -0
- package/dist/routes/revoke.js.map +1 -0
- package/dist/routes/scopes.d.ts +9 -0
- package/dist/routes/scopes.d.ts.map +1 -0
- package/dist/routes/scopes.js +13 -0
- package/dist/routes/scopes.js.map +1 -0
- package/dist/routes/token.d.ts +24 -0
- package/dist/routes/token.d.ts.map +1 -0
- package/dist/routes/token.js +121 -0
- package/dist/routes/token.js.map +1 -0
- package/dist/routes/types.d.ts +132 -0
- package/dist/routes/types.d.ts.map +1 -0
- package/dist/routes/types.js +2 -0
- package/dist/routes/types.js.map +1 -0
- package/dist/routes.d.ts +2 -120
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +16 -411
- package/dist/routes.js.map +1 -1
- package/package.json +7 -6
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '@rudderjs/contracts';
|
|
2
|
+
import type { Router } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register `GET/POST/DELETE /oauth/authorize` — the consent flow.
|
|
5
|
+
*
|
|
6
|
+
* - `GET` validates the authorization request and renders the consent screen
|
|
7
|
+
* (custom via `Passport.authorizationView()` or JSON by default).
|
|
8
|
+
* - `POST` requires a signed-in user and issues an authorization code on
|
|
9
|
+
* approval, redirecting back to `redirect_uri` with `code` + `state`.
|
|
10
|
+
* - `DELETE` issues an `access_denied` redirect on rejection.
|
|
11
|
+
*
|
|
12
|
+
* The redirect_uri on POST/DELETE bodies is attacker-controlled and is
|
|
13
|
+
* re-validated against the client's registered list (see
|
|
14
|
+
* `validateClientRedirect` in `helpers.ts`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function registerAuthorizeRoutes(router: Router, prefix: string, mw: MiddlewareHandler[]): void;
|
|
17
|
+
//# sourceMappingURL=authorize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize.d.ts","sourceRoot":"","sources":["../../src/routes/authorize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxC;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,iBAAiB,EAAE,GAAG,IAAI,CA+FrG"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Passport } from '../Passport.js';
|
|
2
|
+
import { validateAuthorizationRequest, issueAuthCode } from '../grants/index.js';
|
|
3
|
+
import { authErrorResponse, requesterIdFrom, validateClientRedirect } from './helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Register `GET/POST/DELETE /oauth/authorize` — the consent flow.
|
|
6
|
+
*
|
|
7
|
+
* - `GET` validates the authorization request and renders the consent screen
|
|
8
|
+
* (custom via `Passport.authorizationView()` or JSON by default).
|
|
9
|
+
* - `POST` requires a signed-in user and issues an authorization code on
|
|
10
|
+
* approval, redirecting back to `redirect_uri` with `code` + `state`.
|
|
11
|
+
* - `DELETE` issues an `access_denied` redirect on rejection.
|
|
12
|
+
*
|
|
13
|
+
* The redirect_uri on POST/DELETE bodies is attacker-controlled and is
|
|
14
|
+
* re-validated against the client's registered list (see
|
|
15
|
+
* `validateClientRedirect` in `helpers.ts`).
|
|
16
|
+
*/
|
|
17
|
+
export function registerAuthorizeRoutes(router, prefix, mw) {
|
|
18
|
+
// GET /oauth/authorize — show consent (returns JSON or renders custom view)
|
|
19
|
+
router.get(`${prefix}/authorize`, async (req, res) => {
|
|
20
|
+
const query = req.query ?? {};
|
|
21
|
+
try {
|
|
22
|
+
const validated = await validateAuthorizationRequest({
|
|
23
|
+
clientId: query['client_id'] ?? '',
|
|
24
|
+
redirectUri: query['redirect_uri'] ?? '',
|
|
25
|
+
responseType: query['response_type'] ?? '',
|
|
26
|
+
scope: query['scope'] ?? '',
|
|
27
|
+
state: query['state'],
|
|
28
|
+
codeChallenge: query['code_challenge'],
|
|
29
|
+
codeChallengeMethod: query['code_challenge_method'],
|
|
30
|
+
});
|
|
31
|
+
const ctx = {
|
|
32
|
+
client: {
|
|
33
|
+
id: validated.client.id,
|
|
34
|
+
name: validated.client.name,
|
|
35
|
+
},
|
|
36
|
+
scopes: validated.scopes,
|
|
37
|
+
redirectUri: validated.redirectUri,
|
|
38
|
+
...(validated.state !== undefined ? { state: validated.state } : {}),
|
|
39
|
+
...(validated.codeChallenge !== undefined ? { codeChallenge: validated.codeChallenge } : {}),
|
|
40
|
+
...(validated.codeChallengeMethod !== undefined ? { codeChallengeMethod: validated.codeChallengeMethod } : {}),
|
|
41
|
+
request: req,
|
|
42
|
+
};
|
|
43
|
+
const viewFn = Passport.authorizationViewFn();
|
|
44
|
+
if (viewFn) {
|
|
45
|
+
return await viewFn(ctx);
|
|
46
|
+
}
|
|
47
|
+
// Default: JSON response — the app's consent screen reads this
|
|
48
|
+
res.json({
|
|
49
|
+
client: ctx.client,
|
|
50
|
+
scopes: ctx.scopes,
|
|
51
|
+
state: ctx.state,
|
|
52
|
+
redirectUri: ctx.redirectUri,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
authErrorResponse(res, e, query['state']);
|
|
57
|
+
}
|
|
58
|
+
}, mw);
|
|
59
|
+
// POST /oauth/authorize — user approves
|
|
60
|
+
router.post(`${prefix}/authorize`, async (req, res) => {
|
|
61
|
+
const body = req.body ?? {};
|
|
62
|
+
try {
|
|
63
|
+
const userId = requesterIdFrom(req);
|
|
64
|
+
if (!userId) {
|
|
65
|
+
// Echo state on the unauthenticated branch too — the consent UI
|
|
66
|
+
// round-trips the same payload regardless of the auth gate result.
|
|
67
|
+
const stateEcho = typeof body['state'] === 'string' && body['state'] ? { state: body['state'] } : {};
|
|
68
|
+
res.status(401).json({ error: 'unauthenticated', error_description: 'User must be signed in.', ...stateEcho });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await validateClientRedirect(body['client_id'], body['redirect_uri']);
|
|
72
|
+
const code = await issueAuthCode({
|
|
73
|
+
userId,
|
|
74
|
+
clientId: body['client_id'],
|
|
75
|
+
scopes: body['scopes'] ?? [],
|
|
76
|
+
redirectUri: body['redirect_uri'],
|
|
77
|
+
codeChallenge: body['code_challenge'],
|
|
78
|
+
codeChallengeMethod: body['code_challenge_method'],
|
|
79
|
+
});
|
|
80
|
+
const redirectUri = new URL(body['redirect_uri']);
|
|
81
|
+
redirectUri.searchParams.set('code', code);
|
|
82
|
+
if (body['state'])
|
|
83
|
+
redirectUri.searchParams.set('state', body['state']);
|
|
84
|
+
res.json({ redirect_uri: redirectUri.toString() });
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
authErrorResponse(res, e, body['state']);
|
|
88
|
+
}
|
|
89
|
+
}, mw);
|
|
90
|
+
// DELETE /oauth/authorize — user denies
|
|
91
|
+
router.delete(`${prefix}/authorize`, async (req, res) => {
|
|
92
|
+
const body = req.body ?? {};
|
|
93
|
+
try {
|
|
94
|
+
await validateClientRedirect(body['client_id'], body['redirect_uri']);
|
|
95
|
+
const redirectUri = new URL(body['redirect_uri']);
|
|
96
|
+
redirectUri.searchParams.set('error', 'access_denied');
|
|
97
|
+
redirectUri.searchParams.set('error_description', 'The user denied the request.');
|
|
98
|
+
if (body['state'])
|
|
99
|
+
redirectUri.searchParams.set('state', body['state']);
|
|
100
|
+
res.json({ redirect_uri: redirectUri.toString() });
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
authErrorResponse(res, e, body['state']);
|
|
104
|
+
}
|
|
105
|
+
}, mw);
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=authorize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authorize.js","sourceRoot":"","sources":["../../src/routes/authorize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '@rudderjs/contracts';
|
|
2
|
+
import type { PassportRouteOptions, Router } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register `POST /oauth/device/code` + `POST /oauth/device/approve` — the
|
|
5
|
+
* RFC 8628 device authorization flow.
|
|
6
|
+
*
|
|
7
|
+
* - `POST /oauth/device/code` is stateless: a device requests a `device_code`
|
|
8
|
+
* + `user_code` pair, plus the `verification_uri` for the user to visit.
|
|
9
|
+
* - `POST /oauth/device/approve` is session-backed: the signed-in user
|
|
10
|
+
* approves or denies the device after typing the user_code.
|
|
11
|
+
*
|
|
12
|
+
* `mw` runs ahead of both handlers. The RFC 8628 §5.2 brute-force concern
|
|
13
|
+
* on user_code is already covered by a typical 60/min api-group rate
|
|
14
|
+
* limiter; pass a tighter per-route limiter via `deviceMiddleware` if your
|
|
15
|
+
* threat model warrants it.
|
|
16
|
+
*
|
|
17
|
+
* `verification_uri` resolution priority: explicit `opts.verificationUri`
|
|
18
|
+
* > `config('app.url')` > `req.protocol + req.hostname` (last resort with
|
|
19
|
+
* a one-shot warning, since `Host` is attacker-controlled behind a
|
|
20
|
+
* reverse proxy without trust-proxy).
|
|
21
|
+
*/
|
|
22
|
+
export declare function registerDeviceRoutes(router: Router, opts: PassportRouteOptions, prefix: string, mw: MiddlewareHandler[]): void;
|
|
23
|
+
//# sourceMappingURL=device.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../../src/routes/device.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAG9D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,MAAM,EACd,EAAE,EAAE,iBAAiB,EAAE,GACtB,IAAI,CA0CN"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { report } from '@rudderjs/core';
|
|
2
|
+
import { requestDeviceCode, approveDeviceCode, OAuthError } from '../grants/index.js';
|
|
3
|
+
import { requesterIdFrom, resolveVerificationUri } from './helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Register `POST /oauth/device/code` + `POST /oauth/device/approve` — the
|
|
6
|
+
* RFC 8628 device authorization flow.
|
|
7
|
+
*
|
|
8
|
+
* - `POST /oauth/device/code` is stateless: a device requests a `device_code`
|
|
9
|
+
* + `user_code` pair, plus the `verification_uri` for the user to visit.
|
|
10
|
+
* - `POST /oauth/device/approve` is session-backed: the signed-in user
|
|
11
|
+
* approves or denies the device after typing the user_code.
|
|
12
|
+
*
|
|
13
|
+
* `mw` runs ahead of both handlers. The RFC 8628 §5.2 brute-force concern
|
|
14
|
+
* on user_code is already covered by a typical 60/min api-group rate
|
|
15
|
+
* limiter; pass a tighter per-route limiter via `deviceMiddleware` if your
|
|
16
|
+
* threat model warrants it.
|
|
17
|
+
*
|
|
18
|
+
* `verification_uri` resolution priority: explicit `opts.verificationUri`
|
|
19
|
+
* > `config('app.url')` > `req.protocol + req.hostname` (last resort with
|
|
20
|
+
* a one-shot warning, since `Host` is attacker-controlled behind a
|
|
21
|
+
* reverse proxy without trust-proxy).
|
|
22
|
+
*/
|
|
23
|
+
export function registerDeviceRoutes(router, opts, prefix, mw) {
|
|
24
|
+
// POST /oauth/device/code — request device authorization
|
|
25
|
+
router.post(`${prefix}/device/code`, async (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
const body = req.body ?? {};
|
|
28
|
+
const verificationUri = resolveVerificationUri(opts, req, prefix);
|
|
29
|
+
const result = await requestDeviceCode({
|
|
30
|
+
clientId: body['client_id'],
|
|
31
|
+
scope: body['scope'],
|
|
32
|
+
verificationUri,
|
|
33
|
+
});
|
|
34
|
+
res.json(result);
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
if (e instanceof OAuthError) {
|
|
38
|
+
res.status(e.statusCode).json(e.toJSON());
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
report(e);
|
|
42
|
+
res.status(500).json({ error: 'server_error', error_description: 'Internal server error.' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}, mw);
|
|
46
|
+
// POST /oauth/device/approve — user approves/denies device
|
|
47
|
+
router.post(`${prefix}/device/approve`, async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const body = req.body ?? {};
|
|
50
|
+
const userId = requesterIdFrom(req);
|
|
51
|
+
if (!userId) {
|
|
52
|
+
res.status(401).json({ error: 'unauthenticated', error_description: 'User must be signed in.' });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
await approveDeviceCode(body['user_code'], userId, body['approved'] !== false);
|
|
56
|
+
res.json({ status: 'ok' });
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (e instanceof OAuthError) {
|
|
60
|
+
res.status(e.statusCode).json(e.toJSON());
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
report(e);
|
|
64
|
+
res.status(500).json({ error: 'server_error', error_description: 'Internal server error.' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, mw);
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../src/routes/device.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAErF,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAA;AAEtE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAc,EACd,IAA0B,EAC1B,MAAc,EACd,EAAuB;IAEvB,yDAAyD;IACzD,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,cAAc,EAAE,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,EAAE;QAChE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAA;YAC3B,MAAM,eAAe,GAAG,sBAAsB,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;YACjE,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;gBACrC,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC;gBAC3B,KAAK,EAAK,IAAI,CAAC,OAAO,CAAC;gBACvB,eAAe;aAChB,CAAC,CAAA;YACF,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAClB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,YAAY,UAAU,EAAE,CAAC;gBAC5B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;YAC3C,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,CAAC,CAAC,CAAA;gBACT,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,CAAC,CAAA;YAC9F,CAAC;QACH,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,2DAA2D;IAC3D,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,iBAAiB,EAAE,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,EAAE;QACnE,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAA;YAC3B,MAAM,MAAM,GAAG,eAAe,CAAC,GAAG,CAAC,CAAA;YACnC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,CAAC,CAAA;gBAChG,OAAM;YACR,CAAC;YACD,MAAM,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK,KAAK,CAAC,CAAA;YAC9E,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC5B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,YAAY,UAAU,EAAE,CAAC;gBAC5B,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;YAC3C,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,CAAC,CAAC,CAAA;gBACT,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,CAAC,CAAA;YAC9F,CAAC;QACH,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;AACR,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '@rudderjs/contracts';
|
|
2
|
+
import type { OAuthClient } from '../models/OAuthClient.js';
|
|
3
|
+
import type { PassportRouteOptions } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Re-validate that `redirect_uri` is on the requesting client's whitelist.
|
|
6
|
+
* The consent UI sees the validated URI from `GET /oauth/authorize`, but the
|
|
7
|
+
* subsequent POST/DELETE bodies are attacker-controlled and must be
|
|
8
|
+
* re-checked — otherwise the response leaks an authorization code (POST) or
|
|
9
|
+
* an open redirect (DELETE) to a host the client never registered.
|
|
10
|
+
* Throws `OAuthError` so the surrounding try/catch returns the correct
|
|
11
|
+
* status + payload.
|
|
12
|
+
*/
|
|
13
|
+
export declare function validateClientRedirect(clientId: unknown, redirectUri: unknown): Promise<OAuthClient>;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve client credentials at the token endpoint per RFC 6749 §2.3.
|
|
16
|
+
*
|
|
17
|
+
* Confidential clients can authenticate via:
|
|
18
|
+
* 1. `Authorization: Basic base64(client_id:client_secret)` (§2.3.1, MUST support)
|
|
19
|
+
* 2. `client_id` + `client_secret` in the request body (alternative)
|
|
20
|
+
*
|
|
21
|
+
* §2.3 forbids using both at once — clients MUST NOT pass credentials in
|
|
22
|
+
* the body when the header is present. We reject that combination with
|
|
23
|
+
* `invalid_request` so SDK bugs surface loudly instead of silently
|
|
24
|
+
* accepting one set and ignoring the other.
|
|
25
|
+
*
|
|
26
|
+
* Public clients send only `client_id` in the body; both Basic creds and
|
|
27
|
+
* a body `client_id` mismatch is also rejected.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveClientCredentials(req: {
|
|
30
|
+
headers?: Record<string, unknown>;
|
|
31
|
+
}, body: Record<string, unknown>): {
|
|
32
|
+
clientId: string;
|
|
33
|
+
clientSecret?: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function resolveVerificationUri(opts: PassportRouteOptions, req: {
|
|
36
|
+
protocol?: string;
|
|
37
|
+
hostname?: string;
|
|
38
|
+
}, prefix: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Render an error response from a `/oauth/authorize` handler. RFC 6749
|
|
41
|
+
* §4.1.2.1 requires that `state` is echoed back on errors (so the client
|
|
42
|
+
* can reconcile the response against its own session) — independent of
|
|
43
|
+
* whether the response shape is a redirect or JSON, and independent of
|
|
44
|
+
* the underlying error code.
|
|
45
|
+
*
|
|
46
|
+
* We additionally call `report()` on non-`OAuthError` throws so the root
|
|
47
|
+
* cause surfaces through the configured exception reporter instead of
|
|
48
|
+
* being silently collapsed under `server_error`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function authErrorResponse(res: any, err: unknown, state: unknown): void;
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the authenticated requester's id from a passport-route request,
|
|
53
|
+
* checking both the `__rjs_user` raw-bag stamp (set by server-hono's auth
|
|
54
|
+
* middleware) and the plain `req.user` fallback (set on the universal
|
|
55
|
+
* middleware bridge). Returns `null` when neither is populated — typically
|
|
56
|
+
* means no session / not signed in, and the caller should respond 401.
|
|
57
|
+
*/
|
|
58
|
+
export declare function requesterIdFrom(req: {
|
|
59
|
+
raw?: unknown;
|
|
60
|
+
user?: unknown;
|
|
61
|
+
}): string | null;
|
|
62
|
+
/** Normalize an optional middleware option into an array, dropping `undefined`. */
|
|
63
|
+
export declare function asMiddlewareArray(input: MiddlewareHandler | MiddlewareHandler[] | undefined): MiddlewareHandler[];
|
|
64
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/routes/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAG5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAG3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAEtD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAgB1G;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC1C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CA4C7C;AAcD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB,EAAE,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAiBxI;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAQ9E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE;IAAE,GAAG,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAKrF;AAED,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,iBAAiB,GAAG,iBAAiB,EAAE,GAAG,SAAS,GAAG,iBAAiB,EAAE,CAGjH"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { config, report } from '@rudderjs/core';
|
|
2
|
+
import { Passport } from '../Passport.js';
|
|
3
|
+
import { clientHelpers } from '../models/helpers.js';
|
|
4
|
+
import { OAuthError } from '../grants/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Re-validate that `redirect_uri` is on the requesting client's whitelist.
|
|
7
|
+
* The consent UI sees the validated URI from `GET /oauth/authorize`, but the
|
|
8
|
+
* subsequent POST/DELETE bodies are attacker-controlled and must be
|
|
9
|
+
* re-checked — otherwise the response leaks an authorization code (POST) or
|
|
10
|
+
* an open redirect (DELETE) to a host the client never registered.
|
|
11
|
+
* Throws `OAuthError` so the surrounding try/catch returns the correct
|
|
12
|
+
* status + payload.
|
|
13
|
+
*/
|
|
14
|
+
export async function validateClientRedirect(clientId, redirectUri) {
|
|
15
|
+
if (typeof clientId !== 'string' || !clientId) {
|
|
16
|
+
throw new OAuthError('invalid_request', 'client_id is required.');
|
|
17
|
+
}
|
|
18
|
+
if (typeof redirectUri !== 'string' || !redirectUri) {
|
|
19
|
+
throw new OAuthError('invalid_request', 'redirect_uri is required.');
|
|
20
|
+
}
|
|
21
|
+
const ClientCls = await Passport.clientModel();
|
|
22
|
+
const client = await ClientCls.where('id', clientId).first();
|
|
23
|
+
if (!client || client.revoked) {
|
|
24
|
+
throw new OAuthError('invalid_client', 'Client not found.');
|
|
25
|
+
}
|
|
26
|
+
if (!clientHelpers.hasRedirectUri(client, redirectUri)) {
|
|
27
|
+
throw new OAuthError('invalid_request', 'Invalid redirect_uri.');
|
|
28
|
+
}
|
|
29
|
+
return client;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve client credentials at the token endpoint per RFC 6749 §2.3.
|
|
33
|
+
*
|
|
34
|
+
* Confidential clients can authenticate via:
|
|
35
|
+
* 1. `Authorization: Basic base64(client_id:client_secret)` (§2.3.1, MUST support)
|
|
36
|
+
* 2. `client_id` + `client_secret` in the request body (alternative)
|
|
37
|
+
*
|
|
38
|
+
* §2.3 forbids using both at once — clients MUST NOT pass credentials in
|
|
39
|
+
* the body when the header is present. We reject that combination with
|
|
40
|
+
* `invalid_request` so SDK bugs surface loudly instead of silently
|
|
41
|
+
* accepting one set and ignoring the other.
|
|
42
|
+
*
|
|
43
|
+
* Public clients send only `client_id` in the body; both Basic creds and
|
|
44
|
+
* a body `client_id` mismatch is also rejected.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveClientCredentials(req, body) {
|
|
47
|
+
const authHeader = req.headers?.['authorization'];
|
|
48
|
+
const bodyClientId = body['client_id'];
|
|
49
|
+
const bodyClientSecret = body['client_secret'];
|
|
50
|
+
if (typeof authHeader === 'string' && authHeader.length >= 6 && authHeader.slice(0, 6).toLowerCase() === 'basic ') {
|
|
51
|
+
const encoded = authHeader.slice(6).trim();
|
|
52
|
+
let decoded;
|
|
53
|
+
try {
|
|
54
|
+
decoded = Buffer.from(encoded, 'base64').toString('utf8');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new OAuthError('invalid_request', 'Malformed HTTP Basic credentials.', 401);
|
|
58
|
+
}
|
|
59
|
+
const sep = decoded.indexOf(':');
|
|
60
|
+
if (sep === -1) {
|
|
61
|
+
throw new OAuthError('invalid_request', 'Malformed HTTP Basic credentials.', 401);
|
|
62
|
+
}
|
|
63
|
+
// RFC 6749 §2.3.1 — client_id and client_secret in Basic are
|
|
64
|
+
// application/x-www-form-urlencoded-encoded before base64. SDKs in
|
|
65
|
+
// the wild often skip the percent-encoding step; we accept the raw
|
|
66
|
+
// form because requiring percent-decoding here would reject every
|
|
67
|
+
// ASCII-only credential pair (which is the overwhelming majority).
|
|
68
|
+
const headerClientId = decoded.slice(0, sep);
|
|
69
|
+
const headerClientSecret = decoded.slice(sep + 1);
|
|
70
|
+
if (!headerClientId) {
|
|
71
|
+
throw new OAuthError('invalid_request', 'Malformed HTTP Basic credentials.', 401);
|
|
72
|
+
}
|
|
73
|
+
if (bodyClientSecret !== undefined) {
|
|
74
|
+
throw new OAuthError('invalid_request', 'client_secret must not be sent in both Authorization header and request body.', 401);
|
|
75
|
+
}
|
|
76
|
+
if (bodyClientId !== undefined && bodyClientId !== headerClientId) {
|
|
77
|
+
throw new OAuthError('invalid_request', 'client_id in Authorization header does not match request body.', 401);
|
|
78
|
+
}
|
|
79
|
+
return { clientId: headerClientId, clientSecret: headerClientSecret };
|
|
80
|
+
}
|
|
81
|
+
if (typeof bodyClientId !== 'string' || !bodyClientId) {
|
|
82
|
+
throw new OAuthError('invalid_request', 'client_id is required.');
|
|
83
|
+
}
|
|
84
|
+
return bodyClientSecret !== undefined
|
|
85
|
+
? { clientId: bodyClientId, clientSecret: bodyClientSecret }
|
|
86
|
+
: { clientId: bodyClientId };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the device-flow verification URI in this priority order:
|
|
90
|
+
*
|
|
91
|
+
* 1. `opts.verificationUri` — explicit caller override.
|
|
92
|
+
* 2. `config('app.url')` — `${appUrl}${prefix}/device`. Trailing slash on
|
|
93
|
+
* the configured value is tolerated.
|
|
94
|
+
* 3. `req.protocol` + `req.hostname` — last-resort fallback for dev / when
|
|
95
|
+
* neither knob is configured. The `Host` header is attacker-controlled
|
|
96
|
+
* behind a reverse proxy without trust-proxy, so we emit a one-shot
|
|
97
|
+
* warning the first time we land here. Documented in CLAUDE.md.
|
|
98
|
+
*/
|
|
99
|
+
let _hostHeaderFallbackWarned = false;
|
|
100
|
+
export function resolveVerificationUri(opts, req, prefix) {
|
|
101
|
+
if (opts.verificationUri)
|
|
102
|
+
return opts.verificationUri;
|
|
103
|
+
const appUrl = config('app.url', undefined);
|
|
104
|
+
if (typeof appUrl === 'string' && appUrl) {
|
|
105
|
+
return `${appUrl.replace(/\/$/, '')}${prefix}/device`;
|
|
106
|
+
}
|
|
107
|
+
if (!_hostHeaderFallbackWarned) {
|
|
108
|
+
_hostHeaderFallbackWarned = true;
|
|
109
|
+
console.warn('[@rudderjs/passport] Falling back to req.protocol/req.hostname for the device-flow verification URI. ' +
|
|
110
|
+
'The Host header is attacker-controlled behind a reverse proxy without trust-proxy. ' +
|
|
111
|
+
'Set APP_URL (config(\'app.url\')) or pass an explicit `verificationUri` to registerPassportRoutes() to silence this.');
|
|
112
|
+
}
|
|
113
|
+
return `${req.protocol}://${req.hostname}${prefix}/device`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Render an error response from a `/oauth/authorize` handler. RFC 6749
|
|
117
|
+
* §4.1.2.1 requires that `state` is echoed back on errors (so the client
|
|
118
|
+
* can reconcile the response against its own session) — independent of
|
|
119
|
+
* whether the response shape is a redirect or JSON, and independent of
|
|
120
|
+
* the underlying error code.
|
|
121
|
+
*
|
|
122
|
+
* We additionally call `report()` on non-`OAuthError` throws so the root
|
|
123
|
+
* cause surfaces through the configured exception reporter instead of
|
|
124
|
+
* being silently collapsed under `server_error`.
|
|
125
|
+
*/
|
|
126
|
+
export function authErrorResponse(res, err, state) {
|
|
127
|
+
const stateEcho = typeof state === 'string' && state ? { state } : {};
|
|
128
|
+
if (err instanceof OAuthError) {
|
|
129
|
+
res.status(err.statusCode).json({ ...err.toJSON(), ...stateEcho });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
report(err);
|
|
133
|
+
res.status(500).json({ error: 'server_error', error_description: 'Internal server error.', ...stateEcho });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Resolve the authenticated requester's id from a passport-route request,
|
|
137
|
+
* checking both the `__rjs_user` raw-bag stamp (set by server-hono's auth
|
|
138
|
+
* middleware) and the plain `req.user` fallback (set on the universal
|
|
139
|
+
* middleware bridge). Returns `null` when neither is populated — typically
|
|
140
|
+
* means no session / not signed in, and the caller should respond 401.
|
|
141
|
+
*/
|
|
142
|
+
export function requesterIdFrom(req) {
|
|
143
|
+
const fromRaw = req.raw?.__rjs_user?.id;
|
|
144
|
+
const fromReq = req.user?.id;
|
|
145
|
+
const id = fromRaw ?? fromReq;
|
|
146
|
+
return typeof id === 'string' && id ? id : null;
|
|
147
|
+
}
|
|
148
|
+
/** Normalize an optional middleware option into an array, dropping `undefined`. */
|
|
149
|
+
export function asMiddlewareArray(input) {
|
|
150
|
+
if (!input)
|
|
151
|
+
return [];
|
|
152
|
+
return Array.isArray(input) ? input : [input];
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/routes/helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '@rudderjs/contracts';
|
|
2
|
+
import type { Router } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register `DELETE /oauth/tokens/:id` — revoke a specific access token.
|
|
5
|
+
*
|
|
6
|
+
* Requires a valid bearer token AND ownership of the token being revoked.
|
|
7
|
+
* Token ids appear in JWT `jti` claims (semi-public), so without an
|
|
8
|
+
* ownership check anyone with a single captured JWT could DoS arbitrary
|
|
9
|
+
* users. Returns 404 (not 403) on ownership mismatch to avoid leaking
|
|
10
|
+
* whether a given id exists.
|
|
11
|
+
*
|
|
12
|
+
* `mw` is the `authorizeMiddleware` array — typically empty or CSRF when
|
|
13
|
+
* the app doesn't mount CSRF at the web-group level.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerRevokeRoute(router: Router, prefix: string, mw: MiddlewareHandler[]): void;
|
|
16
|
+
//# sourceMappingURL=revoke.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revoke.d.ts","sourceRoot":"","sources":["../../src/routes/revoke.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAI5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxC;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAkBjG"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Passport } from '../Passport.js';
|
|
2
|
+
import { RequireBearer } from '../middleware/bearer.js';
|
|
3
|
+
import { requesterIdFrom } from './helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Register `DELETE /oauth/tokens/:id` — revoke a specific access token.
|
|
6
|
+
*
|
|
7
|
+
* Requires a valid bearer token AND ownership of the token being revoked.
|
|
8
|
+
* Token ids appear in JWT `jti` claims (semi-public), so without an
|
|
9
|
+
* ownership check anyone with a single captured JWT could DoS arbitrary
|
|
10
|
+
* users. Returns 404 (not 403) on ownership mismatch to avoid leaking
|
|
11
|
+
* whether a given id exists.
|
|
12
|
+
*
|
|
13
|
+
* `mw` is the `authorizeMiddleware` array — typically empty or CSRF when
|
|
14
|
+
* the app doesn't mount CSRF at the web-group level.
|
|
15
|
+
*/
|
|
16
|
+
export function registerRevokeRoute(router, prefix, mw) {
|
|
17
|
+
router.delete(`${prefix}/tokens/:id`, async (req, res) => {
|
|
18
|
+
const tokenId = req.params?.['id'] ?? '';
|
|
19
|
+
const AccessTokenCls = await Passport.tokenModel();
|
|
20
|
+
const token = await AccessTokenCls.where('id', tokenId).first();
|
|
21
|
+
const requesterId = requesterIdFrom(req);
|
|
22
|
+
if (!token || !requesterId || token.userId !== requesterId) {
|
|
23
|
+
res.status(404).json({ error: 'not_found', error_description: 'Token not found.' });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// QueryBuilder.updateAll() bypasses the mass-assignment filter;
|
|
27
|
+
// `revoked` is no longer in `AccessToken.fillable`.
|
|
28
|
+
await AccessTokenCls.where('id', token.id)
|
|
29
|
+
.updateAll({ revoked: true });
|
|
30
|
+
res.status(204).send();
|
|
31
|
+
}, [RequireBearer(), ...mw]);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=revoke.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revoke.js","sourceRoot":"","sources":["../../src/routes/revoke.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAEzC,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAEvD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE9C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,MAAc,EAAE,EAAuB;IACzF,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,aAAa,EAAE,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,EAAE;QACjE,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;QACxC,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,CAAA;QAClD,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,EAAwB,CAAA;QAErF,MAAM,WAAW,GAAG,eAAe,CAAC,GAAG,CAAC,CAAA;QACxC,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAC3D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,CAAC,CAAA;YACnF,OAAM;QACR,CAAC;QAED,gEAAgE;QAChE,oDAAoD;QACpD,MAAM,cAAc,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,EAAE,CAAC;aACvC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAA6B,CAAC,CAAA;QAC1D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC,EAAE,CAAC,aAAa,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Router } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Register `GET /oauth/scopes` — list the OAuth scopes the app has declared
|
|
4
|
+
* via `Passport.tokensCan({...})`. Stateless, no auth required — useful for
|
|
5
|
+
* client SDKs that want to render the consent screen's scope list without
|
|
6
|
+
* round-tripping `/oauth/authorize`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerScopesRoute(router: Router, prefix: string): void;
|
|
9
|
+
//# sourceMappingURL=scopes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopes.d.ts","sourceRoot":"","sources":["../../src/routes/scopes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAIxE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Passport } from '../Passport.js';
|
|
2
|
+
/**
|
|
3
|
+
* Register `GET /oauth/scopes` — list the OAuth scopes the app has declared
|
|
4
|
+
* via `Passport.tokensCan({...})`. Stateless, no auth required — useful for
|
|
5
|
+
* client SDKs that want to render the consent screen's scope list without
|
|
6
|
+
* round-tripping `/oauth/authorize`.
|
|
7
|
+
*/
|
|
8
|
+
export function registerScopesRoute(router, prefix) {
|
|
9
|
+
router.get(`${prefix}/scopes`, async (_req, res) => {
|
|
10
|
+
res.json(Passport.scopes());
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=scopes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopes.js","sourceRoot":"","sources":["../../src/routes/scopes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGzC;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,MAAc;IAChE,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,SAAS,EAAE,KAAK,EAAE,IAAS,EAAE,GAAQ,EAAE,EAAE;QAC3D,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAA;IAC7B,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from '@rudderjs/contracts';
|
|
2
|
+
import type { Router } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Register `POST /oauth/token` — the OAuth 2 token endpoint.
|
|
5
|
+
*
|
|
6
|
+
* Dispatches to one of the four supported grants:
|
|
7
|
+
* - `authorization_code` — exchanges an auth code for tokens
|
|
8
|
+
* - `client_credentials` — machine-to-machine, confidential clients only
|
|
9
|
+
* - `refresh_token` — rotates an access+refresh pair
|
|
10
|
+
* - `urn:ietf:params:oauth:grant-type:device_code` — polls device flow
|
|
11
|
+
*
|
|
12
|
+
* `mw` runs ahead of the handler. The token endpoint is the canonical
|
|
13
|
+
* brute-force target for client_secret guessing — every production app
|
|
14
|
+
* SHOULD pass a per-route rate limiter here. See
|
|
15
|
+
* `PassportRouteOptions.tokenMiddleware` jsdoc for the recommended config.
|
|
16
|
+
*
|
|
17
|
+
* RFC 6749 §5.2 — client-auth failures (HTTP 401) are signalled with a
|
|
18
|
+
* `WWW-Authenticate: Basic` header alongside the body. RFC 8628 §3.5 —
|
|
19
|
+
* device-flow polling errors (`authorization_pending`, `slow_down`,
|
|
20
|
+
* `expired_token`, `access_denied`) return HTTP 400; 429 is for transport-
|
|
21
|
+
* level rate-limiting, not the OAuth `slow_down` signal.
|
|
22
|
+
*/
|
|
23
|
+
export declare function registerTokenRoute(router: Router, prefix: string, mw: MiddlewareHandler[]): void;
|
|
24
|
+
//# sourceMappingURL=token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/routes/token.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAS5D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAGxC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,iBAAiB,EAAE,GAAG,IAAI,CAoGhG"}
|