@mantiq/oauth 0.1.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/package.json +41 -0
- package/src/OAuthServer.ts +75 -0
- package/src/OAuthServiceProvider.ts +89 -0
- package/src/commands/OAuthClientCommand.ts +50 -0
- package/src/commands/OAuthInstallCommand.ts +67 -0
- package/src/commands/OAuthKeysCommand.ts +43 -0
- package/src/commands/OAuthPurgeCommand.ts +89 -0
- package/src/errors/OAuthError.ts +22 -0
- package/src/grants/AuthCodeGrant.ts +159 -0
- package/src/grants/ClientCredentialsGrant.ts +79 -0
- package/src/grants/GrantHandler.ts +14 -0
- package/src/grants/PersonalAccessGrant.ts +72 -0
- package/src/grants/RefreshTokenGrant.ts +129 -0
- package/src/guards/JwtGuard.ts +95 -0
- package/src/helpers/oauth.ts +15 -0
- package/src/index.ts +58 -0
- package/src/jwt/JwtEncoder.ts +51 -0
- package/src/jwt/JwtPayload.ts +9 -0
- package/src/jwt/JwtSigner.ts +175 -0
- package/src/middleware/CheckClientCredentials.ts +60 -0
- package/src/middleware/CheckForAnyScope.ts +52 -0
- package/src/middleware/CheckScopes.ts +53 -0
- package/src/migrations/CreateOAuthAccessTokensTable.ts +21 -0
- package/src/migrations/CreateOAuthAuthCodesTable.ts +22 -0
- package/src/migrations/CreateOAuthClientsTable.ts +24 -0
- package/src/migrations/CreateOAuthRefreshTokensTable.ts +18 -0
- package/src/models/AccessToken.ts +52 -0
- package/src/models/AuthCode.ts +20 -0
- package/src/models/Client.ts +36 -0
- package/src/models/RefreshToken.ts +23 -0
- package/src/routes/AuthorizationController.ts +132 -0
- package/src/routes/ClientController.ts +138 -0
- package/src/routes/ScopeController.ts +19 -0
- package/src/routes/TokenController.ts +38 -0
- package/src/routes/oauthRoutes.ts +55 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import type { GrantHandler, OAuthTokenResponse } from './GrantHandler.ts'
|
|
3
|
+
import type { JwtSigner } from '../jwt/JwtSigner.ts'
|
|
4
|
+
import type { OAuthServer } from '../OAuthServer.ts'
|
|
5
|
+
import { Client } from '../models/Client.ts'
|
|
6
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
7
|
+
import { OAuthError } from '../errors/OAuthError.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client Credentials grant — machine-to-machine authentication.
|
|
11
|
+
* No user involved, no refresh token issued.
|
|
12
|
+
*/
|
|
13
|
+
export class ClientCredentialsGrant implements GrantHandler {
|
|
14
|
+
readonly grantType = 'client_credentials'
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly signer: JwtSigner,
|
|
18
|
+
private readonly server: OAuthServer,
|
|
19
|
+
) {}
|
|
20
|
+
|
|
21
|
+
async handle(request: MantiqRequest): Promise<OAuthTokenResponse> {
|
|
22
|
+
const clientId = await request.input('client_id') as string | undefined
|
|
23
|
+
const clientSecret = await request.input('client_secret') as string | undefined
|
|
24
|
+
const scopeParam = await request.input('scope') as string | undefined
|
|
25
|
+
|
|
26
|
+
if (!clientId) throw new OAuthError('The client_id parameter is required.', 'invalid_request')
|
|
27
|
+
if (!clientSecret) throw new OAuthError('The client_secret parameter is required.', 'invalid_request')
|
|
28
|
+
|
|
29
|
+
// Resolve client
|
|
30
|
+
const client = await Client.find(clientId)
|
|
31
|
+
if (!client) throw new OAuthError('Client not found.', 'invalid_client', 401)
|
|
32
|
+
|
|
33
|
+
// Verify secret
|
|
34
|
+
if (clientSecret !== client.getAttribute('secret')) {
|
|
35
|
+
throw new OAuthError('Invalid client credentials.', 'invalid_client', 401)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse requested scopes
|
|
39
|
+
const scopes = scopeParam ? scopeParam.split(' ').filter(Boolean) : []
|
|
40
|
+
|
|
41
|
+
// Validate scopes
|
|
42
|
+
for (const scope of scopes) {
|
|
43
|
+
if (!this.server.hasScope(scope)) {
|
|
44
|
+
throw new OAuthError(`Invalid scope: ${scope}`, 'invalid_scope')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const tokenId = crypto.randomUUID()
|
|
49
|
+
const now = Math.floor(Date.now() / 1000)
|
|
50
|
+
|
|
51
|
+
// Create access token record (no user_id for client credentials)
|
|
52
|
+
await AccessToken.create({
|
|
53
|
+
id: tokenId,
|
|
54
|
+
user_id: null,
|
|
55
|
+
client_id: clientId,
|
|
56
|
+
name: null,
|
|
57
|
+
scopes: JSON.stringify(scopes),
|
|
58
|
+
revoked: false,
|
|
59
|
+
expires_at: new Date((now + this.server.tokenLifetime) * 1000).toISOString(),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Sign JWT (no sub — no user)
|
|
63
|
+
const jwt = await this.signer.sign({
|
|
64
|
+
iss: 'mantiq-oauth',
|
|
65
|
+
aud: clientId,
|
|
66
|
+
exp: now + this.server.tokenLifetime,
|
|
67
|
+
iat: now,
|
|
68
|
+
jti: tokenId,
|
|
69
|
+
scopes,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
token_type: 'Bearer',
|
|
74
|
+
expires_in: this.server.tokenLifetime,
|
|
75
|
+
access_token: jwt,
|
|
76
|
+
scope: scopes.join(' ') || undefined,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
|
|
3
|
+
export interface OAuthTokenResponse {
|
|
4
|
+
token_type: 'Bearer'
|
|
5
|
+
expires_in: number
|
|
6
|
+
access_token: string
|
|
7
|
+
refresh_token?: string
|
|
8
|
+
scope?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface GrantHandler {
|
|
12
|
+
readonly grantType: string
|
|
13
|
+
handle(request: MantiqRequest): Promise<OAuthTokenResponse>
|
|
14
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import type { GrantHandler, OAuthTokenResponse } from './GrantHandler.ts'
|
|
3
|
+
import type { JwtSigner } from '../jwt/JwtSigner.ts'
|
|
4
|
+
import type { OAuthServer } from '../OAuthServer.ts'
|
|
5
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
6
|
+
import { OAuthError } from '../errors/OAuthError.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Personal Access Token grant — for authenticated users creating
|
|
10
|
+
* their own API tokens with specific scopes.
|
|
11
|
+
*/
|
|
12
|
+
export class PersonalAccessGrant implements GrantHandler {
|
|
13
|
+
readonly grantType = 'personal_access'
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly signer: JwtSigner,
|
|
17
|
+
private readonly server: OAuthServer,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async handle(request: MantiqRequest): Promise<OAuthTokenResponse> {
|
|
21
|
+
const user = request.user<any>()
|
|
22
|
+
if (!user) throw new OAuthError('User must be authenticated.', 'invalid_request', 401)
|
|
23
|
+
|
|
24
|
+
const scopeParam = await request.input('scope') as string | undefined
|
|
25
|
+
const name = await request.input('name') as string | undefined
|
|
26
|
+
|
|
27
|
+
// Parse requested scopes
|
|
28
|
+
const scopes = scopeParam ? scopeParam.split(' ').filter(Boolean) : []
|
|
29
|
+
|
|
30
|
+
// Validate scopes
|
|
31
|
+
for (const scope of scopes) {
|
|
32
|
+
if (!this.server.hasScope(scope)) {
|
|
33
|
+
throw new OAuthError(`Invalid scope: ${scope}`, 'invalid_scope')
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userId = typeof user.getAuthIdentifier === 'function'
|
|
38
|
+
? user.getAuthIdentifier()
|
|
39
|
+
: user.id ?? user.getAttribute?.('id')
|
|
40
|
+
|
|
41
|
+
const tokenId = crypto.randomUUID()
|
|
42
|
+
const now = Math.floor(Date.now() / 1000)
|
|
43
|
+
|
|
44
|
+
// Create access token record
|
|
45
|
+
await AccessToken.create({
|
|
46
|
+
id: tokenId,
|
|
47
|
+
user_id: String(userId),
|
|
48
|
+
client_id: null,
|
|
49
|
+
name: name ?? 'Personal Access Token',
|
|
50
|
+
scopes: JSON.stringify(scopes),
|
|
51
|
+
revoked: false,
|
|
52
|
+
expires_at: new Date((now + this.server.tokenLifetime) * 1000).toISOString(),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Sign JWT
|
|
56
|
+
const jwt = await this.signer.sign({
|
|
57
|
+
iss: 'mantiq-oauth',
|
|
58
|
+
sub: String(userId),
|
|
59
|
+
exp: now + this.server.tokenLifetime,
|
|
60
|
+
iat: now,
|
|
61
|
+
jti: tokenId,
|
|
62
|
+
scopes,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
token_type: 'Bearer',
|
|
67
|
+
expires_in: this.server.tokenLifetime,
|
|
68
|
+
access_token: jwt,
|
|
69
|
+
scope: scopes.join(' ') || undefined,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import type { GrantHandler, OAuthTokenResponse } from './GrantHandler.ts'
|
|
3
|
+
import type { JwtSigner } from '../jwt/JwtSigner.ts'
|
|
4
|
+
import type { OAuthServer } from '../OAuthServer.ts'
|
|
5
|
+
import { Client } from '../models/Client.ts'
|
|
6
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
7
|
+
import { RefreshToken } from '../models/RefreshToken.ts'
|
|
8
|
+
import { OAuthError } from '../errors/OAuthError.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Refresh Token grant — exchange a refresh token for a new token pair.
|
|
12
|
+
* Revokes the old access + refresh tokens and issues new ones.
|
|
13
|
+
*/
|
|
14
|
+
export class RefreshTokenGrant implements GrantHandler {
|
|
15
|
+
readonly grantType = 'refresh_token'
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly signer: JwtSigner,
|
|
19
|
+
private readonly server: OAuthServer,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
async handle(request: MantiqRequest): Promise<OAuthTokenResponse> {
|
|
23
|
+
const refreshTokenId = await request.input('refresh_token') as string | undefined
|
|
24
|
+
const clientId = await request.input('client_id') as string | undefined
|
|
25
|
+
const clientSecret = await request.input('client_secret') as string | undefined
|
|
26
|
+
const scopeParam = await request.input('scope') as string | undefined
|
|
27
|
+
|
|
28
|
+
if (!refreshTokenId) throw new OAuthError('The refresh_token parameter is required.', 'invalid_request')
|
|
29
|
+
if (!clientId) throw new OAuthError('The client_id parameter is required.', 'invalid_request')
|
|
30
|
+
|
|
31
|
+
// Resolve client
|
|
32
|
+
const client = await Client.find(clientId)
|
|
33
|
+
if (!client) throw new OAuthError('Client not found.', 'invalid_client', 401)
|
|
34
|
+
|
|
35
|
+
// Verify secret for confidential clients
|
|
36
|
+
if (client.confidential()) {
|
|
37
|
+
if (!clientSecret || clientSecret !== client.getAttribute('secret')) {
|
|
38
|
+
throw new OAuthError('Invalid client credentials.', 'invalid_client', 401)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve refresh token
|
|
43
|
+
const refreshToken = await RefreshToken.find(refreshTokenId)
|
|
44
|
+
if (!refreshToken) throw new OAuthError('Invalid refresh token.', 'invalid_grant')
|
|
45
|
+
|
|
46
|
+
if (refreshToken.getAttribute('revoked')) {
|
|
47
|
+
throw new OAuthError('Refresh token has been revoked.', 'invalid_grant')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check expiration
|
|
51
|
+
const expiresAt = refreshToken.getAttribute('expires_at')
|
|
52
|
+
if (expiresAt && new Date(expiresAt) < new Date()) {
|
|
53
|
+
throw new OAuthError('Refresh token has expired.', 'invalid_grant')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Resolve the original access token
|
|
57
|
+
const oldAccessTokenId = refreshToken.getAttribute('access_token_id') as string
|
|
58
|
+
const oldAccessToken = await AccessToken.find(oldAccessTokenId)
|
|
59
|
+
if (!oldAccessToken) throw new OAuthError('Associated access token not found.', 'invalid_grant')
|
|
60
|
+
|
|
61
|
+
// Verify the token belongs to this client
|
|
62
|
+
if (oldAccessToken.getAttribute('client_id') !== clientId) {
|
|
63
|
+
throw new OAuthError('Refresh token does not belong to this client.', 'invalid_grant')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Revoke old tokens
|
|
67
|
+
await refreshToken.revoke()
|
|
68
|
+
if (!oldAccessToken.getAttribute('revoked')) {
|
|
69
|
+
await oldAccessToken.revoke()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Determine scopes (keep original scopes, or narrow if requested)
|
|
73
|
+
const originalScopes = (oldAccessToken.getAttribute('scopes') as string[]) || []
|
|
74
|
+
let scopes = originalScopes
|
|
75
|
+
|
|
76
|
+
if (scopeParam) {
|
|
77
|
+
const requestedScopes = scopeParam.split(' ').filter(Boolean)
|
|
78
|
+
// Can only request a subset of original scopes
|
|
79
|
+
for (const scope of requestedScopes) {
|
|
80
|
+
if (!originalScopes.includes('*') && !originalScopes.includes(scope)) {
|
|
81
|
+
throw new OAuthError(`Scope "${scope}" was not granted on the original token.`, 'invalid_scope')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
scopes = requestedScopes
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Issue new tokens
|
|
88
|
+
const userId = oldAccessToken.getAttribute('user_id') as string | null
|
|
89
|
+
const tokenId = crypto.randomUUID()
|
|
90
|
+
const now = Math.floor(Date.now() / 1000)
|
|
91
|
+
|
|
92
|
+
await AccessToken.create({
|
|
93
|
+
id: tokenId,
|
|
94
|
+
user_id: userId,
|
|
95
|
+
client_id: clientId,
|
|
96
|
+
name: null,
|
|
97
|
+
scopes: JSON.stringify(scopes),
|
|
98
|
+
revoked: false,
|
|
99
|
+
expires_at: new Date((now + this.server.tokenLifetime) * 1000).toISOString(),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const newRefreshTokenId = crypto.randomUUID()
|
|
103
|
+
await RefreshToken.create({
|
|
104
|
+
id: newRefreshTokenId,
|
|
105
|
+
access_token_id: tokenId,
|
|
106
|
+
revoked: false,
|
|
107
|
+
expires_at: new Date((now + this.server.refreshTokenLifetime) * 1000).toISOString(),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Sign JWT
|
|
111
|
+
const jwt = await this.signer.sign({
|
|
112
|
+
iss: 'mantiq-oauth',
|
|
113
|
+
sub: userId ?? undefined,
|
|
114
|
+
aud: clientId,
|
|
115
|
+
exp: now + this.server.tokenLifetime,
|
|
116
|
+
iat: now,
|
|
117
|
+
jti: tokenId,
|
|
118
|
+
scopes,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
token_type: 'Bearer',
|
|
123
|
+
expires_in: this.server.tokenLifetime,
|
|
124
|
+
access_token: jwt,
|
|
125
|
+
refresh_token: newRefreshTokenId,
|
|
126
|
+
scope: scopes.join(' '),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Guard } from '@mantiq/auth'
|
|
2
|
+
import type { Authenticatable, UserProvider } from '@mantiq/auth'
|
|
3
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
4
|
+
import type { JwtSigner } from '../jwt/JwtSigner.ts'
|
|
5
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JWT-based stateless guard for OAuth access tokens.
|
|
9
|
+
* Extracts the bearer token, verifies the JWT signature,
|
|
10
|
+
* checks the token record in the database, and resolves the user.
|
|
11
|
+
*/
|
|
12
|
+
export class JwtGuard implements Guard {
|
|
13
|
+
private _user: Authenticatable | null = null
|
|
14
|
+
private _request: MantiqRequest | null = null
|
|
15
|
+
private _resolved = false
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly signer: JwtSigner,
|
|
19
|
+
private readonly provider: UserProvider,
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
async check(): Promise<boolean> {
|
|
23
|
+
return (await this.user()) !== null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async guest(): Promise<boolean> {
|
|
27
|
+
return !(await this.check())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async user(): Promise<Authenticatable | null> {
|
|
31
|
+
if (this._resolved) return this._user
|
|
32
|
+
this._resolved = true
|
|
33
|
+
|
|
34
|
+
if (!this._request) return null
|
|
35
|
+
|
|
36
|
+
const bearerToken = this._request.bearerToken()
|
|
37
|
+
if (!bearerToken) return null
|
|
38
|
+
|
|
39
|
+
// Verify JWT signature and expiration
|
|
40
|
+
const payload = await this.signer.verify(bearerToken)
|
|
41
|
+
if (!payload || !payload.jti) return null
|
|
42
|
+
|
|
43
|
+
// Look up the access token in the database
|
|
44
|
+
const accessToken = await AccessToken.find(payload.jti)
|
|
45
|
+
if (!accessToken) return null
|
|
46
|
+
|
|
47
|
+
// Check if revoked
|
|
48
|
+
if (accessToken.getAttribute('revoked')) return null
|
|
49
|
+
|
|
50
|
+
// Check if expired (belt-and-suspenders — JWT exp is already checked)
|
|
51
|
+
if (accessToken.isExpired()) return null
|
|
52
|
+
|
|
53
|
+
// For client_credentials tokens with no user
|
|
54
|
+
const userId = payload.sub
|
|
55
|
+
if (!userId) return null
|
|
56
|
+
|
|
57
|
+
// Resolve user from provider
|
|
58
|
+
const user = await this.provider.retrieveById(userId)
|
|
59
|
+
if (!user) return null
|
|
60
|
+
|
|
61
|
+
// Attach token info to the user if it supports it
|
|
62
|
+
if (typeof (user as any).withAccessToken === 'function') {
|
|
63
|
+
(user as any).withAccessToken(accessToken)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._user = user
|
|
67
|
+
return user
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async id(): Promise<string | number | null> {
|
|
71
|
+
const user = await this.user()
|
|
72
|
+
return user?.getAuthIdentifier() ?? null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async validate(credentials: Record<string, any>): Promise<boolean> {
|
|
76
|
+
const user = await this.provider.retrieveByCredentials(credentials)
|
|
77
|
+
if (!user) return false
|
|
78
|
+
return this.provider.validateCredentials(user, credentials)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setUser(user: Authenticatable): void {
|
|
82
|
+
this._user = user
|
|
83
|
+
this._resolved = true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
hasUser(): boolean {
|
|
87
|
+
return this._user !== null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setRequest(request: MantiqRequest): void {
|
|
91
|
+
this._request = request
|
|
92
|
+
this._user = null
|
|
93
|
+
this._resolved = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Application } from '@mantiq/core'
|
|
2
|
+
import type { OAuthServer } from '../OAuthServer.ts'
|
|
3
|
+
|
|
4
|
+
export const OAUTH_SERVER = Symbol('OAuthServer')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Access the OAuthServer singleton.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const server = oauth()
|
|
11
|
+
* server.tokensCan({ 'read': 'Read data', 'write': 'Write data' })
|
|
12
|
+
*/
|
|
13
|
+
export function oauth(): OAuthServer {
|
|
14
|
+
return Application.getInstance().make<OAuthServer>(OAUTH_SERVER)
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ── JWT ──────────────────────────────────────────────────────────────────────
|
|
2
|
+
export type { JwtPayload } from './jwt/JwtPayload.ts'
|
|
3
|
+
export { base64UrlEncode, base64UrlDecode, base64UrlEncodeString, base64UrlDecodeString } from './jwt/JwtEncoder.ts'
|
|
4
|
+
export { JwtSigner } from './jwt/JwtSigner.ts'
|
|
5
|
+
|
|
6
|
+
// ── Models ───────────────────────────────────────────────────────────────────
|
|
7
|
+
export { Client } from './models/Client.ts'
|
|
8
|
+
export { AccessToken } from './models/AccessToken.ts'
|
|
9
|
+
export { AuthCode } from './models/AuthCode.ts'
|
|
10
|
+
export { RefreshToken } from './models/RefreshToken.ts'
|
|
11
|
+
|
|
12
|
+
// ── Server ───────────────────────────────────────────────────────────────────
|
|
13
|
+
export { OAuthServer } from './OAuthServer.ts'
|
|
14
|
+
export type { OAuthConfig } from './OAuthServer.ts'
|
|
15
|
+
|
|
16
|
+
// ── Grants ───────────────────────────────────────────────────────────────────
|
|
17
|
+
export type { GrantHandler, OAuthTokenResponse } from './grants/GrantHandler.ts'
|
|
18
|
+
export { AuthCodeGrant } from './grants/AuthCodeGrant.ts'
|
|
19
|
+
export { ClientCredentialsGrant } from './grants/ClientCredentialsGrant.ts'
|
|
20
|
+
export { RefreshTokenGrant } from './grants/RefreshTokenGrant.ts'
|
|
21
|
+
export { PersonalAccessGrant } from './grants/PersonalAccessGrant.ts'
|
|
22
|
+
|
|
23
|
+
// ── Guards ───────────────────────────────────────────────────────────────────
|
|
24
|
+
export { JwtGuard } from './guards/JwtGuard.ts'
|
|
25
|
+
|
|
26
|
+
// ── Middleware ────────────────────────────────────────────────────────────────
|
|
27
|
+
export { CheckScopes } from './middleware/CheckScopes.ts'
|
|
28
|
+
export { CheckForAnyScope } from './middleware/CheckForAnyScope.ts'
|
|
29
|
+
export { CheckClientCredentials } from './middleware/CheckClientCredentials.ts'
|
|
30
|
+
|
|
31
|
+
// ── Routes ───────────────────────────────────────────────────────────────────
|
|
32
|
+
export { oauthRoutes } from './routes/oauthRoutes.ts'
|
|
33
|
+
export type { OAuthRouteOptions } from './routes/oauthRoutes.ts'
|
|
34
|
+
export { TokenController } from './routes/TokenController.ts'
|
|
35
|
+
export { AuthorizationController } from './routes/AuthorizationController.ts'
|
|
36
|
+
export { ClientController } from './routes/ClientController.ts'
|
|
37
|
+
export { ScopeController } from './routes/ScopeController.ts'
|
|
38
|
+
|
|
39
|
+
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
40
|
+
export { OAuthInstallCommand } from './commands/OAuthInstallCommand.ts'
|
|
41
|
+
export { OAuthKeysCommand } from './commands/OAuthKeysCommand.ts'
|
|
42
|
+
export { OAuthClientCommand } from './commands/OAuthClientCommand.ts'
|
|
43
|
+
export { OAuthPurgeCommand } from './commands/OAuthPurgeCommand.ts'
|
|
44
|
+
|
|
45
|
+
// ── Migrations ───────────────────────────────────────────────────────────────
|
|
46
|
+
export { CreateOAuthClientsTable } from './migrations/CreateOAuthClientsTable.ts'
|
|
47
|
+
export { CreateOAuthAccessTokensTable } from './migrations/CreateOAuthAccessTokensTable.ts'
|
|
48
|
+
export { CreateOAuthAuthCodesTable } from './migrations/CreateOAuthAuthCodesTable.ts'
|
|
49
|
+
export { CreateOAuthRefreshTokensTable } from './migrations/CreateOAuthRefreshTokensTable.ts'
|
|
50
|
+
|
|
51
|
+
// ── Errors ───────────────────────────────────────────────────────────────────
|
|
52
|
+
export { OAuthError } from './errors/OAuthError.ts'
|
|
53
|
+
|
|
54
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
55
|
+
export { oauth, OAUTH_SERVER } from './helpers/oauth.ts'
|
|
56
|
+
|
|
57
|
+
// ── Service Provider ─────────────────────────────────────────────────────────
|
|
58
|
+
export { OAuthServiceProvider } from './OAuthServiceProvider.ts'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64URL encode/decode utilities for JWT.
|
|
3
|
+
* No external dependencies — uses standard APIs only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Encode a Uint8Array to a Base64URL string (no padding).
|
|
8
|
+
*/
|
|
9
|
+
export function base64UrlEncode(data: Uint8Array): string {
|
|
10
|
+
let binary = ''
|
|
11
|
+
for (let i = 0; i < data.length; i++) {
|
|
12
|
+
binary += String.fromCharCode(data[i]!)
|
|
13
|
+
}
|
|
14
|
+
return btoa(binary)
|
|
15
|
+
.replace(/\+/g, '-')
|
|
16
|
+
.replace(/\//g, '_')
|
|
17
|
+
.replace(/=+$/, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Decode a Base64URL string to a Uint8Array.
|
|
22
|
+
*/
|
|
23
|
+
export function base64UrlDecode(str: string): Uint8Array {
|
|
24
|
+
// Restore standard Base64 characters
|
|
25
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/')
|
|
26
|
+
// Restore padding
|
|
27
|
+
const pad = base64.length % 4
|
|
28
|
+
if (pad === 2) base64 += '=='
|
|
29
|
+
else if (pad === 3) base64 += '='
|
|
30
|
+
|
|
31
|
+
const binary = atob(base64)
|
|
32
|
+
const bytes = new Uint8Array(binary.length)
|
|
33
|
+
for (let i = 0; i < binary.length; i++) {
|
|
34
|
+
bytes[i] = binary.charCodeAt(i)
|
|
35
|
+
}
|
|
36
|
+
return bytes
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Encode a UTF-8 string to Base64URL.
|
|
41
|
+
*/
|
|
42
|
+
export function base64UrlEncodeString(str: string): string {
|
|
43
|
+
return base64UrlEncode(new TextEncoder().encode(str))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Decode a Base64URL string to a UTF-8 string.
|
|
48
|
+
*/
|
|
49
|
+
export function base64UrlDecodeString(str: string): string {
|
|
50
|
+
return new TextDecoder().decode(base64UrlDecode(str))
|
|
51
|
+
}
|