@julr/sesame 0.5.1 → 0.6.0
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/LICENSE.md +1 -1
- package/README.md +405 -62
- package/build/authorize_controller-BiycO4be.js +251 -0
- package/build/chunk-DF48asd8.js +9 -0
- package/build/{client_info_controller-BucHGx4u.js → client_info_controller-AcOG8lWu.js} +11 -3
- package/build/commands/sesame_client.d.ts +20 -0
- package/build/commands/sesame_key.d.ts +12 -0
- package/build/commands/sesame_purge.d.ts +0 -2
- package/build/commands/sesame_purge.js +15 -3
- package/build/configure-DkDkIlt8.js +27 -0
- package/build/configure.js +2 -24
- package/build/consent_controller-Dsdhv6-f.js +108 -0
- package/build/id_token_service-CpTzOUDe.js +54 -0
- package/build/index.d.ts +1 -1
- package/build/index.js +30 -10
- package/build/{introspect_controller-6bRt9sZt.js → introspect_controller-DvOp9scr.js} +21 -7
- package/build/issue_authorization_code-B9ERu1uO.js +40 -0
- package/build/jwks_controller-keo4kBZc.js +26 -0
- package/build/{main-EbeMS5S9.js → main-DGBJhq3E.js} +34 -4
- package/build/{metadata_controller-DeaMRnUr.js → metadata_controller-BVsTo0Gp.js} +83 -6
- package/build/{oauth_access_token-bsoM5KeU.js → oauth_access_token-Cz_5gNBx.js} +12 -1
- package/build/oauth_client-BSanvSql.js +63 -0
- package/build/oauth_error-C7UhDb2q.js +189 -0
- package/build/providers/sesame_provider.js +14 -3
- package/build/{register_controller-sIJ1rxdM.js → register_controller-gbq7p8a5.js} +46 -7
- package/build/{revoke_controller-D6isoQCi.js → revoke_controller-z_ghrEB7.js} +21 -8
- package/build/services/main.js +7 -3
- package/build/sesame_manager-B1Jgq1v2.js +6 -0
- package/build/sesame_manager-DYUSZ0NC.js +693 -0
- package/build/src/actions/authorize.d.ts +46 -0
- package/build/src/actions/exchange_authorization_code.d.ts +34 -0
- package/build/src/actions/exchange_client_credentials.d.ts +28 -0
- package/build/src/actions/exchange_refresh_token.d.ts +59 -0
- package/build/src/actions/issue_authorization_code.d.ts +26 -0
- package/build/src/controllers/authorize_controller.d.ts +13 -12
- package/build/src/controllers/consent_controller.d.ts +5 -0
- package/build/src/controllers/jwks_controller.d.ts +14 -0
- package/build/src/controllers/metadata_controller.d.ts +8 -1
- package/build/src/controllers/token_controller.d.ts +8 -5
- package/build/src/controllers/userinfo_controller.d.ts +14 -0
- package/build/src/guard/main.js +5 -5
- package/build/src/middleware/any_scope_middleware.js +11 -1
- package/build/src/middleware/scope_middleware.js +11 -1
- package/build/src/models/oauth_authorization_code.d.ts +1 -0
- package/build/src/models/oauth_pending_authorization_request.d.ts +1 -0
- package/build/src/oauth_error.d.ts +1 -1
- package/build/src/routes.d.ts +3 -1
- package/build/src/services/id_token_service.d.ts +30 -0
- package/build/src/services/key_service.d.ts +20 -0
- package/build/src/sesame_manager.d.ts +54 -3
- package/build/src/types.d.ts +112 -0
- package/build/stubs/main.ts +5 -0
- package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +1 -0
- package/build/stubs/migrations/create_oauth_pending_authorization_requests_table.stub +1 -0
- package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +1 -1
- package/build/token_controller-DyI7oy-U.js +481 -0
- package/build/token_service-DwnfAR9F.js +59 -0
- package/build/userinfo_controller-RLk8cN_o.js +40 -0
- package/build/vite.config.d.ts +2 -0
- package/package.json +26 -41
- package/build/authorize_controller-YUfAy-R2.js +0 -138
- package/build/client_service-WTNMqWzY.js +0 -65
- package/build/consent_controller-Dprwd1ed.js +0 -85
- package/build/decorate-BKZEjPRg.js +0 -15
- package/build/oauth_client-BIoY5jBR.js +0 -24
- package/build/oauth_error-CnJ3L8tf.js +0 -94
- package/build/sesame_manager-Bu4MHqZV.js +0 -4
- package/build/sesame_manager-DwDZy5Vy.js +0 -167
- package/build/src/grants/authorization_code_grant.d.ts +0 -23
- package/build/src/grants/client_credentials_grant.d.ts +0 -23
- package/build/src/grants/refresh_token_grant.d.ts +0 -27
- package/build/token_controller-DzcrLMyS.js +0 -194
- package/build/token_service-fhoA4slP.js +0 -31
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { n as json, t as __decorate } from "./decorate-BKZEjPRg.js";
|
|
2
|
-
import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
|
|
3
|
-
import { DateTime } from "luxon";
|
|
4
|
-
import { BaseModel, column } from "@adonisjs/lucid/orm";
|
|
5
|
-
const BUILTIN_SCOPES = new Set(["offline_access"]);
|
|
6
|
-
const controllers = {
|
|
7
|
-
token: () => import("./token_controller-DzcrLMyS.js"),
|
|
8
|
-
authorize: () => import("./authorize_controller-YUfAy-R2.js"),
|
|
9
|
-
consent: () => import("./consent_controller-Dprwd1ed.js"),
|
|
10
|
-
introspect: () => import("./introspect_controller-6bRt9sZt.js"),
|
|
11
|
-
revoke: () => import("./revoke_controller-D6isoQCi.js"),
|
|
12
|
-
register: () => import("./register_controller-sIJ1rxdM.js"),
|
|
13
|
-
metadata: () => import("./metadata_controller-DeaMRnUr.js"),
|
|
14
|
-
clientInfo: () => import("./client_info_controller-BucHGx4u.js")
|
|
15
|
-
};
|
|
16
|
-
function registerOAuthRoutes(router) {
|
|
17
|
-
router.post("/token", [controllers.token]).as("sesame.token");
|
|
18
|
-
router.get("/authorize", [controllers.authorize]).as("sesame.authorize");
|
|
19
|
-
router.post("/consent", [controllers.consent]).as("sesame.consent");
|
|
20
|
-
router.get("/client-info", [controllers.clientInfo]).as("sesame.clientInfo");
|
|
21
|
-
router.post("/introspect", [controllers.introspect]).as("sesame.introspect");
|
|
22
|
-
router.post("/revoke", [controllers.revoke]).as("sesame.revoke");
|
|
23
|
-
router.post("/register", [controllers.register]).as("sesame.register");
|
|
24
|
-
}
|
|
25
|
-
function registerWellKnownRoutes(router) {
|
|
26
|
-
router.get("/.well-known/oauth-authorization-server", [controllers.metadata, "authServer"]).as("sesame.metadata.authServer");
|
|
27
|
-
router.get("/.well-known/openid-configuration", [controllers.metadata, "oidc"]).as("sesame.metadata.oidc");
|
|
28
|
-
router.get("/.well-known/oauth-protected-resource", [controllers.metadata, "protectedResource"]).as("sesame.metadata.protectedResource");
|
|
29
|
-
}
|
|
30
|
-
var OAuthRefreshToken = class extends BaseModel {
|
|
31
|
-
static table = "oauth_refresh_tokens";
|
|
32
|
-
};
|
|
33
|
-
__decorate([column({ isPrimary: true })], OAuthRefreshToken.prototype, "id", void 0);
|
|
34
|
-
__decorate([column({ serializeAs: null })], OAuthRefreshToken.prototype, "token", void 0);
|
|
35
|
-
__decorate([column()], OAuthRefreshToken.prototype, "accessTokenId", void 0);
|
|
36
|
-
__decorate([column()], OAuthRefreshToken.prototype, "clientId", void 0);
|
|
37
|
-
__decorate([column()], OAuthRefreshToken.prototype, "userId", void 0);
|
|
38
|
-
__decorate([json()], OAuthRefreshToken.prototype, "scopes", void 0);
|
|
39
|
-
__decorate([column.dateTime()], OAuthRefreshToken.prototype, "expiresAt", void 0);
|
|
40
|
-
__decorate([column.dateTime()], OAuthRefreshToken.prototype, "revokedAt", void 0);
|
|
41
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthRefreshToken.prototype, "createdAt", void 0);
|
|
42
|
-
__decorate([column.dateTime({
|
|
43
|
-
autoCreate: true,
|
|
44
|
-
autoUpdate: true
|
|
45
|
-
})], OAuthRefreshToken.prototype, "updatedAt", void 0);
|
|
46
|
-
var OAuthAuthorizationCode = class extends BaseModel {
|
|
47
|
-
static table = "oauth_authorization_codes";
|
|
48
|
-
};
|
|
49
|
-
__decorate([column({ isPrimary: true })], OAuthAuthorizationCode.prototype, "id", void 0);
|
|
50
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "code", void 0);
|
|
51
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "clientId", void 0);
|
|
52
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "userId", void 0);
|
|
53
|
-
__decorate([json()], OAuthAuthorizationCode.prototype, "scopes", void 0);
|
|
54
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "redirectUri", void 0);
|
|
55
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "codeChallenge", void 0);
|
|
56
|
-
__decorate([column()], OAuthAuthorizationCode.prototype, "codeChallengeMethod", void 0);
|
|
57
|
-
__decorate([column.dateTime()], OAuthAuthorizationCode.prototype, "expiresAt", void 0);
|
|
58
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthAuthorizationCode.prototype, "createdAt", void 0);
|
|
59
|
-
__decorate([column.dateTime({
|
|
60
|
-
autoCreate: true,
|
|
61
|
-
autoUpdate: true
|
|
62
|
-
})], OAuthAuthorizationCode.prototype, "updatedAt", void 0);
|
|
63
|
-
var OAuthConsent = class extends BaseModel {
|
|
64
|
-
static table = "oauth_consents";
|
|
65
|
-
};
|
|
66
|
-
__decorate([column({ isPrimary: true })], OAuthConsent.prototype, "id", void 0);
|
|
67
|
-
__decorate([column()], OAuthConsent.prototype, "clientId", void 0);
|
|
68
|
-
__decorate([column()], OAuthConsent.prototype, "userId", void 0);
|
|
69
|
-
__decorate([json()], OAuthConsent.prototype, "scopes", void 0);
|
|
70
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthConsent.prototype, "createdAt", void 0);
|
|
71
|
-
__decorate([column.dateTime({
|
|
72
|
-
autoCreate: true,
|
|
73
|
-
autoUpdate: true
|
|
74
|
-
})], OAuthConsent.prototype, "updatedAt", void 0);
|
|
75
|
-
var OAuthPendingAuthorizationRequest = class extends BaseModel {
|
|
76
|
-
static table = "oauth_pending_authorization_requests";
|
|
77
|
-
};
|
|
78
|
-
__decorate([column({ isPrimary: true })], OAuthPendingAuthorizationRequest.prototype, "id", void 0);
|
|
79
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "token", void 0);
|
|
80
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "userId", void 0);
|
|
81
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "clientId", void 0);
|
|
82
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "redirectUri", void 0);
|
|
83
|
-
__decorate([json()], OAuthPendingAuthorizationRequest.prototype, "scopes", void 0);
|
|
84
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "state", void 0);
|
|
85
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallenge", void 0);
|
|
86
|
-
__decorate([column()], OAuthPendingAuthorizationRequest.prototype, "codeChallengeMethod", void 0);
|
|
87
|
-
__decorate([column.dateTime()], OAuthPendingAuthorizationRequest.prototype, "expiresAt", void 0);
|
|
88
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthPendingAuthorizationRequest.prototype, "createdAt", void 0);
|
|
89
|
-
var SesameManager = class {
|
|
90
|
-
#config;
|
|
91
|
-
#router;
|
|
92
|
-
constructor(config, router) {
|
|
93
|
-
this.#config = config;
|
|
94
|
-
this.#router = router;
|
|
95
|
-
}
|
|
96
|
-
get config() {
|
|
97
|
-
return this.#config;
|
|
98
|
-
}
|
|
99
|
-
hasScope(scope) {
|
|
100
|
-
return scope in this.#config.scopes;
|
|
101
|
-
}
|
|
102
|
-
validateScopes(scopes) {
|
|
103
|
-
if (Object.keys(this.#config.scopes).length === 0) return scopes.filter((s) => !BUILTIN_SCOPES.has(s));
|
|
104
|
-
return scopes.filter((s) => !BUILTIN_SCOPES.has(s) && !this.hasScope(s));
|
|
105
|
-
}
|
|
106
|
-
isGrantTypeEnabled(grantType) {
|
|
107
|
-
return this.#config.grantTypes.includes(grantType);
|
|
108
|
-
}
|
|
109
|
-
async revokeAllForUser(userId) {
|
|
110
|
-
const now = DateTime.now();
|
|
111
|
-
await OAuthAccessToken.query().where("userId", userId).whereNull("revokedAt").update({ revokedAt: now.toSQL() });
|
|
112
|
-
await OAuthRefreshToken.query().where("userId", userId).whereNull("revokedAt").update({ revokedAt: now.toSQL() });
|
|
113
|
-
await OAuthAuthorizationCode.query().where("userId", userId).delete();
|
|
114
|
-
await OAuthPendingAuthorizationRequest.query().where("userId", userId).delete();
|
|
115
|
-
await OAuthConsent.query().where("userId", userId).delete();
|
|
116
|
-
}
|
|
117
|
-
async purgeTokens(options) {
|
|
118
|
-
const revokedOnly = options?.revokedOnly ?? false;
|
|
119
|
-
const expiredOnly = options?.expiredOnly ?? false;
|
|
120
|
-
const retentionHours = options?.retentionHours ?? 168;
|
|
121
|
-
const purgeRevoked = revokedOnly || !expiredOnly;
|
|
122
|
-
const purgeExpired = expiredOnly || !revokedOnly;
|
|
123
|
-
const cutoff = DateTime.now().minus({ hours: retentionHours });
|
|
124
|
-
let accessTokens = 0;
|
|
125
|
-
let refreshTokens = 0;
|
|
126
|
-
let authorizationCodes = 0;
|
|
127
|
-
let pendingRequests = 0;
|
|
128
|
-
if (purgeRevoked) {
|
|
129
|
-
accessTokens += await this.#deleteCount(OAuthAccessToken.query().whereNotNull("revokedAt").delete());
|
|
130
|
-
refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().whereNotNull("revokedAt").delete());
|
|
131
|
-
}
|
|
132
|
-
if (purgeExpired) {
|
|
133
|
-
accessTokens += await this.#deleteCount(OAuthAccessToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
|
|
134
|
-
refreshTokens += await this.#deleteCount(OAuthRefreshToken.query().where("expiresAt", "<", cutoff.toSQL()).whereNull("revokedAt").delete());
|
|
135
|
-
authorizationCodes += await this.#deleteCount(OAuthAuthorizationCode.query().where("expiresAt", "<", cutoff.toSQL()).delete());
|
|
136
|
-
}
|
|
137
|
-
pendingRequests += await this.#deleteCount(OAuthPendingAuthorizationRequest.query().where("expiresAt", "<", DateTime.now().toSQL()).delete());
|
|
138
|
-
return {
|
|
139
|
-
accessTokens,
|
|
140
|
-
refreshTokens,
|
|
141
|
-
authorizationCodes,
|
|
142
|
-
pendingRequests
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
registerRoutes() {
|
|
146
|
-
registerOAuthRoutes(this.#router);
|
|
147
|
-
}
|
|
148
|
-
registerWellKnownRoutes() {
|
|
149
|
-
registerWellKnownRoutes(this.#router);
|
|
150
|
-
}
|
|
151
|
-
registerProtectedResource(options) {
|
|
152
|
-
const wellKnownPath = `/.well-known/oauth-protected-resource${options.resource}`;
|
|
153
|
-
this.#router.get(wellKnownPath, async (ctx) => {
|
|
154
|
-
ctx.response.header("Cache-Control", "public, max-age=15, stale-while-revalidate=15, stale-if-error=86400");
|
|
155
|
-
return {
|
|
156
|
-
resource: `${this.#config.issuer}${options.resource}`,
|
|
157
|
-
authorization_servers: [this.#config.issuer],
|
|
158
|
-
scopes_supported: [...options.scopes ?? Object.keys(this.#config.scopes), ...BUILTIN_SCOPES],
|
|
159
|
-
bearer_methods_supported: ["header"]
|
|
160
|
-
};
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
#deleteCount(result) {
|
|
164
|
-
return result.then((r) => Array.isArray(r) ? Number(r[0] ?? 0) : Number(r));
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
export { OAuthRefreshToken as a, OAuthAuthorizationCode as i, OAuthPendingAuthorizationRequest as n, BUILTIN_SCOPES as o, OAuthConsent as r, SesameManager as t };
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
-
import type { SesameManager } from '../sesame_manager.ts';
|
|
3
|
-
/**
|
|
4
|
-
* Handle the Authorization Code Grant (RFC 6749 §4.1.3).
|
|
5
|
-
*
|
|
6
|
-
* Exchanges an authorization code for an access token and a refresh
|
|
7
|
-
* token. A refresh token is always issued when the `refresh_token`
|
|
8
|
-
* grant type is enabled on the server — the client does not need
|
|
9
|
-
* to request `offline_access` explicitly.
|
|
10
|
-
*
|
|
11
|
-
* This matches the behavior of major OAuth providers and avoids
|
|
12
|
-
* forcing MCP clients like ClaudeDesktop to know about `offline_access` to get long-lived sessions.
|
|
13
|
-
*
|
|
14
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
|
|
15
|
-
* @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6
|
|
16
|
-
*/
|
|
17
|
-
export declare function handleAuthorizationCodeGrant(ctx: HttpContext, manager: SesameManager): Promise<{
|
|
18
|
-
refresh_token?: string | undefined;
|
|
19
|
-
access_token: string;
|
|
20
|
-
token_type: string;
|
|
21
|
-
expires_in: number;
|
|
22
|
-
scope: string;
|
|
23
|
-
}>;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
-
import type { SesameManager } from '../sesame_manager.ts';
|
|
3
|
-
/**
|
|
4
|
-
* Handle the Client Credentials Grant (RFC 6749 §4.4).
|
|
5
|
-
*
|
|
6
|
-
* Issues an access token directly to the client for
|
|
7
|
-
* machine-to-machine (M2M) communication. The client
|
|
8
|
-
* authenticates with its own credentials and receives an
|
|
9
|
-
* access token without an interactive user step.
|
|
10
|
-
*
|
|
11
|
-
* No refresh token is issued (per spec and convention).
|
|
12
|
-
*
|
|
13
|
-
* Built-in OIDC scopes (e.g. `offline_access`) are rejected
|
|
14
|
-
* since they are user-centric and meaningless in an M2M context.
|
|
15
|
-
*
|
|
16
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.4
|
|
17
|
-
*/
|
|
18
|
-
export declare function handleClientCredentialsGrant(ctx: HttpContext, manager: SesameManager): Promise<{
|
|
19
|
-
access_token: string;
|
|
20
|
-
token_type: string;
|
|
21
|
-
expires_in: number;
|
|
22
|
-
scope: string;
|
|
23
|
-
}>;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
-
import type { SesameManager } from '../sesame_manager.ts';
|
|
3
|
-
/**
|
|
4
|
-
* Handle the Refresh Token Grant (RFC 6749 §6).
|
|
5
|
-
*
|
|
6
|
-
* Exchanges a refresh token for a new access token and a new
|
|
7
|
-
* refresh token (rotation). The old refresh token is revoked
|
|
8
|
-
* immediately after use.
|
|
9
|
-
*
|
|
10
|
-
* Implements replay detection: if a revoked refresh token is
|
|
11
|
-
* presented, all tokens for that client+user pair are nuked
|
|
12
|
-
* (both access and refresh tokens) to mitigate stolen token
|
|
13
|
-
* reuse attacks.
|
|
14
|
-
*
|
|
15
|
-
* Scope narrowing is supported: the client may request a subset
|
|
16
|
-
* of the originally granted scopes, but cannot request new ones.
|
|
17
|
-
*
|
|
18
|
-
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
|
|
19
|
-
* @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.14.2
|
|
20
|
-
*/
|
|
21
|
-
export declare function handleRefreshTokenGrant(ctx: HttpContext, manager: SesameManager): Promise<{
|
|
22
|
-
access_token: string;
|
|
23
|
-
token_type: string;
|
|
24
|
-
expires_in: number;
|
|
25
|
-
scope: string;
|
|
26
|
-
refresh_token: string;
|
|
27
|
-
}>;
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, o as BUILTIN_SCOPES, t as SesameManager } from "./sesame_manager-DwDZy5Vy.js";
|
|
2
|
-
import "./decorate-BKZEjPRg.js";
|
|
3
|
-
import { t as OAuthAccessToken } from "./oauth_access_token-bsoM5KeU.js";
|
|
4
|
-
import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE, u as E_UNSUPPORTED_GRANT_TYPE } from "./oauth_error-CnJ3L8tf.js";
|
|
5
|
-
import "./oauth_client-BIoY5jBR.js";
|
|
6
|
-
import { t as TokenService } from "./token_service-fhoA4slP.js";
|
|
7
|
-
import { t as ClientService } from "./client_service-WTNMqWzY.js";
|
|
8
|
-
import { DateTime } from "luxon";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import string from "@adonisjs/core/helpers/string";
|
|
11
|
-
import vine from "@vinejs/vine";
|
|
12
|
-
const codeVerifierValidator = vine.create({ code_verifier: vine.string().minLength(43).maxLength(128).regex(/^[A-Za-z0-9\-._~]+$/) });
|
|
13
|
-
async function handleAuthorizationCodeGrant(ctx, manager) {
|
|
14
|
-
const tokenService = new TokenService(manager);
|
|
15
|
-
const clientService = new ClientService();
|
|
16
|
-
const body = ctx.request.body();
|
|
17
|
-
const code = body.code;
|
|
18
|
-
const redirectUri = body.redirect_uri;
|
|
19
|
-
const codeVerifier = body.code_verifier;
|
|
20
|
-
if (!code) throw new E_INVALID_REQUEST("Missing required parameter: code");
|
|
21
|
-
if (!redirectUri) throw new E_INVALID_REQUEST("Missing required parameter: redirect_uri");
|
|
22
|
-
const client = await clientService.authenticateClient({
|
|
23
|
-
authorizationHeader: ctx.request.header("authorization"),
|
|
24
|
-
bodyClientId: body.client_id,
|
|
25
|
-
bodyClientSecret: body.client_secret
|
|
26
|
-
});
|
|
27
|
-
if (!client.grantTypes.includes("authorization_code")) throw new E_INVALID_CLIENT("Client is not allowed to use the authorization_code grant");
|
|
28
|
-
const hashedCode = tokenService.hashToken(code);
|
|
29
|
-
const authCode = await OAuthAuthorizationCode.query().where("code", hashedCode).where("clientId", client.clientId).first();
|
|
30
|
-
if (!authCode) throw new E_INVALID_GRANT("Authorization code not found");
|
|
31
|
-
if (authCode.expiresAt < DateTime.now()) {
|
|
32
|
-
await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
|
|
33
|
-
throw new E_INVALID_GRANT("Authorization code has expired");
|
|
34
|
-
}
|
|
35
|
-
if (authCode.redirectUri !== redirectUri) throw new E_INVALID_GRANT("Redirect URI mismatch");
|
|
36
|
-
const deleteResult = await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
|
|
37
|
-
if ((Array.isArray(deleteResult) ? Number(deleteResult[0] ?? 0) : Number(deleteResult)) !== 1) throw new E_INVALID_GRANT("Authorization code has already been consumed");
|
|
38
|
-
const [verifierError] = await codeVerifierValidator.tryValidate({ code_verifier: codeVerifier });
|
|
39
|
-
if (verifierError) throw new E_INVALID_REQUEST("code_verifier must be 43-128 characters using only [A-Za-z0-9-._~] (RFC 7636 §4.1)");
|
|
40
|
-
if (!authCode.codeChallenge) throw new E_INVALID_GRANT("Authorization code is missing PKCE challenge");
|
|
41
|
-
if (createHash("sha256").update(codeVerifier).digest("base64url") !== authCode.codeChallenge) throw new E_INVALID_GRANT("PKCE verification failed");
|
|
42
|
-
clientService.validateClientScopes(authCode.scopes, client.scopes);
|
|
43
|
-
const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
|
|
44
|
-
await OAuthAccessToken.create({
|
|
45
|
-
id: crypto.randomUUID(),
|
|
46
|
-
tokenHash,
|
|
47
|
-
clientId: client.clientId,
|
|
48
|
-
userId: authCode.userId,
|
|
49
|
-
scopes: authCode.scopes,
|
|
50
|
-
expiresAt: DateTime.fromJSDate(expiresAt)
|
|
51
|
-
});
|
|
52
|
-
let refreshTokenRaw;
|
|
53
|
-
if (manager.isGrantTypeEnabled("refresh_token")) {
|
|
54
|
-
const { raw, hash } = tokenService.createRefreshToken();
|
|
55
|
-
const refreshTtl = string.seconds.parse(manager.config.refreshTokenTtl);
|
|
56
|
-
await OAuthRefreshToken.create({
|
|
57
|
-
id: crypto.randomUUID(),
|
|
58
|
-
token: hash,
|
|
59
|
-
accessTokenId: tokenHash,
|
|
60
|
-
clientId: client.clientId,
|
|
61
|
-
userId: authCode.userId,
|
|
62
|
-
scopes: authCode.scopes,
|
|
63
|
-
expiresAt: DateTime.now().plus({ seconds: refreshTtl })
|
|
64
|
-
});
|
|
65
|
-
refreshTokenRaw = raw;
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
access_token: accessTokenRaw,
|
|
69
|
-
token_type: "Bearer",
|
|
70
|
-
expires_in: string.seconds.parse(manager.config.accessTokenTtl),
|
|
71
|
-
scope: authCode.scopes.join(" "),
|
|
72
|
-
...refreshTokenRaw ? { refresh_token: refreshTokenRaw } : {}
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
async function handleRefreshTokenGrant(ctx, manager) {
|
|
76
|
-
const tokenService = new TokenService(manager);
|
|
77
|
-
const clientService = new ClientService();
|
|
78
|
-
const body = ctx.request.body();
|
|
79
|
-
const refreshTokenRaw = body.refresh_token;
|
|
80
|
-
if (!refreshTokenRaw) throw new E_INVALID_REQUEST("Missing required parameter: refresh_token");
|
|
81
|
-
const client = await clientService.authenticateClient({
|
|
82
|
-
authorizationHeader: ctx.request.header("authorization"),
|
|
83
|
-
bodyClientId: body.client_id,
|
|
84
|
-
bodyClientSecret: body.client_secret
|
|
85
|
-
});
|
|
86
|
-
if (!client.grantTypes.includes("refresh_token")) throw new E_INVALID_CLIENT("Client is not allowed to use the refresh_token grant");
|
|
87
|
-
const hashedToken = tokenService.hashToken(refreshTokenRaw);
|
|
88
|
-
const refreshToken = await OAuthRefreshToken.query().where("token", hashedToken).where("clientId", client.clientId).first();
|
|
89
|
-
if (!refreshToken) throw new E_INVALID_GRANT("Refresh token not found");
|
|
90
|
-
if (refreshToken.revokedAt) {
|
|
91
|
-
await OAuthRefreshToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).delete();
|
|
92
|
-
await OAuthAccessToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).whereNull("revokedAt").update({ revokedAt: DateTime.now().toSQL() });
|
|
93
|
-
throw new E_INVALID_GRANT("Refresh token has been revoked (possible replay attack)");
|
|
94
|
-
}
|
|
95
|
-
if (refreshToken.expiresAt < DateTime.now()) throw new E_INVALID_GRANT("Refresh token has expired");
|
|
96
|
-
const requestedScope = body.scope;
|
|
97
|
-
let scopes = refreshToken.scopes;
|
|
98
|
-
if (requestedScope) {
|
|
99
|
-
const requested = requestedScope.split(" ");
|
|
100
|
-
const originalSet = new Set(refreshToken.scopes);
|
|
101
|
-
const invalid = requested.filter((s) => !originalSet.has(s));
|
|
102
|
-
if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not in original grant: ${invalid.join(", ")}`);
|
|
103
|
-
scopes = requested;
|
|
104
|
-
}
|
|
105
|
-
clientService.validateClientScopes(scopes, client.scopes);
|
|
106
|
-
const revokedAt = DateTime.now();
|
|
107
|
-
const updateResult = await OAuthRefreshToken.query().where("id", refreshToken.id).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
|
|
108
|
-
if ((Array.isArray(updateResult) ? Number(updateResult[0] ?? 0) : Number(updateResult)) !== 1) throw new E_INVALID_GRANT("Refresh token has already been consumed");
|
|
109
|
-
await OAuthAccessToken.query().where("tokenHash", refreshToken.accessTokenId).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
|
|
110
|
-
const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
|
|
111
|
-
await OAuthAccessToken.create({
|
|
112
|
-
id: crypto.randomUUID(),
|
|
113
|
-
tokenHash,
|
|
114
|
-
clientId: client.clientId,
|
|
115
|
-
userId: refreshToken.userId,
|
|
116
|
-
scopes,
|
|
117
|
-
expiresAt: DateTime.fromJSDate(expiresAt)
|
|
118
|
-
});
|
|
119
|
-
const { raw: newRefreshTokenRaw, hash: newRefreshTokenHash } = tokenService.createRefreshToken();
|
|
120
|
-
const refreshTtl = string.seconds.parse(manager.config.refreshTokenTtl);
|
|
121
|
-
await OAuthRefreshToken.create({
|
|
122
|
-
id: crypto.randomUUID(),
|
|
123
|
-
token: newRefreshTokenHash,
|
|
124
|
-
accessTokenId: tokenHash,
|
|
125
|
-
clientId: client.clientId,
|
|
126
|
-
userId: refreshToken.userId,
|
|
127
|
-
scopes,
|
|
128
|
-
expiresAt: DateTime.now().plus({ seconds: refreshTtl })
|
|
129
|
-
});
|
|
130
|
-
return {
|
|
131
|
-
access_token: accessTokenRaw,
|
|
132
|
-
token_type: "Bearer",
|
|
133
|
-
expires_in: string.seconds.parse(manager.config.accessTokenTtl),
|
|
134
|
-
scope: scopes.join(" "),
|
|
135
|
-
refresh_token: newRefreshTokenRaw
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
async function handleClientCredentialsGrant(ctx, manager) {
|
|
139
|
-
const tokenService = new TokenService(manager);
|
|
140
|
-
const clientService = new ClientService();
|
|
141
|
-
const body = ctx.request.body();
|
|
142
|
-
const client = await clientService.authenticateClient({
|
|
143
|
-
authorizationHeader: ctx.request.header("authorization"),
|
|
144
|
-
bodyClientId: body.client_id,
|
|
145
|
-
bodyClientSecret: body.client_secret
|
|
146
|
-
});
|
|
147
|
-
if (client.isPublic) throw new E_INVALID_CLIENT("Public clients cannot use the client_credentials grant");
|
|
148
|
-
if (!client.grantTypes.includes("client_credentials")) throw new E_INVALID_CLIENT("Client is not allowed to use the client_credentials grant");
|
|
149
|
-
const requestedScopes = body.scope ? body.scope.split(" ") : client.scopes.filter((scope) => !BUILTIN_SCOPES.has(scope));
|
|
150
|
-
const builtinRequested = requestedScopes.filter((s) => BUILTIN_SCOPES.has(s));
|
|
151
|
-
if (builtinRequested.length > 0) throw new E_INVALID_SCOPE(`Scopes not allowed for client_credentials: ${builtinRequested.join(", ")}`);
|
|
152
|
-
const invalidScopes = manager.validateScopes(requestedScopes);
|
|
153
|
-
if (invalidScopes.length > 0) throw new E_INVALID_SCOPE(`Invalid scopes: ${invalidScopes.join(", ")}`);
|
|
154
|
-
clientService.validateClientScopes(requestedScopes, client.scopes);
|
|
155
|
-
if (!client.userId) throw new E_INVALID_CLIENT("Client must be associated with a user to use the client_credentials grant");
|
|
156
|
-
const ttlSeconds = string.seconds.parse(manager.config.clientCredentialsAccessTokenTtl);
|
|
157
|
-
const { raw: accessTokenRaw, hash: tokenHash } = tokenService.createAccessToken();
|
|
158
|
-
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
159
|
-
await OAuthAccessToken.create({
|
|
160
|
-
id: crypto.randomUUID(),
|
|
161
|
-
tokenHash,
|
|
162
|
-
clientId: client.clientId,
|
|
163
|
-
userId: client.userId,
|
|
164
|
-
scopes: requestedScopes,
|
|
165
|
-
expiresAt: DateTime.fromJSDate(expiresAt)
|
|
166
|
-
});
|
|
167
|
-
return {
|
|
168
|
-
access_token: accessTokenRaw,
|
|
169
|
-
token_type: "Bearer",
|
|
170
|
-
expires_in: ttlSeconds,
|
|
171
|
-
scope: requestedScopes.join(" ")
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
const grantHandlers = {
|
|
175
|
-
authorization_code: handleAuthorizationCodeGrant,
|
|
176
|
-
refresh_token: handleRefreshTokenGrant,
|
|
177
|
-
client_credentials: handleClientCredentialsGrant
|
|
178
|
-
};
|
|
179
|
-
var TokenController = class TokenController {
|
|
180
|
-
static validator = vine.create({ grant_type: vine.string() });
|
|
181
|
-
async handle(ctx) {
|
|
182
|
-
const manager = await ctx.containerResolver.make(SesameManager);
|
|
183
|
-
const [error, body] = await TokenController.validator.tryValidate(ctx.request.body());
|
|
184
|
-
if (error) throw new E_UNSUPPORTED_GRANT_TYPE("Missing required parameter: grant_type");
|
|
185
|
-
if (!manager.isGrantTypeEnabled(body.grant_type)) throw new E_UNSUPPORTED_GRANT_TYPE(`Grant type "${body.grant_type}" is not enabled`);
|
|
186
|
-
const handler = grantHandlers[body.grant_type];
|
|
187
|
-
if (!handler) throw new E_UNSUPPORTED_GRANT_TYPE(`Unsupported grant type: ${body.grant_type}`);
|
|
188
|
-
const result = await handler(ctx, manager);
|
|
189
|
-
ctx.response.header("Cache-Control", "no-store");
|
|
190
|
-
ctx.response.header("Pragma", "no-cache");
|
|
191
|
-
return result;
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
export { TokenController as default };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import string from "@adonisjs/core/helpers/string";
|
|
3
|
-
var TokenService = class {
|
|
4
|
-
#manager;
|
|
5
|
-
constructor(manager) {
|
|
6
|
-
this.#manager = manager;
|
|
7
|
-
}
|
|
8
|
-
createAccessToken() {
|
|
9
|
-
const raw = this.generateOpaqueToken();
|
|
10
|
-
const ttlSeconds = string.seconds.parse(this.#manager.config.accessTokenTtl);
|
|
11
|
-
return {
|
|
12
|
-
raw,
|
|
13
|
-
hash: this.hashToken(raw),
|
|
14
|
-
expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
createRefreshToken() {
|
|
18
|
-
const raw = this.generateOpaqueToken();
|
|
19
|
-
return {
|
|
20
|
-
raw,
|
|
21
|
-
hash: this.hashToken(raw)
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
generateOpaqueToken() {
|
|
25
|
-
return randomBytes(32).toString("base64url");
|
|
26
|
-
}
|
|
27
|
-
hashToken(token) {
|
|
28
|
-
return createHash("sha256").update(token).digest("base64url");
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
export { TokenService as t };
|