@rudderjs/passport 1.1.1 → 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/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 +3 -3
|
@@ -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"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { report } from '@rudderjs/core';
|
|
2
|
+
import { exchangeAuthCode, clientCredentialsGrant, refreshTokenGrant, pollDeviceCode, OAuthError, } from '../grants/index.js';
|
|
3
|
+
import { resolveClientCredentials } from './helpers.js';
|
|
4
|
+
/**
|
|
5
|
+
* Register `POST /oauth/token` — the OAuth 2 token endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Dispatches to one of the four supported grants:
|
|
8
|
+
* - `authorization_code` — exchanges an auth code for tokens
|
|
9
|
+
* - `client_credentials` — machine-to-machine, confidential clients only
|
|
10
|
+
* - `refresh_token` — rotates an access+refresh pair
|
|
11
|
+
* - `urn:ietf:params:oauth:grant-type:device_code` — polls device flow
|
|
12
|
+
*
|
|
13
|
+
* `mw` runs ahead of the handler. The token endpoint is the canonical
|
|
14
|
+
* brute-force target for client_secret guessing — every production app
|
|
15
|
+
* SHOULD pass a per-route rate limiter here. See
|
|
16
|
+
* `PassportRouteOptions.tokenMiddleware` jsdoc for the recommended config.
|
|
17
|
+
*
|
|
18
|
+
* RFC 6749 §5.2 — client-auth failures (HTTP 401) are signalled with a
|
|
19
|
+
* `WWW-Authenticate: Basic` header alongside the body. RFC 8628 §3.5 —
|
|
20
|
+
* device-flow polling errors (`authorization_pending`, `slow_down`,
|
|
21
|
+
* `expired_token`, `access_denied`) return HTTP 400; 429 is for transport-
|
|
22
|
+
* level rate-limiting, not the OAuth `slow_down` signal.
|
|
23
|
+
*/
|
|
24
|
+
export function registerTokenRoute(router, prefix, mw) {
|
|
25
|
+
router.post(`${prefix}/token`, async (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
const body = req.body ?? {};
|
|
28
|
+
const grantType = body['grant_type'];
|
|
29
|
+
// RFC 6749 §2.3.1 — confidential clients MUST be able to
|
|
30
|
+
// authenticate via HTTP Basic; body params are an alternative.
|
|
31
|
+
// §2.3 forbids using both at once. Resolve credentials once for
|
|
32
|
+
// all grants instead of repeating the parsing in each branch.
|
|
33
|
+
const credentials = resolveClientCredentials(req, body);
|
|
34
|
+
let result;
|
|
35
|
+
switch (grantType) {
|
|
36
|
+
case 'authorization_code':
|
|
37
|
+
result = await exchangeAuthCode({
|
|
38
|
+
grantType,
|
|
39
|
+
code: body['code'],
|
|
40
|
+
...credentials,
|
|
41
|
+
redirectUri: body['redirect_uri'],
|
|
42
|
+
codeVerifier: body['code_verifier'],
|
|
43
|
+
});
|
|
44
|
+
break;
|
|
45
|
+
case 'client_credentials':
|
|
46
|
+
// ClientCredentialsRequest requires clientSecret (the grant
|
|
47
|
+
// is confidential-only by spec). Surface the missing-secret
|
|
48
|
+
// case as invalid_request rather than letting it surface
|
|
49
|
+
// downstream as "Invalid client secret."
|
|
50
|
+
if (credentials.clientSecret === undefined) {
|
|
51
|
+
throw new OAuthError('invalid_request', 'client_secret is required for the client_credentials grant.', 401);
|
|
52
|
+
}
|
|
53
|
+
result = await clientCredentialsGrant({
|
|
54
|
+
grantType,
|
|
55
|
+
clientId: credentials.clientId,
|
|
56
|
+
clientSecret: credentials.clientSecret,
|
|
57
|
+
scope: body['scope'],
|
|
58
|
+
});
|
|
59
|
+
break;
|
|
60
|
+
case 'refresh_token':
|
|
61
|
+
result = await refreshTokenGrant({
|
|
62
|
+
grantType,
|
|
63
|
+
refreshToken: body['refresh_token'],
|
|
64
|
+
...credentials,
|
|
65
|
+
scope: body['scope'],
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
case 'urn:ietf:params:oauth:grant-type:device_code': {
|
|
69
|
+
const pollResult = await pollDeviceCode({
|
|
70
|
+
grantType,
|
|
71
|
+
deviceCode: body['device_code'],
|
|
72
|
+
clientId: credentials.clientId,
|
|
73
|
+
});
|
|
74
|
+
if (pollResult.status === 'authorized') {
|
|
75
|
+
result = pollResult.tokens;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// RFC 8628 §3.5 — device-flow polling errors (including
|
|
79
|
+
// slow_down) are §5.2-shaped errors and MUST return HTTP
|
|
80
|
+
// 400. 429 is for transport-level rate-limiting, not the
|
|
81
|
+
// OAuth `slow_down` signal.
|
|
82
|
+
//
|
|
83
|
+
// On slow_down, forward the escalated `interval` so a
|
|
84
|
+
// well-behaved client uses the new value instead of having
|
|
85
|
+
// to add 5 itself. Other variants don't need it.
|
|
86
|
+
if (pollResult.status === 'slow_down') {
|
|
87
|
+
res.status(400).json({ error: 'slow_down', interval: pollResult.interval });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
res.status(400).json({ error: pollResult.status });
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
res.status(400).json({
|
|
98
|
+
error: 'unsupported_grant_type',
|
|
99
|
+
error_description: `Grant type "${grantType}" is not supported.`,
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
res.json(result);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
if (e instanceof OAuthError) {
|
|
107
|
+
// RFC 6749 §5.2 — client-auth failures at the token endpoint
|
|
108
|
+
// are signalled with WWW-Authenticate alongside the 401 status.
|
|
109
|
+
if (e.statusCode === 401 && typeof res.header === 'function') {
|
|
110
|
+
res.header('WWW-Authenticate', 'Basic realm="oauth"');
|
|
111
|
+
}
|
|
112
|
+
res.status(e.statusCode).json(e.toJSON());
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
report(e);
|
|
116
|
+
res.status(500).json({ error: 'server_error', error_description: 'Internal server error.' });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, mw);
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/routes/token.ts"],"names":[],"mappings":"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"}
|