@julr/sesame 0.0.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 +9 -0
- package/README.md +130 -0
- package/build/authorize_controller-CUdEDNEi.js +136 -0
- package/build/client_info_controller-DeIVcW8B.js +18 -0
- package/build/client_service-B9fD3ZGe.js +53 -0
- package/build/commands/commands.json +1 -0
- package/build/commands/main.d.ts +4 -0
- package/build/commands/main.js +36 -0
- package/build/commands/sesame_purge.d.ts +21 -0
- package/build/commands/sesame_purge.js +28 -0
- package/build/configure.d.ts +2 -0
- package/build/configure.js +16 -0
- package/build/consent_controller-DFfx7qVs.js +87 -0
- package/build/decorate-2_8Ex77k.js +15 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +27 -0
- package/build/introspect_controller-BzwfaUUE.js +63 -0
- package/build/main-kn40V-hF.js +2 -0
- package/build/metadata_controller-BSRRElQX.js +51 -0
- package/build/oauth_access_token-BpG8sq-c.js +18 -0
- package/build/oauth_client-eh0e5ql-.js +24 -0
- package/build/oauth_error-BQPqV-MV.js +78 -0
- package/build/providers/sesame_provider.d.ts +21 -0
- package/build/providers/sesame_provider.js +19 -0
- package/build/register_controller-BA7uQAgt.js +139 -0
- package/build/revoke_controller-CNIgNKH3.js +50 -0
- package/build/rolldown-runtime-BASaM9lw.js +12 -0
- package/build/routes-D6QCu0Pz.js +43 -0
- package/build/sesame_manager-B4tO2PLO.js +116 -0
- package/build/src/controllers/authorize_controller.d.ts +53 -0
- package/build/src/controllers/client_info_controller.d.ts +22 -0
- package/build/src/controllers/consent_controller.d.ts +27 -0
- package/build/src/controllers/introspect_controller.d.ts +28 -0
- package/build/src/controllers/metadata_controller.d.ts +64 -0
- package/build/src/controllers/register_controller.d.ts +91 -0
- package/build/src/controllers/revoke_controller.d.ts +16 -0
- package/build/src/controllers/token_controller.d.ts +24 -0
- package/build/src/decorators.d.ts +10 -0
- package/build/src/define_config.d.ts +16 -0
- package/build/src/grants/authorization_code_grant.d.ts +27 -0
- package/build/src/grants/refresh_token_grant.d.ts +27 -0
- package/build/src/guard/guard.d.ts +30 -0
- package/build/src/guard/main.d.ts +20 -0
- package/build/src/guard/main.js +17 -0
- package/build/src/guard/types.d.ts +46 -0
- package/build/src/guard/user_provider.d.ts +14 -0
- package/build/src/models/oauth_access_token.d.ts +23 -0
- package/build/src/models/oauth_authorization_code.d.ts +30 -0
- package/build/src/models/oauth_client.d.ts +33 -0
- package/build/src/models/oauth_consent.d.ts +22 -0
- package/build/src/models/oauth_refresh_token.d.ts +29 -0
- package/build/src/oauth_error.d.ts +359 -0
- package/build/src/routes.d.ts +28 -0
- package/build/src/rules.d.ts +12 -0
- package/build/src/services/client_service.d.ts +67 -0
- package/build/src/services/token_service.d.ts +42 -0
- package/build/src/sesame_manager.d.ts +66 -0
- package/build/src/types.d.ts +141 -0
- package/build/stubs/config/sesame.stub +30 -0
- package/build/stubs/main.d.ts +5 -0
- package/build/stubs/migrations/create_oauth_access_tokens_table.stub +25 -0
- package/build/stubs/migrations/create_oauth_authorization_codes_table.stub +27 -0
- package/build/stubs/migrations/create_oauth_clients_table.stub +31 -0
- package/build/stubs/migrations/create_oauth_consents_table.stub +24 -0
- package/build/stubs/migrations/create_oauth_refresh_tokens_table.stub +26 -0
- package/build/token_controller-C9wh813f.js +172 -0
- package/build/token_service-Czz9v5GI.js +30 -0
- package/build/user_provider-B3rXEUT3.js +150 -0
- package/package.json +144 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { HttpContext } from '@adonisjs/core/http';
|
|
2
|
+
/**
|
|
3
|
+
* Supported grant types for v1 (MCP-focused).
|
|
4
|
+
*
|
|
5
|
+
* - `authorization_code`: RFC 6749 §4.1 — Authorization Code Grant
|
|
6
|
+
* - `refresh_token`: RFC 6749 §6 — Refreshing an Access Token
|
|
7
|
+
*
|
|
8
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
|
|
9
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-6
|
|
10
|
+
*/
|
|
11
|
+
export type GrantType = 'authorization_code' | 'refresh_token';
|
|
12
|
+
/**
|
|
13
|
+
* User-facing configuration interface for Sésame.
|
|
14
|
+
*
|
|
15
|
+
* Provides all options needed to set up the OAuth 2.1 authorization
|
|
16
|
+
* server, including issuer identity, scopes, token lifetimes, and
|
|
17
|
+
* page redirects for the authorization flow.
|
|
18
|
+
*/
|
|
19
|
+
export interface SesameConfig {
|
|
20
|
+
/**
|
|
21
|
+
* The issuer URL (must be HTTPS in production).
|
|
22
|
+
* Used in JWT `iss` claim and discovery metadata.
|
|
23
|
+
*
|
|
24
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
|
25
|
+
*/
|
|
26
|
+
issuer: string;
|
|
27
|
+
/**
|
|
28
|
+
* Available scopes as a record of scope name to description.
|
|
29
|
+
*
|
|
30
|
+
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
|
|
31
|
+
*/
|
|
32
|
+
scopes?: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Default scopes assigned when none are requested
|
|
35
|
+
* in the authorization request.
|
|
36
|
+
*/
|
|
37
|
+
defaultScopes?: string[];
|
|
38
|
+
/**
|
|
39
|
+
* Enabled grant types.
|
|
40
|
+
* Defaults to `['authorization_code', 'refresh_token']`.
|
|
41
|
+
*/
|
|
42
|
+
grantTypes?: GrantType[];
|
|
43
|
+
/**
|
|
44
|
+
* Access token TTL as a string duration (e.g. '1h', '30m').
|
|
45
|
+
* Defaults to '1h'.
|
|
46
|
+
*/
|
|
47
|
+
accessTokenTtl?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Refresh token TTL as a string duration.
|
|
50
|
+
* Defaults to '30d'.
|
|
51
|
+
*/
|
|
52
|
+
refreshTokenTtl?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Authorization code TTL as a string duration.
|
|
55
|
+
* Defaults to '10m'.
|
|
56
|
+
*/
|
|
57
|
+
authorizationCodeTtl?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Route or URL where unauthenticated users are redirected
|
|
60
|
+
* to log in during the authorization flow. Can be a string
|
|
61
|
+
* path or a function receiving the HttpContext and authorize
|
|
62
|
+
* query parameters.
|
|
63
|
+
*/
|
|
64
|
+
loginPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
|
|
65
|
+
/**
|
|
66
|
+
* Route or URL where users are redirected to approve/deny
|
|
67
|
+
* client access during the authorization flow. Can be a string
|
|
68
|
+
* path or a function receiving the HttpContext and authorize
|
|
69
|
+
* query parameters.
|
|
70
|
+
*/
|
|
71
|
+
consentPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
|
|
72
|
+
/**
|
|
73
|
+
* Allow dynamic client registration.
|
|
74
|
+
* Defaults to `false`.
|
|
75
|
+
*
|
|
76
|
+
* @see https://datatracker.ietf.org/doc/html/rfc7591
|
|
77
|
+
*/
|
|
78
|
+
allowDynamicRegistration?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Allow unauthenticated client registration (needed for MCP).
|
|
81
|
+
* Defaults to `false`.
|
|
82
|
+
*/
|
|
83
|
+
allowPublicRegistration?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fully resolved configuration with defaults applied.
|
|
87
|
+
* Created by `defineConfig()` from user-supplied `SesameConfig`.
|
|
88
|
+
*/
|
|
89
|
+
export interface ResolvedSesameConfig {
|
|
90
|
+
issuer: string;
|
|
91
|
+
scopes: Record<string, string>;
|
|
92
|
+
defaultScopes: string[];
|
|
93
|
+
grantTypes: GrantType[];
|
|
94
|
+
accessTokenTtl: string;
|
|
95
|
+
refreshTokenTtl: string;
|
|
96
|
+
authorizationCodeTtl: string;
|
|
97
|
+
loginPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
|
|
98
|
+
consentPage: string | ((ctx: HttpContext, params: URLSearchParams) => string);
|
|
99
|
+
allowDynamicRegistration: boolean;
|
|
100
|
+
allowPublicRegistration: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* OAuth 2.0 Authorization Server Metadata as defined by RFC 8414.
|
|
104
|
+
*
|
|
105
|
+
* Returned by the `/.well-known/oauth-authorization-server` endpoint
|
|
106
|
+
* to allow clients to discover server capabilities.
|
|
107
|
+
*
|
|
108
|
+
* @see https://datatracker.ietf.org/doc/html/rfc8414#section-2
|
|
109
|
+
*/
|
|
110
|
+
export interface AuthServerMetadata {
|
|
111
|
+
issuer: string;
|
|
112
|
+
authorization_endpoint: string;
|
|
113
|
+
token_endpoint: string;
|
|
114
|
+
registration_endpoint?: string;
|
|
115
|
+
introspection_endpoint?: string;
|
|
116
|
+
revocation_endpoint?: string;
|
|
117
|
+
response_types_supported: string[];
|
|
118
|
+
response_modes_supported: string[];
|
|
119
|
+
grant_types_supported: string[];
|
|
120
|
+
token_endpoint_auth_methods_supported: string[];
|
|
121
|
+
introspection_endpoint_auth_methods_supported?: string[];
|
|
122
|
+
revocation_endpoint_auth_methods_supported?: string[];
|
|
123
|
+
code_challenge_methods_supported: string[];
|
|
124
|
+
authorization_response_iss_parameter_supported: boolean;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* OAuth 2.0 Protected Resource Metadata as defined by RFC 9728.
|
|
128
|
+
*
|
|
129
|
+
* Returned by the `/.well-known/oauth-protected-resource` endpoint.
|
|
130
|
+
* Used by MCP clients to discover which authorization servers
|
|
131
|
+
* protect a given resource.
|
|
132
|
+
*
|
|
133
|
+
* @see https://datatracker.ietf.org/doc/html/rfc9728
|
|
134
|
+
*/
|
|
135
|
+
export interface ResourceServerMetadata {
|
|
136
|
+
resource: string;
|
|
137
|
+
authorization_servers: string[];
|
|
138
|
+
scopes_supported?: string[];
|
|
139
|
+
bearer_methods_supported?: string[];
|
|
140
|
+
resource_documentation?: string;
|
|
141
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports: { defineConfig: 'import { defineConfig } from \'@adonisjs/sesame\'' }
|
|
3
|
+
}}}
|
|
4
|
+
import env from '#start/env'
|
|
5
|
+
|
|
6
|
+
const sesameConfig = defineConfig({
|
|
7
|
+
issuer: env.get('APP_URL'),
|
|
8
|
+
|
|
9
|
+
scopes: {
|
|
10
|
+
// Define your available scopes here
|
|
11
|
+
// 'read': 'Read access',
|
|
12
|
+
// 'write': 'Write access',
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
defaultScopes: [],
|
|
16
|
+
|
|
17
|
+
grantTypes: ['authorization_code', 'refresh_token'],
|
|
18
|
+
|
|
19
|
+
accessTokenTtl: '1h',
|
|
20
|
+
refreshTokenTtl: '30d',
|
|
21
|
+
authorizationCodeTtl: '10m',
|
|
22
|
+
|
|
23
|
+
loginPage: '/login',
|
|
24
|
+
consentPage: '/oauth/consent',
|
|
25
|
+
|
|
26
|
+
allowDynamicRegistration: false,
|
|
27
|
+
allowPublicRegistration: false,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default sesameConfig
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
|
|
3
|
+
}}}
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = 'oauth_access_tokens'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.uuid('id').primary()
|
|
11
|
+
table.string('token_hash').unique().notNullable()
|
|
12
|
+
table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
|
|
13
|
+
table.string('user_id').nullable()
|
|
14
|
+
table.json('scopes').notNullable()
|
|
15
|
+
table.timestamp('expires_at').notNullable()
|
|
16
|
+
table.timestamp('revoked_at').nullable()
|
|
17
|
+
table.timestamp('created_at').notNullable()
|
|
18
|
+
table.timestamp('updated_at').notNullable()
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async down() {
|
|
23
|
+
this.schema.dropTable(this.tableName)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
|
|
3
|
+
}}}
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = 'oauth_authorization_codes'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.uuid('id').primary()
|
|
11
|
+
table.string('code').notNullable().index()
|
|
12
|
+
table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
|
|
13
|
+
table.string('user_id').notNullable()
|
|
14
|
+
table.json('scopes').notNullable()
|
|
15
|
+
table.text('redirect_uri').notNullable()
|
|
16
|
+
table.string('code_challenge').nullable()
|
|
17
|
+
table.string('code_challenge_method').nullable()
|
|
18
|
+
table.timestamp('expires_at').notNullable()
|
|
19
|
+
table.timestamp('created_at').notNullable()
|
|
20
|
+
table.timestamp('updated_at').notNullable()
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async down() {
|
|
25
|
+
this.schema.dropTable(this.tableName)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
|
|
3
|
+
}}}
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = 'oauth_clients'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.uuid('id').primary()
|
|
11
|
+
table.string('client_id').unique().notNullable()
|
|
12
|
+
table.text('client_secret').nullable()
|
|
13
|
+
table.string('name').notNullable()
|
|
14
|
+
table.json('redirect_uris').notNullable()
|
|
15
|
+
table.json('scopes').notNullable().defaultTo('[]')
|
|
16
|
+
table.json('grant_types').notNullable().defaultTo('[]')
|
|
17
|
+
table.boolean('is_public').notNullable().defaultTo(false)
|
|
18
|
+
table.boolean('is_disabled').notNullable().defaultTo(false)
|
|
19
|
+
table.boolean('require_pkce').notNullable().defaultTo(true)
|
|
20
|
+
table.string('type').nullable()
|
|
21
|
+
table.json('metadata').nullable()
|
|
22
|
+
table.string('user_id').nullable()
|
|
23
|
+
table.timestamp('created_at').notNullable()
|
|
24
|
+
table.timestamp('updated_at').notNullable()
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async down() {
|
|
29
|
+
this.schema.dropTable(this.tableName)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
|
|
3
|
+
}}}
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = 'oauth_consents'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.uuid('id').primary()
|
|
11
|
+
table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
|
|
12
|
+
table.string('user_id').notNullable()
|
|
13
|
+
table.json('scopes').notNullable()
|
|
14
|
+
table.timestamp('created_at').notNullable()
|
|
15
|
+
table.timestamp('updated_at').notNullable()
|
|
16
|
+
|
|
17
|
+
table.unique(['client_id', 'user_id'])
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async down() {
|
|
22
|
+
this.schema.dropTable(this.tableName)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{{{
|
|
2
|
+
exports: { migration: "import { BaseSchema } from '@adonisjs/lucid/schema'" }
|
|
3
|
+
}}}
|
|
4
|
+
|
|
5
|
+
export default class extends BaseSchema {
|
|
6
|
+
protected tableName = 'oauth_refresh_tokens'
|
|
7
|
+
|
|
8
|
+
async up() {
|
|
9
|
+
this.schema.createTable(this.tableName, (table) => {
|
|
10
|
+
table.uuid('id').primary()
|
|
11
|
+
table.string('token').notNullable().index()
|
|
12
|
+
table.string('access_token_id').notNullable()
|
|
13
|
+
table.string('client_id').notNullable().references('client_id').inTable('oauth_clients').onDelete('CASCADE')
|
|
14
|
+
table.string('user_id').notNullable()
|
|
15
|
+
table.json('scopes').notNullable()
|
|
16
|
+
table.timestamp('expires_at').notNullable()
|
|
17
|
+
table.timestamp('revoked_at').nullable()
|
|
18
|
+
table.timestamp('created_at').notNullable()
|
|
19
|
+
table.timestamp('updated_at').notNullable()
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async down() {
|
|
24
|
+
this.schema.dropTable(this.tableName)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import "./decorate-2_8Ex77k.js";
|
|
2
|
+
import { t as OAuthAccessToken } from "./oauth_access_token-BpG8sq-c.js";
|
|
3
|
+
import { a as OAuthRefreshToken, i as OAuthAuthorizationCode, t as SesameManager } from "./sesame_manager-B4tO2PLO.js";
|
|
4
|
+
import { a as E_INVALID_REQUEST, i as E_INVALID_GRANT, l as E_UNSUPPORTED_GRANT_TYPE, n as E_INVALID_CLIENT, o as E_INVALID_SCOPE } from "./oauth_error-BQPqV-MV.js";
|
|
5
|
+
import { t as OAuthClient } from "./oauth_client-eh0e5ql-.js";
|
|
6
|
+
import { t as TokenService } from "./token_service-Czz9v5GI.js";
|
|
7
|
+
import { t as ClientService } from "./client_service-B9fD3ZGe.js";
|
|
8
|
+
import { DateTime } from "luxon";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import vine from "@vinejs/vine";
|
|
11
|
+
const codeVerifierValidator = vine.create({ code_verifier: vine.string().minLength(43).maxLength(128).regex(/^[A-Za-z0-9\-._~]+$/) });
|
|
12
|
+
async function handleAuthorizationCodeGrant(ctx, manager) {
|
|
13
|
+
const tokenService = new TokenService(manager);
|
|
14
|
+
const clientService = new ClientService();
|
|
15
|
+
const body = ctx.request.body();
|
|
16
|
+
const code = body.code;
|
|
17
|
+
const redirectUri = body.redirect_uri;
|
|
18
|
+
const codeVerifier = body.code_verifier;
|
|
19
|
+
if (!code) throw new E_INVALID_REQUEST("Missing required parameter: code");
|
|
20
|
+
if (!redirectUri) throw new E_INVALID_REQUEST("Missing required parameter: redirect_uri");
|
|
21
|
+
const credentials = clientService.extractCredentials({
|
|
22
|
+
authorizationHeader: ctx.request.header("authorization"),
|
|
23
|
+
bodyClientId: body.client_id,
|
|
24
|
+
bodyClientSecret: body.client_secret
|
|
25
|
+
});
|
|
26
|
+
if (!credentials) throw new E_INVALID_CLIENT("Missing client credentials");
|
|
27
|
+
const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
|
|
28
|
+
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
29
|
+
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
30
|
+
if (!client.isPublic) {
|
|
31
|
+
if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
|
|
32
|
+
if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
|
|
33
|
+
}
|
|
34
|
+
if (!client.grantTypes.includes("authorization_code")) throw new E_INVALID_CLIENT("Client is not allowed to use the authorization_code grant");
|
|
35
|
+
const hashedCode = tokenService.hashToken(code);
|
|
36
|
+
const authCode = await OAuthAuthorizationCode.query().where("code", hashedCode).where("clientId", client.clientId).first();
|
|
37
|
+
if (!authCode) throw new E_INVALID_GRANT("Authorization code not found");
|
|
38
|
+
if (authCode.expiresAt < DateTime.now()) {
|
|
39
|
+
await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
|
|
40
|
+
throw new E_INVALID_GRANT("Authorization code has expired");
|
|
41
|
+
}
|
|
42
|
+
if (authCode.redirectUri !== redirectUri) throw new E_INVALID_GRANT("Redirect URI mismatch");
|
|
43
|
+
const deleteResult = await OAuthAuthorizationCode.query().where("id", authCode.id).delete();
|
|
44
|
+
if ((Array.isArray(deleteResult) ? Number(deleteResult[0] ?? 0) : Number(deleteResult)) !== 1) throw new E_INVALID_GRANT("Authorization code has already been consumed");
|
|
45
|
+
const [verifierError] = await codeVerifierValidator.tryValidate({ code_verifier: codeVerifier });
|
|
46
|
+
if (verifierError) throw new E_INVALID_REQUEST("code_verifier must be 43-128 characters using only [A-Za-z0-9-._~] (RFC 7636 §4.1)");
|
|
47
|
+
if (!authCode.codeChallenge) throw new E_INVALID_GRANT("Authorization code is missing PKCE challenge");
|
|
48
|
+
if (createHash("sha256").update(codeVerifier).digest("base64url") !== authCode.codeChallenge) throw new E_INVALID_GRANT("PKCE verification failed");
|
|
49
|
+
clientService.validateClientScopes(authCode.scopes, client.scopes);
|
|
50
|
+
const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
|
|
51
|
+
await OAuthAccessToken.create({
|
|
52
|
+
id: crypto.randomUUID(),
|
|
53
|
+
tokenHash,
|
|
54
|
+
clientId: client.clientId,
|
|
55
|
+
userId: authCode.userId,
|
|
56
|
+
scopes: authCode.scopes,
|
|
57
|
+
expiresAt: DateTime.fromJSDate(expiresAt)
|
|
58
|
+
});
|
|
59
|
+
let refreshTokenRaw;
|
|
60
|
+
if (authCode.scopes.includes("offline_access")) {
|
|
61
|
+
const { raw, hash } = tokenService.createRefreshToken();
|
|
62
|
+
const refreshTtl = manager.parseTtl(manager.config.refreshTokenTtl);
|
|
63
|
+
await OAuthRefreshToken.create({
|
|
64
|
+
id: crypto.randomUUID(),
|
|
65
|
+
token: hash,
|
|
66
|
+
accessTokenId: tokenHash,
|
|
67
|
+
clientId: client.clientId,
|
|
68
|
+
userId: authCode.userId,
|
|
69
|
+
scopes: authCode.scopes,
|
|
70
|
+
expiresAt: DateTime.now().plus({ seconds: refreshTtl })
|
|
71
|
+
});
|
|
72
|
+
refreshTokenRaw = raw;
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
access_token: accessTokenRaw,
|
|
76
|
+
token_type: "Bearer",
|
|
77
|
+
expires_in: manager.parseTtl(manager.config.accessTokenTtl),
|
|
78
|
+
scope: authCode.scopes.join(" "),
|
|
79
|
+
...refreshTokenRaw ? { refresh_token: refreshTokenRaw } : {}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function handleRefreshTokenGrant(ctx, manager) {
|
|
83
|
+
const tokenService = new TokenService(manager);
|
|
84
|
+
const clientService = new ClientService();
|
|
85
|
+
const body = ctx.request.body();
|
|
86
|
+
const refreshTokenRaw = body.refresh_token;
|
|
87
|
+
if (!refreshTokenRaw) throw new E_INVALID_REQUEST("Missing required parameter: refresh_token");
|
|
88
|
+
const credentials = clientService.extractCredentials({
|
|
89
|
+
authorizationHeader: ctx.request.header("authorization"),
|
|
90
|
+
bodyClientId: body.client_id,
|
|
91
|
+
bodyClientSecret: body.client_secret
|
|
92
|
+
});
|
|
93
|
+
if (!credentials) throw new E_INVALID_CLIENT("Missing client credentials");
|
|
94
|
+
const client = await OAuthClient.query().where("clientId", credentials.clientId).first();
|
|
95
|
+
if (!client) throw new E_INVALID_CLIENT("Client not found");
|
|
96
|
+
if (client.isDisabled) throw new E_INVALID_CLIENT("Client is disabled");
|
|
97
|
+
if (!client.isPublic) {
|
|
98
|
+
if (!credentials.clientSecret) throw new E_INVALID_CLIENT("Missing client secret");
|
|
99
|
+
if (!clientService.verifySecret(credentials.clientSecret, client.clientSecret)) throw new E_INVALID_CLIENT("Invalid client secret");
|
|
100
|
+
}
|
|
101
|
+
if (!client.grantTypes.includes("refresh_token")) throw new E_INVALID_CLIENT("Client is not allowed to use the refresh_token grant");
|
|
102
|
+
const hashedToken = tokenService.hashToken(refreshTokenRaw);
|
|
103
|
+
const refreshToken = await OAuthRefreshToken.query().where("token", hashedToken).where("clientId", client.clientId).first();
|
|
104
|
+
if (!refreshToken) throw new E_INVALID_GRANT("Refresh token not found");
|
|
105
|
+
if (refreshToken.revokedAt) {
|
|
106
|
+
await OAuthRefreshToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).delete();
|
|
107
|
+
await OAuthAccessToken.query().where("clientId", client.clientId).where("userId", refreshToken.userId).whereNull("revokedAt").update({ revokedAt: DateTime.now().toSQL() });
|
|
108
|
+
throw new E_INVALID_GRANT("Refresh token has been revoked (possible replay attack)");
|
|
109
|
+
}
|
|
110
|
+
if (refreshToken.expiresAt < DateTime.now()) throw new E_INVALID_GRANT("Refresh token has expired");
|
|
111
|
+
const requestedScope = body.scope;
|
|
112
|
+
let scopes = refreshToken.scopes;
|
|
113
|
+
if (requestedScope) {
|
|
114
|
+
const requested = requestedScope.split(" ");
|
|
115
|
+
const originalSet = new Set(refreshToken.scopes);
|
|
116
|
+
const invalid = requested.filter((s) => !originalSet.has(s));
|
|
117
|
+
if (invalid.length > 0) throw new E_INVALID_SCOPE(`Scope not in original grant: ${invalid.join(", ")}`);
|
|
118
|
+
scopes = requested;
|
|
119
|
+
}
|
|
120
|
+
clientService.validateClientScopes(scopes, client.scopes);
|
|
121
|
+
const revokedAt = DateTime.now();
|
|
122
|
+
const updateResult = await OAuthRefreshToken.query().where("id", refreshToken.id).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
|
|
123
|
+
if ((Array.isArray(updateResult) ? Number(updateResult[0] ?? 0) : Number(updateResult)) !== 1) throw new E_INVALID_GRANT("Refresh token has already been consumed");
|
|
124
|
+
await OAuthAccessToken.query().where("tokenHash", refreshToken.accessTokenId).whereNull("revokedAt").update({ revokedAt: revokedAt.toSQL() });
|
|
125
|
+
const { raw: accessTokenRaw, hash: tokenHash, expiresAt } = tokenService.createAccessToken();
|
|
126
|
+
await OAuthAccessToken.create({
|
|
127
|
+
id: crypto.randomUUID(),
|
|
128
|
+
tokenHash,
|
|
129
|
+
clientId: client.clientId,
|
|
130
|
+
userId: refreshToken.userId,
|
|
131
|
+
scopes,
|
|
132
|
+
expiresAt: DateTime.fromJSDate(expiresAt)
|
|
133
|
+
});
|
|
134
|
+
const { raw: newRefreshTokenRaw, hash: newRefreshTokenHash } = tokenService.createRefreshToken();
|
|
135
|
+
const refreshTtl = manager.parseTtl(manager.config.refreshTokenTtl);
|
|
136
|
+
await OAuthRefreshToken.create({
|
|
137
|
+
id: crypto.randomUUID(),
|
|
138
|
+
token: newRefreshTokenHash,
|
|
139
|
+
accessTokenId: tokenHash,
|
|
140
|
+
clientId: client.clientId,
|
|
141
|
+
userId: refreshToken.userId,
|
|
142
|
+
scopes,
|
|
143
|
+
expiresAt: DateTime.now().plus({ seconds: refreshTtl })
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
access_token: accessTokenRaw,
|
|
147
|
+
token_type: "Bearer",
|
|
148
|
+
expires_in: manager.parseTtl(manager.config.accessTokenTtl),
|
|
149
|
+
scope: scopes.join(" "),
|
|
150
|
+
refresh_token: newRefreshTokenRaw
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const grantHandlers = {
|
|
154
|
+
authorization_code: handleAuthorizationCodeGrant,
|
|
155
|
+
refresh_token: handleRefreshTokenGrant
|
|
156
|
+
};
|
|
157
|
+
var TokenController = class TokenController {
|
|
158
|
+
static validator = vine.create({ grant_type: vine.string() });
|
|
159
|
+
async handle(ctx) {
|
|
160
|
+
const manager = await ctx.containerResolver.make(SesameManager);
|
|
161
|
+
const [error, body] = await TokenController.validator.tryValidate(ctx.request.body());
|
|
162
|
+
if (error) throw new E_UNSUPPORTED_GRANT_TYPE("Missing required parameter: grant_type");
|
|
163
|
+
if (!manager.isGrantTypeEnabled(body.grant_type)) throw new E_UNSUPPORTED_GRANT_TYPE(`Grant type "${body.grant_type}" is not enabled`);
|
|
164
|
+
const handler = grantHandlers[body.grant_type];
|
|
165
|
+
if (!handler) throw new E_UNSUPPORTED_GRANT_TYPE(`Unsupported grant type: ${body.grant_type}`);
|
|
166
|
+
const result = await handler(ctx, manager);
|
|
167
|
+
ctx.response.header("Cache-Control", "no-store");
|
|
168
|
+
ctx.response.header("Pragma", "no-cache");
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
export { TokenController as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
var TokenService = class {
|
|
3
|
+
#manager;
|
|
4
|
+
constructor(manager) {
|
|
5
|
+
this.#manager = manager;
|
|
6
|
+
}
|
|
7
|
+
createAccessToken() {
|
|
8
|
+
const raw = this.generateOpaqueToken();
|
|
9
|
+
const ttlSeconds = this.#manager.parseTtl(this.#manager.config.accessTokenTtl);
|
|
10
|
+
return {
|
|
11
|
+
raw,
|
|
12
|
+
hash: this.hashToken(raw),
|
|
13
|
+
expiresAt: new Date(Date.now() + ttlSeconds * 1e3)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
createRefreshToken() {
|
|
17
|
+
const raw = this.generateOpaqueToken();
|
|
18
|
+
return {
|
|
19
|
+
raw,
|
|
20
|
+
hash: this.hashToken(raw)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
generateOpaqueToken() {
|
|
24
|
+
return randomBytes(32).toString("base64url");
|
|
25
|
+
}
|
|
26
|
+
hashToken(token) {
|
|
27
|
+
return createHash("sha256").update(token).digest("base64url");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export { TokenService as t };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { t as OAuthAccessToken } from "./oauth_access_token-BpG8sq-c.js";
|
|
2
|
+
import { t as OAuthClient } from "./oauth_client-eh0e5ql-.js";
|
|
3
|
+
import { t as TokenService } from "./token_service-Czz9v5GI.js";
|
|
4
|
+
import { DateTime } from "luxon";
|
|
5
|
+
import { RuntimeException } from "@adonisjs/core/exceptions";
|
|
6
|
+
import { errors } from "@adonisjs/auth";
|
|
7
|
+
var OAuthGuard = class {
|
|
8
|
+
driverName = "oauth";
|
|
9
|
+
authenticationAttempted = false;
|
|
10
|
+
isAuthenticated = false;
|
|
11
|
+
user;
|
|
12
|
+
scopes = [];
|
|
13
|
+
clientId;
|
|
14
|
+
#name;
|
|
15
|
+
#ctx;
|
|
16
|
+
#emitter;
|
|
17
|
+
#userProvider;
|
|
18
|
+
#manager;
|
|
19
|
+
#resource;
|
|
20
|
+
constructor(name, ctx, emitter, userProvider, manager, resource) {
|
|
21
|
+
this.#name = name;
|
|
22
|
+
this.#ctx = ctx;
|
|
23
|
+
this.#emitter = emitter;
|
|
24
|
+
this.#userProvider = userProvider;
|
|
25
|
+
this.#manager = manager;
|
|
26
|
+
this.#resource = resource;
|
|
27
|
+
}
|
|
28
|
+
#extractBearerToken() {
|
|
29
|
+
const [type, token] = (this.#ctx.request.header("authorization") ?? "").split(" ");
|
|
30
|
+
if (!type || type.toLowerCase() !== "bearer" || !token) throw this.#authenticationFailed("Missing Bearer token");
|
|
31
|
+
return token;
|
|
32
|
+
}
|
|
33
|
+
#authenticationFailed(description, options) {
|
|
34
|
+
const suffix = this.#resource ?? "";
|
|
35
|
+
let header = `Bearer resource_metadata="${`${this.#manager.config.issuer}/.well-known/oauth-protected-resource${suffix}`}"`;
|
|
36
|
+
if (options?.includeError) header += `, error="invalid_token", error_description="${description}"`;
|
|
37
|
+
this.#ctx.response.header("WWW-Authenticate", header);
|
|
38
|
+
const error = new errors.E_UNAUTHORIZED_ACCESS(description, { guardDriverName: this.driverName });
|
|
39
|
+
this.#emitter.emit("oauth_auth:authentication_failed", {
|
|
40
|
+
ctx: this.#ctx,
|
|
41
|
+
guardName: this.#name,
|
|
42
|
+
error
|
|
43
|
+
});
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
getUserOrFail() {
|
|
47
|
+
if (!this.user) throw new errors.E_UNAUTHORIZED_ACCESS("Unauthorized access", { guardDriverName: this.driverName });
|
|
48
|
+
return this.user;
|
|
49
|
+
}
|
|
50
|
+
async authenticate() {
|
|
51
|
+
if (this.authenticationAttempted) return this.getUserOrFail();
|
|
52
|
+
this.authenticationAttempted = true;
|
|
53
|
+
this.#emitter.emit("oauth_auth:authentication_attempted", {
|
|
54
|
+
ctx: this.#ctx,
|
|
55
|
+
guardName: this.#name
|
|
56
|
+
});
|
|
57
|
+
const rawToken = this.#extractBearerToken();
|
|
58
|
+
const tokenService = new TokenService(this.#manager);
|
|
59
|
+
const includeError = { includeError: true };
|
|
60
|
+
const hashed = tokenService.hashToken(rawToken);
|
|
61
|
+
const record = await OAuthAccessToken.query().where("tokenHash", hashed).first();
|
|
62
|
+
if (!record) throw this.#authenticationFailed("Invalid or expired token", includeError);
|
|
63
|
+
if (record.revokedAt) throw this.#authenticationFailed("Token has been revoked", includeError);
|
|
64
|
+
if (record.expiresAt.toJSDate() < /* @__PURE__ */ new Date()) throw this.#authenticationFailed("Invalid or expired token", includeError);
|
|
65
|
+
if (!record.userId) throw this.#authenticationFailed("M2M tokens are not supported", includeError);
|
|
66
|
+
const providerUser = await this.#userProvider.findById(record.userId);
|
|
67
|
+
if (!providerUser) throw this.#authenticationFailed("User not found", includeError);
|
|
68
|
+
this.isAuthenticated = true;
|
|
69
|
+
this.user = providerUser.getOriginal();
|
|
70
|
+
this.scopes = record.scopes;
|
|
71
|
+
this.clientId = record.clientId;
|
|
72
|
+
this.#emitter.emit("oauth_auth:authentication_succeeded", {
|
|
73
|
+
ctx: this.#ctx,
|
|
74
|
+
guardName: this.#name,
|
|
75
|
+
user: this.user
|
|
76
|
+
});
|
|
77
|
+
return this.user;
|
|
78
|
+
}
|
|
79
|
+
async check() {
|
|
80
|
+
try {
|
|
81
|
+
await this.authenticate();
|
|
82
|
+
return true;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (error instanceof errors.E_UNAUTHORIZED_ACCESS) return false;
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
hasScope(...scopes) {
|
|
89
|
+
return scopes.every((s) => this.scopes.includes(s));
|
|
90
|
+
}
|
|
91
|
+
hasAnyScope(...scopes) {
|
|
92
|
+
return scopes.some((s) => this.scopes.includes(s));
|
|
93
|
+
}
|
|
94
|
+
async authenticateAsClient(user) {
|
|
95
|
+
const tokenService = new TokenService(this.#manager);
|
|
96
|
+
const defaultScopes = this.#manager.config.defaultScopes;
|
|
97
|
+
const testClient = await OAuthClient.firstOrCreate({ clientId: "__test_client__" }, {
|
|
98
|
+
id: crypto.randomUUID(),
|
|
99
|
+
clientId: "__test_client__",
|
|
100
|
+
name: "Test Client",
|
|
101
|
+
redirectUris: ["http://localhost/callback"],
|
|
102
|
+
grantTypes: ["authorization_code"],
|
|
103
|
+
scopes: defaultScopes,
|
|
104
|
+
isPublic: true,
|
|
105
|
+
requirePkce: false
|
|
106
|
+
});
|
|
107
|
+
const userId = String(user.id ?? user.getId?.() ?? "test-user");
|
|
108
|
+
const { raw, hash, expiresAt } = tokenService.createAccessToken();
|
|
109
|
+
await OAuthAccessToken.create({
|
|
110
|
+
id: crypto.randomUUID(),
|
|
111
|
+
tokenHash: hash,
|
|
112
|
+
clientId: testClient.clientId,
|
|
113
|
+
userId,
|
|
114
|
+
scopes: defaultScopes,
|
|
115
|
+
expiresAt: DateTime.fromJSDate(expiresAt)
|
|
116
|
+
});
|
|
117
|
+
return { headers: { authorization: `Bearer ${raw}` } };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
var OAuthLucidUserProvider = class {
|
|
121
|
+
#model;
|
|
122
|
+
#options;
|
|
123
|
+
constructor(options) {
|
|
124
|
+
this.#options = options;
|
|
125
|
+
}
|
|
126
|
+
async #getModel() {
|
|
127
|
+
if (this.#model && !("hot" in import.meta)) return this.#model;
|
|
128
|
+
this.#model = (await this.#options.model()).default;
|
|
129
|
+
return this.#model;
|
|
130
|
+
}
|
|
131
|
+
async createUserForGuard(user) {
|
|
132
|
+
const model = await this.#getModel();
|
|
133
|
+
if (user instanceof model === false) throw new RuntimeException(`Invalid user object. It must be an instance of the "${model.name}" model`);
|
|
134
|
+
return {
|
|
135
|
+
getId() {
|
|
136
|
+
if (!user.$primaryKeyValue) throw new RuntimeException(`Cannot use "${model.name}" model for authentication. The value of column "${model.primaryKey}" is undefined or null`);
|
|
137
|
+
return user.$primaryKeyValue;
|
|
138
|
+
},
|
|
139
|
+
getOriginal() {
|
|
140
|
+
return user;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async findById(identifier) {
|
|
145
|
+
const user = await (await this.#getModel()).find(identifier);
|
|
146
|
+
if (!user) return null;
|
|
147
|
+
return this.createUserForGuard(user);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
export { OAuthGuard as n, OAuthLucidUserProvider as t };
|