@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import string from "@adonisjs/core/helpers/string";
|
|
3
|
+
//#region src/services/token_service.ts
|
|
4
|
+
/**
|
|
5
|
+
* Handles opaque token generation for access tokens, refresh tokens,
|
|
6
|
+
* and authorization codes.
|
|
7
|
+
*
|
|
8
|
+
* All tokens are random opaque values. Only their SHA-256 hashes
|
|
9
|
+
* are stored in the database, so the raw tokens cannot be
|
|
10
|
+
* reconstructed from a database leak.
|
|
11
|
+
*/
|
|
12
|
+
var TokenService = class {
|
|
13
|
+
#manager;
|
|
14
|
+
constructor(manager) {
|
|
15
|
+
this.#manager = manager;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create an opaque access token. Returns the raw token
|
|
19
|
+
* (sent to the client), its SHA-256 hash (stored in DB),
|
|
20
|
+
* and the computed expiration date.
|
|
21
|
+
*/
|
|
22
|
+
createAccessToken() {
|
|
23
|
+
const raw = this.generateOpaqueToken();
|
|
24
|
+
const ttlSeconds = string.seconds.parse(this.#manager.config.accessTokenTtl);
|
|
25
|
+
return {
|
|
26
|
+
raw,
|
|
27
|
+
hash: this.hashToken(raw),
|
|
28
|
+
expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create an opaque refresh token. Returns the raw token
|
|
33
|
+
* (sent to the client) and its SHA-256 hash (stored in DB).
|
|
34
|
+
*/
|
|
35
|
+
createRefreshToken() {
|
|
36
|
+
const raw = this.generateOpaqueToken();
|
|
37
|
+
return {
|
|
38
|
+
raw,
|
|
39
|
+
hash: this.hashToken(raw)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generate a cryptographically random opaque token
|
|
44
|
+
* (32 bytes, base64url-encoded).
|
|
45
|
+
*/
|
|
46
|
+
generateOpaqueToken() {
|
|
47
|
+
return randomBytes(32).toString("base64url");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* SHA-256 hash a token value for secure storage.
|
|
51
|
+
* All tokens (codes, access tokens, refresh tokens) are stored
|
|
52
|
+
* as hashes so raw values cannot be reconstructed from a DB leak.
|
|
53
|
+
*/
|
|
54
|
+
hashToken(token) {
|
|
55
|
+
return createHash("sha256").update(token).digest("base64url");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { TokenService as t };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "./chunk-DF48asd8.js";
|
|
2
|
+
import { t as SesameManager } from "./sesame_manager-DYUSZ0NC.js";
|
|
3
|
+
import "./oauth_client-BSanvSql.js";
|
|
4
|
+
import { c as E_INVALID_TOKEN, n as E_INSUFFICIENT_SCOPE, o as E_INVALID_REQUEST } from "./oauth_error-C7UhDb2q.js";
|
|
5
|
+
import { t as OAuthAccessToken } from "./oauth_access_token-Cz_5gNBx.js";
|
|
6
|
+
import { t as TokenService } from "./token_service-DwnfAR9F.js";
|
|
7
|
+
import { t as IdTokenService } from "./id_token_service-CpTzOUDe.js";
|
|
8
|
+
//#region src/controllers/userinfo_controller.ts
|
|
9
|
+
/**
|
|
10
|
+
* OpenID Connect UserInfo endpoint (OIDC Core §5.3).
|
|
11
|
+
*
|
|
12
|
+
* Returns claims about the authenticated user based on the
|
|
13
|
+
* access token's scopes. Supports both GET and POST.
|
|
14
|
+
*
|
|
15
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
|
16
|
+
*/
|
|
17
|
+
var UserinfoController = class {
|
|
18
|
+
async handle(ctx) {
|
|
19
|
+
const manager = await ctx.containerResolver.make(SesameManager);
|
|
20
|
+
const authHeader = ctx.request.header("authorization");
|
|
21
|
+
const rawToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : ctx.request.body().access_token;
|
|
22
|
+
if (!rawToken) throw new E_INVALID_REQUEST("Missing Bearer token");
|
|
23
|
+
const hashed = new TokenService(manager).hashToken(rawToken);
|
|
24
|
+
const token = await OAuthAccessToken.query().where("tokenHash", hashed).first();
|
|
25
|
+
if (!token) throw new E_INVALID_TOKEN("Invalid access token");
|
|
26
|
+
if (token.revokedAt) throw new E_INVALID_TOKEN("Access token has been revoked");
|
|
27
|
+
if (token.expiresAt.toJSDate() < /* @__PURE__ */ new Date()) throw new E_INVALID_TOKEN("Access token has expired");
|
|
28
|
+
if (!token.scopes.includes("openid")) throw new E_INSUFFICIENT_SCOPE(["openid"], "Token does not have openid scope");
|
|
29
|
+
const user = await manager.findUserById(token.userId);
|
|
30
|
+
if (!user) throw new E_INVALID_TOKEN("Invalid access token");
|
|
31
|
+
const userClaims = await IdTokenService.resolveUserClaims(user, token.scopes);
|
|
32
|
+
ctx.response.header("Content-Type", "application/json");
|
|
33
|
+
return {
|
|
34
|
+
sub: token.userId,
|
|
35
|
+
...userClaims
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { UserinfoController as default };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@julr/sesame",
|
|
3
3
|
"description": "OAuth 2.1 + OIDC server for AdonisJS",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.0.0"
|
|
7
7
|
},
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"main": "build/index.js",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./build/index.js",
|
|
18
|
+
"./exceptions": "./build/src/oauth_error.js",
|
|
18
19
|
"./types": "./build/src/types.js",
|
|
19
20
|
"./sesame_provider": "./build/providers/sesame_provider.js",
|
|
20
21
|
"./guard": "./build/src/guard/main.js",
|
|
@@ -25,21 +26,15 @@
|
|
|
25
26
|
"./commands": "./build/commands/main.js"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"lint": "oxlint .",
|
|
31
|
-
"format": "oxfmt --write .",
|
|
29
|
+
"check": "vp check",
|
|
30
|
+
"format": "vp fmt --write .",
|
|
32
31
|
"quick:test": "node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts",
|
|
33
|
-
"pretest": "npm run lint",
|
|
34
32
|
"test": "c8 npm run quick:test",
|
|
35
|
-
"
|
|
36
|
-
"index:commands": "adonis-kit index build/commands",
|
|
37
|
-
"compile": "tsdown && tsc --emitDeclarationOnly --declaration",
|
|
38
|
-
"postcompile": "npm run copy:templates && npm run index:commands",
|
|
39
|
-
"build": "npm run compile",
|
|
33
|
+
"build": "vp pack && tsc --emitDeclarationOnly --declaration && adonis-kit index build/commands",
|
|
40
34
|
"release": "release-it",
|
|
41
35
|
"version": "npm run build",
|
|
42
|
-
"prepublishOnly": "npm run build"
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"prepare": "vp config"
|
|
43
38
|
},
|
|
44
39
|
"repository": {
|
|
45
40
|
"type": "git",
|
|
@@ -49,7 +44,17 @@
|
|
|
49
44
|
"url": "https://github.com/Julien-R44/sesame/issues"
|
|
50
45
|
},
|
|
51
46
|
"homepage": "https://github.com/Julien-R44/sesame#readme",
|
|
52
|
-
"keywords": [
|
|
47
|
+
"keywords": [
|
|
48
|
+
"adonisjs",
|
|
49
|
+
"adonis",
|
|
50
|
+
"oauth",
|
|
51
|
+
"oauth2.1",
|
|
52
|
+
"oidc",
|
|
53
|
+
"openid-connect",
|
|
54
|
+
"authorization-server",
|
|
55
|
+
"mcp",
|
|
56
|
+
"pkce"
|
|
57
|
+
],
|
|
53
58
|
"author": "Julien Ripouteau",
|
|
54
59
|
"license": "MIT",
|
|
55
60
|
"devDependencies": {
|
|
@@ -57,8 +62,8 @@
|
|
|
57
62
|
"@adonisjs/auth": "^10.0.0",
|
|
58
63
|
"@adonisjs/core": "^7.0.0",
|
|
59
64
|
"@adonisjs/lucid": "^22.0.0",
|
|
60
|
-
"@adonisjs/prettier-config": "^1.4.5",
|
|
61
65
|
"@adonisjs/tsconfig": "^2.0.0",
|
|
66
|
+
"@japa/api-client": "^3.2.1",
|
|
62
67
|
"@japa/assert": "^4.2.0",
|
|
63
68
|
"@japa/expect-type": "^2.0.4",
|
|
64
69
|
"@japa/runner": "^5.3.0",
|
|
@@ -69,50 +74,29 @@
|
|
|
69
74
|
"@vinejs/vine": "^4.3.0",
|
|
70
75
|
"better-sqlite3": "^12.6.2",
|
|
71
76
|
"c8": "^11.0.0",
|
|
72
|
-
"copyfiles": "^2.4.1",
|
|
73
77
|
"cross-env": "^10.1.0",
|
|
74
|
-
"oxfmt": "^0.36.0",
|
|
75
|
-
"oxlint": "^1.51.0",
|
|
76
78
|
"release-it": "^19.2.4",
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
+
"typescript": "^5.9.3",
|
|
80
|
+
"vite-plus": "catalog:"
|
|
79
81
|
},
|
|
80
82
|
"peerDependencies": {
|
|
81
83
|
"@adonisjs/assembler": "^8.0.0",
|
|
82
84
|
"@adonisjs/auth": "^10.0.0",
|
|
83
85
|
"@adonisjs/core": "^7.0.0",
|
|
84
|
-
"@adonisjs/lucid": "^21.0.0",
|
|
86
|
+
"@adonisjs/lucid": "^21.0.0 || ^22.0.0",
|
|
85
87
|
"@vinejs/vine": "^4.0.0"
|
|
86
88
|
},
|
|
87
89
|
"peerDependenciesMeta": {
|
|
88
90
|
"@adonisjs/assembler": {
|
|
89
91
|
"optional": true
|
|
92
|
+
},
|
|
93
|
+
"@adonisjs/auth": {
|
|
94
|
+
"optional": true
|
|
90
95
|
}
|
|
91
96
|
},
|
|
92
97
|
"publishConfig": {
|
|
93
98
|
"access": "public"
|
|
94
99
|
},
|
|
95
|
-
"tsdown": {
|
|
96
|
-
"entry": [
|
|
97
|
-
"./index.ts",
|
|
98
|
-
"./configure.ts",
|
|
99
|
-
"./providers/sesame_provider.ts",
|
|
100
|
-
"./services/main.ts",
|
|
101
|
-
"./src/guard/main.ts",
|
|
102
|
-
"./commands/sesame_purge.ts",
|
|
103
|
-
"./src/middleware/scope_middleware.ts",
|
|
104
|
-
"./src/middleware/any_scope_middleware.ts"
|
|
105
|
-
],
|
|
106
|
-
"outDir": "./build",
|
|
107
|
-
"clean": true,
|
|
108
|
-
"format": "esm",
|
|
109
|
-
"minify": "dce-only",
|
|
110
|
-
"fixedExtension": false,
|
|
111
|
-
"dts": false,
|
|
112
|
-
"treeshake": false,
|
|
113
|
-
"sourcemaps": false,
|
|
114
|
-
"target": "esnext"
|
|
115
|
-
},
|
|
116
100
|
"release-it": {
|
|
117
101
|
"git": {
|
|
118
102
|
"requireCleanWorkingDir": true,
|
|
@@ -147,6 +131,7 @@
|
|
|
147
131
|
]
|
|
148
132
|
},
|
|
149
133
|
"dependencies": {
|
|
134
|
+
"jose": "^6.2.1",
|
|
150
135
|
"luxon": "^3.7.2"
|
|
151
136
|
}
|
|
152
137
|
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DwDZy5Vy.js";
|
|
2
|
-
import "./decorate-BKZEjPRg.js";
|
|
3
|
-
import "./oauth_access_token-bsoM5KeU.js";
|
|
4
|
-
import { d as E_UNSUPPORTED_RESPONSE_TYPE, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
|
|
5
|
-
import { t as OAuthClient } from "./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 string from "@adonisjs/core/helpers/string";
|
|
10
|
-
import vine from "@vinejs/vine";
|
|
11
|
-
var AuthorizeController = class AuthorizeController {
|
|
12
|
-
static validator = vine.create({
|
|
13
|
-
client_id: vine.string(),
|
|
14
|
-
response_type: vine.string(),
|
|
15
|
-
redirect_uri: vine.string(),
|
|
16
|
-
scope: vine.string().optional(),
|
|
17
|
-
state: vine.string().optional(),
|
|
18
|
-
code_challenge: vine.string().optional(),
|
|
19
|
-
code_challenge_method: vine.string().optional()
|
|
20
|
-
});
|
|
21
|
-
#redirectWithError(ctx, manager, redirectUri, error, description, state) {
|
|
22
|
-
const url = new URL(redirectUri);
|
|
23
|
-
url.searchParams.set("error", error);
|
|
24
|
-
url.searchParams.set("error_description", description);
|
|
25
|
-
if (state) url.searchParams.set("state", state);
|
|
26
|
-
url.searchParams.set("iss", manager.config.issuer);
|
|
27
|
-
return ctx.response.redirect().toPath(url.toString());
|
|
28
|
-
}
|
|
29
|
-
async #buildAuthorizationRequestParams(manager, options) {
|
|
30
|
-
const tokenService = new TokenService(manager);
|
|
31
|
-
const rawRequestToken = tokenService.generateOpaqueToken();
|
|
32
|
-
const ttl = string.seconds.parse(manager.config.authorizationRequestTtl);
|
|
33
|
-
await OAuthPendingAuthorizationRequest.create({
|
|
34
|
-
id: crypto.randomUUID(),
|
|
35
|
-
token: tokenService.hashToken(rawRequestToken),
|
|
36
|
-
userId: options.userId,
|
|
37
|
-
clientId: options.clientId,
|
|
38
|
-
redirectUri: options.redirectUri,
|
|
39
|
-
scopes: options.scopes,
|
|
40
|
-
state: options.state ?? null,
|
|
41
|
-
codeChallenge: options.codeChallenge ?? null,
|
|
42
|
-
codeChallengeMethod: options.codeChallengeMethod ?? null,
|
|
43
|
-
expiresAt: DateTime.now().plus({ seconds: ttl })
|
|
44
|
-
});
|
|
45
|
-
const params = new URLSearchParams();
|
|
46
|
-
params.set("auth_token", rawRequestToken);
|
|
47
|
-
return params;
|
|
48
|
-
}
|
|
49
|
-
#copyAuthorizeDisplayParams(params, query) {
|
|
50
|
-
for (const [key, value] of Object.entries(query)) {
|
|
51
|
-
if (key === "auth_token") continue;
|
|
52
|
-
if (value != null) params.set(key, String(value));
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
#resolvePageUrl(page, ctx, params) {
|
|
56
|
-
if (typeof page === "function") return page(ctx, params);
|
|
57
|
-
return `${page}?${params.toString()}`;
|
|
58
|
-
}
|
|
59
|
-
async #issueAuthorizationCode(ctx, manager, options) {
|
|
60
|
-
const tokenService = new TokenService(manager);
|
|
61
|
-
const raw = tokenService.generateOpaqueToken();
|
|
62
|
-
const hashed = tokenService.hashToken(raw);
|
|
63
|
-
const ttl = string.seconds.parse(manager.config.authorizationCodeTtl);
|
|
64
|
-
await OAuthAuthorizationCode.create({
|
|
65
|
-
id: crypto.randomUUID(),
|
|
66
|
-
code: hashed,
|
|
67
|
-
clientId: options.client.clientId,
|
|
68
|
-
userId: options.userId,
|
|
69
|
-
scopes: options.scopes,
|
|
70
|
-
redirectUri: options.redirectUri,
|
|
71
|
-
codeChallenge: options.codeChallenge ?? null,
|
|
72
|
-
codeChallengeMethod: options.codeChallengeMethod ?? null,
|
|
73
|
-
expiresAt: DateTime.now().plus({ seconds: ttl })
|
|
74
|
-
});
|
|
75
|
-
const url = new URL(options.redirectUri);
|
|
76
|
-
url.searchParams.set("code", raw);
|
|
77
|
-
if (options.state) url.searchParams.set("state", options.state);
|
|
78
|
-
url.searchParams.set("iss", manager.config.issuer);
|
|
79
|
-
return ctx.response.redirect().toPath(url.toString());
|
|
80
|
-
}
|
|
81
|
-
async handle(ctx) {
|
|
82
|
-
const manager = await ctx.containerResolver.make(SesameManager);
|
|
83
|
-
const clientService = new ClientService();
|
|
84
|
-
const [error, query] = await AuthorizeController.validator.tryValidate(ctx.request.qs());
|
|
85
|
-
if (error) throw new E_INVALID_REQUEST("Invalid authorization request parameters");
|
|
86
|
-
if (query.response_type !== "code") throw new E_UNSUPPORTED_RESPONSE_TYPE("Only \"code\" is supported");
|
|
87
|
-
const client = await OAuthClient.query().where("clientId", query.client_id).first();
|
|
88
|
-
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
89
|
-
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
90
|
-
if (!client.redirectUris.includes(query.redirect_uri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
|
|
91
|
-
if (!client.grantTypes.includes("authorization_code")) return this.#redirectWithError(ctx, manager, query.redirect_uri, "unauthorized_client", "Client is not allowed to use the authorization_code grant", query.state);
|
|
92
|
-
const requestedScopes = query.scope ? query.scope.split(" ") : manager.config.defaultScopes;
|
|
93
|
-
const invalidScopes = manager.validateScopes(requestedScopes);
|
|
94
|
-
if (invalidScopes.length > 0) return this.#redirectWithError(ctx, manager, query.redirect_uri, "invalid_scope", `Invalid scopes: ${invalidScopes.join(", ")}`, query.state);
|
|
95
|
-
try {
|
|
96
|
-
clientService.validateClientScopes(requestedScopes, client.scopes);
|
|
97
|
-
} catch (error) {
|
|
98
|
-
return this.#redirectWithError(ctx, manager, query.redirect_uri, "invalid_scope", error.message, query.state);
|
|
99
|
-
}
|
|
100
|
-
if (!query.code_challenge) return this.#redirectWithError(ctx, manager, query.redirect_uri, "invalid_request", "PKCE code_challenge is required", query.state);
|
|
101
|
-
if (query.code_challenge_method !== "S256") return this.#redirectWithError(ctx, manager, query.redirect_uri, "invalid_request", "Only S256 code_challenge_method is supported", query.state);
|
|
102
|
-
await ctx.auth.check();
|
|
103
|
-
const user = ctx.auth.user;
|
|
104
|
-
if (!user) {
|
|
105
|
-
const params = new URLSearchParams();
|
|
106
|
-
this.#copyAuthorizeDisplayParams(params, ctx.request.qs());
|
|
107
|
-
const loginPage = this.#resolvePageUrl(manager.config.loginPage, ctx, params);
|
|
108
|
-
return ctx.response.redirect().toPath(loginPage);
|
|
109
|
-
}
|
|
110
|
-
const existingConsent = await OAuthConsent.query().where("clientId", client.clientId).where("userId", String(user.id)).first();
|
|
111
|
-
if (existingConsent) {
|
|
112
|
-
const consentedSet = new Set(existingConsent.scopes);
|
|
113
|
-
if (requestedScopes.every((s) => consentedSet.has(s))) return this.#issueAuthorizationCode(ctx, manager, {
|
|
114
|
-
client,
|
|
115
|
-
userId: String(user.id),
|
|
116
|
-
scopes: requestedScopes,
|
|
117
|
-
redirectUri: query.redirect_uri,
|
|
118
|
-
codeChallenge: query.code_challenge,
|
|
119
|
-
codeChallengeMethod: query.code_challenge_method,
|
|
120
|
-
state: query.state
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
const params = await this.#buildAuthorizationRequestParams(manager, {
|
|
124
|
-
userId: String(user.id),
|
|
125
|
-
clientId: client.clientId,
|
|
126
|
-
redirectUri: query.redirect_uri,
|
|
127
|
-
scopes: requestedScopes,
|
|
128
|
-
state: query.state,
|
|
129
|
-
codeChallenge: query.code_challenge,
|
|
130
|
-
codeChallengeMethod: query.code_challenge_method
|
|
131
|
-
});
|
|
132
|
-
this.#copyAuthorizeDisplayParams(params, ctx.request.qs());
|
|
133
|
-
params.set("scope", requestedScopes.join(" "));
|
|
134
|
-
const consentPage = this.#resolvePageUrl(manager.config.consentPage, ctx, params);
|
|
135
|
-
return ctx.response.redirect().toPath(consentPage);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
export { AuthorizeController as default };
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { o as BUILTIN_SCOPES } from "./sesame_manager-DwDZy5Vy.js";
|
|
2
|
-
import { o as E_INVALID_REQUEST, r as E_INVALID_CLIENT, s as E_INVALID_SCOPE } from "./oauth_error-CnJ3L8tf.js";
|
|
3
|
-
import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
|
|
4
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
|
-
var ClientService = class {
|
|
6
|
-
parseBasicAuth(header) {
|
|
7
|
-
if (!header.startsWith("Basic ")) return null;
|
|
8
|
-
try {
|
|
9
|
-
const decoded = Buffer.from(header.slice(6), "base64").toString("utf-8");
|
|
10
|
-
const colonIndex = decoded.indexOf(":");
|
|
11
|
-
if (colonIndex === -1) return null;
|
|
12
|
-
return {
|
|
13
|
-
clientId: decodeURIComponent(decoded.slice(0, colonIndex)),
|
|
14
|
-
clientSecret: decodeURIComponent(decoded.slice(colonIndex + 1))
|
|
15
|
-
};
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
extractCredentials(options) {
|
|
21
|
-
const basic = options.authorizationHeader ? this.parseBasicAuth(options.authorizationHeader) : null;
|
|
22
|
-
if (basic && options.bodyClientId) throw new E_INVALID_REQUEST("Multiple client authentication methods are not allowed");
|
|
23
|
-
if (basic) return basic;
|
|
24
|
-
if (options.bodyClientId) return {
|
|
25
|
-
clientId: options.bodyClientId,
|
|
26
|
-
clientSecret: options.bodyClientSecret
|
|
27
|
-
};
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
async authenticateClient(options) {
|
|
31
|
-
const credentials = this.extractCredentials(options);
|
|
32
|
-
if (!credentials) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
33
|
-
const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
|
|
34
|
-
if (!client || client.isDisabled) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
35
|
-
if (!client.isPublic) {
|
|
36
|
-
if (!credentials.clientSecret || !this.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Client authentication failed");
|
|
37
|
-
}
|
|
38
|
-
return client;
|
|
39
|
-
}
|
|
40
|
-
validateClientScopes(requestedScopes, clientScopes) {
|
|
41
|
-
const nonBuiltinScopes = requestedScopes.filter((s) => !BUILTIN_SCOPES.has(s));
|
|
42
|
-
if (clientScopes.length === 0 && nonBuiltinScopes.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${nonBuiltinScopes.join(", ")}`);
|
|
43
|
-
const allowedSet = new Set(clientScopes);
|
|
44
|
-
const invalid = nonBuiltinScopes.filter((s) => !allowedSet.has(s));
|
|
45
|
-
if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not allowed: ${invalid.join(", ")}`);
|
|
46
|
-
}
|
|
47
|
-
hashSecret(secret) {
|
|
48
|
-
return createHash("sha256").update(secret).digest("base64url");
|
|
49
|
-
}
|
|
50
|
-
verifySecret(secret, storedHash) {
|
|
51
|
-
const hash = this.hashSecret(secret);
|
|
52
|
-
try {
|
|
53
|
-
return timingSafeEqual(Buffer.from(hash), Buffer.from(storedHash));
|
|
54
|
-
} catch {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
generateClientId() {
|
|
59
|
-
return randomBytes(16).toString("hex");
|
|
60
|
-
}
|
|
61
|
-
generateClientSecret() {
|
|
62
|
-
return randomBytes(32).toString("base64url");
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
export { ClientService as t };
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { i as OAuthAuthorizationCode, n as OAuthPendingAuthorizationRequest, r as OAuthConsent, t as SesameManager } from "./sesame_manager-DwDZy5Vy.js";
|
|
2
|
-
import "./decorate-BKZEjPRg.js";
|
|
3
|
-
import "./oauth_access_token-bsoM5KeU.js";
|
|
4
|
-
import { a as E_INVALID_GRANT, o as E_INVALID_REQUEST, r as E_INVALID_CLIENT } from "./oauth_error-CnJ3L8tf.js";
|
|
5
|
-
import { t as OAuthClient } from "./oauth_client-BIoY5jBR.js";
|
|
6
|
-
import { t as TokenService } from "./token_service-fhoA4slP.js";
|
|
7
|
-
import { DateTime } from "luxon";
|
|
8
|
-
import string from "@adonisjs/core/helpers/string";
|
|
9
|
-
import vine from "@vinejs/vine";
|
|
10
|
-
var ConsentController = class ConsentController {
|
|
11
|
-
static validator = vine.create({ auth_token: vine.string() });
|
|
12
|
-
async #consumePendingRequest(hashedToken, userId) {
|
|
13
|
-
const row = await OAuthPendingAuthorizationRequest.query().where("token", hashedToken).where("userId", userId).where("expiresAt", ">", DateTime.now().toSQL()).first();
|
|
14
|
-
if (!row) return null;
|
|
15
|
-
const deleted = await OAuthPendingAuthorizationRequest.query().where("id", row.id).delete();
|
|
16
|
-
if ((Array.isArray(deleted) ? Number(deleted[0]) : Number(deleted)) === 0) return null;
|
|
17
|
-
return row;
|
|
18
|
-
}
|
|
19
|
-
async #issueAuthorizationCode(ctx, manager, options) {
|
|
20
|
-
const tokenService = new TokenService(manager);
|
|
21
|
-
const raw = tokenService.generateOpaqueToken();
|
|
22
|
-
const hashed = tokenService.hashToken(raw);
|
|
23
|
-
const ttl = string.seconds.parse(manager.config.authorizationCodeTtl);
|
|
24
|
-
await OAuthAuthorizationCode.create({
|
|
25
|
-
id: crypto.randomUUID(),
|
|
26
|
-
code: hashed,
|
|
27
|
-
clientId: options.client.clientId,
|
|
28
|
-
userId: options.userId,
|
|
29
|
-
scopes: options.scopes,
|
|
30
|
-
redirectUri: options.redirectUri,
|
|
31
|
-
codeChallenge: options.codeChallenge ?? null,
|
|
32
|
-
codeChallengeMethod: options.codeChallengeMethod ?? null,
|
|
33
|
-
expiresAt: DateTime.now().plus({ seconds: ttl })
|
|
34
|
-
});
|
|
35
|
-
const url = new URL(options.redirectUri);
|
|
36
|
-
url.searchParams.set("code", raw);
|
|
37
|
-
if (options.state) url.searchParams.set("state", options.state);
|
|
38
|
-
url.searchParams.set("iss", manager.config.issuer);
|
|
39
|
-
return ctx.response.redirect().toPath(url.toString());
|
|
40
|
-
}
|
|
41
|
-
async handle(ctx) {
|
|
42
|
-
const manager = await ctx.containerResolver.make(SesameManager);
|
|
43
|
-
await ctx.auth.check();
|
|
44
|
-
const user = ctx.auth.user;
|
|
45
|
-
if (!user) throw new E_INVALID_REQUEST("User must be authenticated");
|
|
46
|
-
const [error, body] = await ConsentController.validator.tryValidate(ctx.request.body());
|
|
47
|
-
if (error) throw new E_INVALID_REQUEST("Missing required parameter: auth_token");
|
|
48
|
-
const accept = ctx.request.body().accept;
|
|
49
|
-
const hashedToken = new TokenService(manager).hashToken(body.auth_token);
|
|
50
|
-
const pendingRequest = await this.#consumePendingRequest(hashedToken, String(user.id));
|
|
51
|
-
if (!pendingRequest) throw new E_INVALID_GRANT("Authorization request not found or expired");
|
|
52
|
-
const client = await OAuthClient.query().where("clientId", pendingRequest.clientId).first();
|
|
53
|
-
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
54
|
-
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
55
|
-
if (!client.redirectUris.includes(pendingRequest.redirectUri)) throw new E_INVALID_REQUEST("Invalid redirect_uri");
|
|
56
|
-
if (!accept) {
|
|
57
|
-
const url = new URL(pendingRequest.redirectUri);
|
|
58
|
-
url.searchParams.set("error", "access_denied");
|
|
59
|
-
url.searchParams.set("error_description", "The user denied the authorization request");
|
|
60
|
-
if (pendingRequest.state) url.searchParams.set("state", pendingRequest.state);
|
|
61
|
-
url.searchParams.set("iss", manager.config.issuer);
|
|
62
|
-
return ctx.response.redirect().toPath(url.toString());
|
|
63
|
-
}
|
|
64
|
-
const existingConsent = await OAuthConsent.query().where("clientId", client.clientId).where("userId", String(user.id)).first();
|
|
65
|
-
if (existingConsent) {
|
|
66
|
-
existingConsent.scopes = [...new Set([...existingConsent.scopes, ...pendingRequest.scopes])];
|
|
67
|
-
await existingConsent.save();
|
|
68
|
-
} else await OAuthConsent.create({
|
|
69
|
-
id: crypto.randomUUID(),
|
|
70
|
-
clientId: client.clientId,
|
|
71
|
-
userId: String(user.id),
|
|
72
|
-
scopes: pendingRequest.scopes
|
|
73
|
-
});
|
|
74
|
-
return this.#issueAuthorizationCode(ctx, manager, {
|
|
75
|
-
client,
|
|
76
|
-
userId: String(user.id),
|
|
77
|
-
scopes: pendingRequest.scopes,
|
|
78
|
-
redirectUri: pendingRequest.redirectUri,
|
|
79
|
-
codeChallenge: pendingRequest.codeChallenge ?? void 0,
|
|
80
|
-
codeChallengeMethod: pendingRequest.codeChallengeMethod ?? void 0,
|
|
81
|
-
state: pendingRequest.state ?? void 0
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
export { ConsentController as default };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { column } from "@adonisjs/lucid/orm";
|
|
2
|
-
function json(options) {
|
|
3
|
-
return column({
|
|
4
|
-
...options,
|
|
5
|
-
prepare: (value) => value == null ? null : JSON.stringify(value),
|
|
6
|
-
consume: (value) => value == null ? null : typeof value === "string" ? JSON.parse(value) : value
|
|
7
|
-
});
|
|
8
|
-
}
|
|
9
|
-
function __decorate(decorators, target, key, desc) {
|
|
10
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
11
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
12
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
13
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
14
|
-
}
|
|
15
|
-
export { json as n, __decorate as t };
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { n as json, t as __decorate } from "./decorate-BKZEjPRg.js";
|
|
2
|
-
import { BaseModel, column } from "@adonisjs/lucid/orm";
|
|
3
|
-
var OAuthClient = class extends BaseModel {
|
|
4
|
-
static table = "oauth_clients";
|
|
5
|
-
};
|
|
6
|
-
__decorate([column({ isPrimary: true })], OAuthClient.prototype, "id", void 0);
|
|
7
|
-
__decorate([column()], OAuthClient.prototype, "clientId", void 0);
|
|
8
|
-
__decorate([column({ serializeAs: null })], OAuthClient.prototype, "clientSecret", void 0);
|
|
9
|
-
__decorate([column()], OAuthClient.prototype, "name", void 0);
|
|
10
|
-
__decorate([json()], OAuthClient.prototype, "redirectUris", void 0);
|
|
11
|
-
__decorate([json()], OAuthClient.prototype, "scopes", void 0);
|
|
12
|
-
__decorate([json()], OAuthClient.prototype, "grantTypes", void 0);
|
|
13
|
-
__decorate([column()], OAuthClient.prototype, "isPublic", void 0);
|
|
14
|
-
__decorate([column()], OAuthClient.prototype, "isDisabled", void 0);
|
|
15
|
-
__decorate([column()], OAuthClient.prototype, "requirePkce", void 0);
|
|
16
|
-
__decorate([column()], OAuthClient.prototype, "type", void 0);
|
|
17
|
-
__decorate([json()], OAuthClient.prototype, "metadata", void 0);
|
|
18
|
-
__decorate([column()], OAuthClient.prototype, "userId", void 0);
|
|
19
|
-
__decorate([column.dateTime({ autoCreate: true })], OAuthClient.prototype, "createdAt", void 0);
|
|
20
|
-
__decorate([column.dateTime({
|
|
21
|
-
autoCreate: true,
|
|
22
|
-
autoUpdate: true
|
|
23
|
-
})], OAuthClient.prototype, "updatedAt", void 0);
|
|
24
|
-
export { OAuthClient as t };
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Exception } from "@adonisjs/core/exceptions";
|
|
2
|
-
var OAuthError = class extends Exception {
|
|
3
|
-
static oauthCode;
|
|
4
|
-
get oauthCode() {
|
|
5
|
-
return this.constructor.oauthCode;
|
|
6
|
-
}
|
|
7
|
-
handle(error, ctx) {
|
|
8
|
-
ctx.response.status(error.status).json({
|
|
9
|
-
error: error.oauthCode,
|
|
10
|
-
error_description: error.message
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
};
|
|
14
|
-
const E_INVALID_REQUEST = class extends OAuthError {
|
|
15
|
-
static status = 400;
|
|
16
|
-
static code = "E_INVALID_REQUEST";
|
|
17
|
-
static message = "Invalid request";
|
|
18
|
-
static oauthCode = "invalid_request";
|
|
19
|
-
};
|
|
20
|
-
const E_INVALID_CLIENT = class extends OAuthError {
|
|
21
|
-
static status = 401;
|
|
22
|
-
static code = "E_INVALID_CLIENT";
|
|
23
|
-
static message = "Invalid client";
|
|
24
|
-
static oauthCode = "invalid_client";
|
|
25
|
-
handle(error, ctx) {
|
|
26
|
-
if (ctx.request.header("authorization")?.startsWith("Basic ")) ctx.response.header("WWW-Authenticate", "Basic");
|
|
27
|
-
super.handle(error, ctx);
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
const E_INVALID_GRANT = class extends OAuthError {
|
|
31
|
-
static status = 400;
|
|
32
|
-
static code = "E_INVALID_GRANT";
|
|
33
|
-
static message = "Invalid grant";
|
|
34
|
-
static oauthCode = "invalid_grant";
|
|
35
|
-
};
|
|
36
|
-
const E_INVALID_SCOPE = class extends OAuthError {
|
|
37
|
-
static status = 400;
|
|
38
|
-
static code = "E_INVALID_SCOPE";
|
|
39
|
-
static message = "Invalid scope";
|
|
40
|
-
static oauthCode = "invalid_scope";
|
|
41
|
-
};
|
|
42
|
-
const E_INVALID_TOKEN = class extends OAuthError {
|
|
43
|
-
static status = 401;
|
|
44
|
-
static code = "E_INVALID_TOKEN";
|
|
45
|
-
static message = "Invalid token";
|
|
46
|
-
static oauthCode = "invalid_token";
|
|
47
|
-
};
|
|
48
|
-
const E_UNSUPPORTED_GRANT_TYPE = class extends OAuthError {
|
|
49
|
-
static status = 400;
|
|
50
|
-
static code = "E_UNSUPPORTED_GRANT_TYPE";
|
|
51
|
-
static message = "Unsupported grant type";
|
|
52
|
-
static oauthCode = "unsupported_grant_type";
|
|
53
|
-
};
|
|
54
|
-
const E_UNSUPPORTED_RESPONSE_TYPE = class extends OAuthError {
|
|
55
|
-
static status = 400;
|
|
56
|
-
static code = "E_UNSUPPORTED_RESPONSE_TYPE";
|
|
57
|
-
static message = "Unsupported response type";
|
|
58
|
-
static oauthCode = "unsupported_response_type";
|
|
59
|
-
};
|
|
60
|
-
const E_ACCESS_DENIED = class extends OAuthError {
|
|
61
|
-
static status = 403;
|
|
62
|
-
static code = "E_ACCESS_DENIED";
|
|
63
|
-
static message = "Access denied";
|
|
64
|
-
static oauthCode = "access_denied";
|
|
65
|
-
};
|
|
66
|
-
const E_INVALID_CLIENT_METADATA = class extends OAuthError {
|
|
67
|
-
static status = 400;
|
|
68
|
-
static code = "E_INVALID_CLIENT_METADATA";
|
|
69
|
-
static message = "Invalid client metadata";
|
|
70
|
-
static oauthCode = "invalid_client_metadata";
|
|
71
|
-
};
|
|
72
|
-
const E_SERVER_ERROR = class extends OAuthError {
|
|
73
|
-
static status = 500;
|
|
74
|
-
static code = "E_SERVER_ERROR";
|
|
75
|
-
static message = "Server error";
|
|
76
|
-
static oauthCode = "server_error";
|
|
77
|
-
};
|
|
78
|
-
const E_INSUFFICIENT_SCOPE = class extends OAuthError {
|
|
79
|
-
static status = 403;
|
|
80
|
-
static code = "E_INSUFFICIENT_SCOPE";
|
|
81
|
-
static message = "Insufficient scope";
|
|
82
|
-
static oauthCode = "insufficient_scope";
|
|
83
|
-
missingScopes;
|
|
84
|
-
constructor(missingScopes, message) {
|
|
85
|
-
super(message ?? "The token does not have the required scope(s)");
|
|
86
|
-
this.missingScopes = missingScopes;
|
|
87
|
-
}
|
|
88
|
-
handle(error, ctx) {
|
|
89
|
-
const scope = error.missingScopes.join(" ");
|
|
90
|
-
ctx.response.header("WWW-Authenticate", `Bearer error="insufficient_scope", error_description="${error.message}", scope="${scope}"`);
|
|
91
|
-
super.handle(error, ctx);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
export { E_INVALID_GRANT as a, E_INVALID_TOKEN as c, E_UNSUPPORTED_RESPONSE_TYPE as d, OAuthError as f, E_INVALID_CLIENT_METADATA as i, E_SERVER_ERROR as l, E_INSUFFICIENT_SCOPE as n, E_INVALID_REQUEST as o, E_INVALID_CLIENT as r, E_INVALID_SCOPE as s, E_ACCESS_DENIED as t, E_UNSUPPORTED_GRANT_TYPE as u };
|