@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,175 @@
|
|
|
1
|
+
import type { JwtPayload } from './JwtPayload.ts'
|
|
2
|
+
import { base64UrlEncode, base64UrlDecode, base64UrlEncodeString } from './JwtEncoder.ts'
|
|
3
|
+
|
|
4
|
+
const ALGORITHM: RsaHashedImportParams = {
|
|
5
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
6
|
+
hash: 'SHA-256',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* RS256 JWT signer/verifier using the Web Crypto API.
|
|
11
|
+
* No external dependencies.
|
|
12
|
+
*/
|
|
13
|
+
export class JwtSigner {
|
|
14
|
+
private privateKey: CryptoKey | null = null
|
|
15
|
+
private publicKey: CryptoKey | null = null
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Import RSA keys from PEM strings.
|
|
19
|
+
*/
|
|
20
|
+
async loadKeys(privatePem: string, publicPem: string): Promise<void> {
|
|
21
|
+
this.privateKey = await importPrivateKey(privatePem)
|
|
22
|
+
this.publicKey = await importPublicKey(publicPem)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a signed JWT string (header.payload.signature).
|
|
27
|
+
*/
|
|
28
|
+
async sign(payload: JwtPayload): Promise<string> {
|
|
29
|
+
if (!this.privateKey) {
|
|
30
|
+
throw new Error('Private key not loaded. Call loadKeys() first.')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Auto-set iat if not provided
|
|
34
|
+
if (!payload.iat) {
|
|
35
|
+
payload = { ...payload, iat: Math.floor(Date.now() / 1000) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const header = { alg: 'RS256', typ: 'JWT' }
|
|
39
|
+
const headerEncoded = base64UrlEncodeString(JSON.stringify(header))
|
|
40
|
+
const payloadEncoded = base64UrlEncodeString(JSON.stringify(payload))
|
|
41
|
+
|
|
42
|
+
const signingInput = `${headerEncoded}.${payloadEncoded}`
|
|
43
|
+
const data = new TextEncoder().encode(signingInput)
|
|
44
|
+
|
|
45
|
+
const signature = await crypto.subtle.sign(
|
|
46
|
+
ALGORITHM.name,
|
|
47
|
+
this.privateKey,
|
|
48
|
+
data,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
const signatureEncoded = base64UrlEncode(new Uint8Array(signature))
|
|
52
|
+
return `${signingInput}.${signatureEncoded}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Verify a JWT token and return the decoded payload.
|
|
57
|
+
* Returns null if the token is invalid or expired.
|
|
58
|
+
*/
|
|
59
|
+
async verify(token: string): Promise<JwtPayload | null> {
|
|
60
|
+
if (!this.publicKey) {
|
|
61
|
+
throw new Error('Public key not loaded. Call loadKeys() first.')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parts = token.split('.')
|
|
65
|
+
if (parts.length !== 3) return null
|
|
66
|
+
|
|
67
|
+
const [headerEncoded, payloadEncoded, signatureEncoded] = parts as [string, string, string]
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const signingInput = `${headerEncoded}.${payloadEncoded}`
|
|
71
|
+
const data = new TextEncoder().encode(signingInput)
|
|
72
|
+
const signature = base64UrlDecode(signatureEncoded)
|
|
73
|
+
|
|
74
|
+
const valid = await crypto.subtle.verify(
|
|
75
|
+
ALGORITHM.name,
|
|
76
|
+
this.publicKey,
|
|
77
|
+
signature,
|
|
78
|
+
data,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if (!valid) return null
|
|
82
|
+
|
|
83
|
+
const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadEncoded))
|
|
84
|
+
const payload: JwtPayload = JSON.parse(payloadJson)
|
|
85
|
+
|
|
86
|
+
// Check expiration
|
|
87
|
+
if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return payload
|
|
92
|
+
} catch {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Generate a new RSA key pair and return as PEM strings.
|
|
99
|
+
*/
|
|
100
|
+
async generateKeyPair(): Promise<{ privateKey: string; publicKey: string }> {
|
|
101
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
102
|
+
{
|
|
103
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
104
|
+
modulusLength: 2048,
|
|
105
|
+
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
|
|
106
|
+
hash: 'SHA-256',
|
|
107
|
+
},
|
|
108
|
+
true, // extractable
|
|
109
|
+
['sign', 'verify'],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const privateDer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey)
|
|
113
|
+
const publicDer = await crypto.subtle.exportKey('spki', keyPair.publicKey)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
privateKey: derToPem(privateDer, 'PRIVATE'),
|
|
117
|
+
publicKey: derToPem(publicDer, 'PUBLIC'),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── PEM helpers ────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function pemToDer(pem: string): ArrayBuffer {
|
|
125
|
+
const lines = pem.split('\n').filter(
|
|
126
|
+
(line) => !line.startsWith('-----') && line.trim().length > 0,
|
|
127
|
+
)
|
|
128
|
+
const base64 = lines.join('')
|
|
129
|
+
const binary = atob(base64)
|
|
130
|
+
const bytes = new Uint8Array(binary.length)
|
|
131
|
+
for (let i = 0; i < binary.length; i++) {
|
|
132
|
+
bytes[i] = binary.charCodeAt(i)
|
|
133
|
+
}
|
|
134
|
+
return bytes.buffer
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function derToPem(der: ArrayBuffer, type: 'PRIVATE' | 'PUBLIC'): string {
|
|
138
|
+
const bytes = new Uint8Array(der)
|
|
139
|
+
let binary = ''
|
|
140
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
141
|
+
binary += String.fromCharCode(bytes[i]!)
|
|
142
|
+
}
|
|
143
|
+
const base64 = btoa(binary)
|
|
144
|
+
|
|
145
|
+
// Wrap at 64 characters
|
|
146
|
+
const lines: string[] = []
|
|
147
|
+
for (let i = 0; i < base64.length; i += 64) {
|
|
148
|
+
lines.push(base64.slice(i, i + 64))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const label = type === 'PRIVATE' ? 'PRIVATE KEY' : 'PUBLIC KEY'
|
|
152
|
+
return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function importPrivateKey(pem: string): Promise<CryptoKey> {
|
|
156
|
+
const der = pemToDer(pem)
|
|
157
|
+
return crypto.subtle.importKey(
|
|
158
|
+
'pkcs8',
|
|
159
|
+
der,
|
|
160
|
+
ALGORITHM,
|
|
161
|
+
false,
|
|
162
|
+
['sign'],
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function importPublicKey(pem: string): Promise<CryptoKey> {
|
|
167
|
+
const der = pemToDer(pem)
|
|
168
|
+
return crypto.subtle.importKey(
|
|
169
|
+
'spki',
|
|
170
|
+
der,
|
|
171
|
+
ALGORITHM,
|
|
172
|
+
false,
|
|
173
|
+
['verify'],
|
|
174
|
+
)
|
|
175
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import type { JwtSigner } from '../jwt/JwtSigner.ts'
|
|
3
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Middleware for client_credentials grant tokens.
|
|
7
|
+
* These tokens have no user — just a client.
|
|
8
|
+
* Validates the JWT and checks that the token exists and has the required scopes.
|
|
9
|
+
*
|
|
10
|
+
* Usage in route middleware: 'client:read,write'
|
|
11
|
+
*/
|
|
12
|
+
export class CheckClientCredentials {
|
|
13
|
+
private scopes: string[] = []
|
|
14
|
+
|
|
15
|
+
constructor(private readonly signer: JwtSigner) {}
|
|
16
|
+
|
|
17
|
+
setParameters(...scopes: string[]): void {
|
|
18
|
+
this.scopes = scopes
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async handle(request: MantiqRequest, next: () => Promise<Response>): Promise<Response> {
|
|
22
|
+
const bearerToken = request.bearerToken()
|
|
23
|
+
if (!bearerToken) {
|
|
24
|
+
return new Response(
|
|
25
|
+
JSON.stringify({ message: 'Unauthenticated.' }),
|
|
26
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Verify JWT
|
|
31
|
+
const payload = await this.signer.verify(bearerToken)
|
|
32
|
+
if (!payload || !payload.jti) {
|
|
33
|
+
return new Response(
|
|
34
|
+
JSON.stringify({ message: 'Invalid token.' }),
|
|
35
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Look up the access token
|
|
40
|
+
const accessToken = await AccessToken.find(payload.jti)
|
|
41
|
+
if (!accessToken || accessToken.getAttribute('revoked') || accessToken.isExpired()) {
|
|
42
|
+
return new Response(
|
|
43
|
+
JSON.stringify({ message: 'Token is invalid or expired.' }),
|
|
44
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check required scopes
|
|
49
|
+
for (const scope of this.scopes) {
|
|
50
|
+
if (accessToken.cant(scope)) {
|
|
51
|
+
return new Response(
|
|
52
|
+
JSON.stringify({ message: `Missing scope: ${scope}` }),
|
|
53
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return next()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Middleware that requires at least ONE of the listed scopes on the JWT token.
|
|
6
|
+
*
|
|
7
|
+
* Usage in route middleware: 'scope:read,write'
|
|
8
|
+
*/
|
|
9
|
+
export class CheckForAnyScope {
|
|
10
|
+
private scopes: string[] = []
|
|
11
|
+
|
|
12
|
+
setParameters(...scopes: string[]): void {
|
|
13
|
+
this.scopes = scopes
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async handle(request: MantiqRequest, next: () => Promise<Response>): Promise<Response> {
|
|
17
|
+
const user = request.user<any>()
|
|
18
|
+
if (!user) {
|
|
19
|
+
return new Response(
|
|
20
|
+
JSON.stringify({ message: 'Unauthenticated.' }),
|
|
21
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get the access token from the user
|
|
26
|
+
const token = this.resolveToken(user)
|
|
27
|
+
if (!token) {
|
|
28
|
+
return new Response(
|
|
29
|
+
JSON.stringify({ message: 'Token not found.' }),
|
|
30
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hasAny = this.scopes.some((scope) => token.can(scope))
|
|
35
|
+
if (!hasAny) {
|
|
36
|
+
return new Response(
|
|
37
|
+
JSON.stringify({ message: 'Insufficient scopes.' }),
|
|
38
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return next()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private resolveToken(user: any): AccessToken | null {
|
|
46
|
+
if (typeof user.currentAccessToken === 'function') {
|
|
47
|
+
return user.currentAccessToken() as AccessToken | null
|
|
48
|
+
}
|
|
49
|
+
if (user._accessToken) return user._accessToken as AccessToken
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MantiqRequest } from '@mantiq/core'
|
|
2
|
+
import { AccessToken } from '../models/AccessToken.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Middleware that requires ALL listed scopes on the JWT token.
|
|
6
|
+
*
|
|
7
|
+
* Usage in route middleware: 'scopes:read,write'
|
|
8
|
+
*/
|
|
9
|
+
export class CheckScopes {
|
|
10
|
+
private scopes: string[] = []
|
|
11
|
+
|
|
12
|
+
setParameters(...scopes: string[]): void {
|
|
13
|
+
this.scopes = scopes
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async handle(request: MantiqRequest, next: () => Promise<Response>): Promise<Response> {
|
|
17
|
+
const user = request.user<any>()
|
|
18
|
+
if (!user) {
|
|
19
|
+
return new Response(
|
|
20
|
+
JSON.stringify({ message: 'Unauthenticated.' }),
|
|
21
|
+
{ status: 401, headers: { 'Content-Type': 'application/json' } },
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get the access token from the user
|
|
26
|
+
const token = this.resolveToken(user)
|
|
27
|
+
if (!token) {
|
|
28
|
+
return new Response(
|
|
29
|
+
JSON.stringify({ message: 'Token not found.' }),
|
|
30
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const scope of this.scopes) {
|
|
35
|
+
if (token.cant(scope)) {
|
|
36
|
+
return new Response(
|
|
37
|
+
JSON.stringify({ message: `Missing scope: ${scope}` }),
|
|
38
|
+
{ status: 403, headers: { 'Content-Type': 'application/json' } },
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return next()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private resolveToken(user: any): AccessToken | null {
|
|
47
|
+
if (typeof user.currentAccessToken === 'function') {
|
|
48
|
+
return user.currentAccessToken() as AccessToken | null
|
|
49
|
+
}
|
|
50
|
+
if (user._accessToken) return user._accessToken as AccessToken
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Migration } from '@mantiq/database'
|
|
2
|
+
import type { SchemaBuilder } from '@mantiq/database'
|
|
3
|
+
|
|
4
|
+
export class CreateOAuthAccessTokensTable extends Migration {
|
|
5
|
+
override async up(schema: SchemaBuilder): Promise<void> {
|
|
6
|
+
await schema.create('oauth_access_tokens', (table) => {
|
|
7
|
+
table.uuid('id').primary()
|
|
8
|
+
table.string('user_id').nullable()
|
|
9
|
+
table.uuid('client_id').nullable()
|
|
10
|
+
table.string('name').nullable()
|
|
11
|
+
table.json('scopes').nullable()
|
|
12
|
+
table.boolean('revoked').default(false)
|
|
13
|
+
table.timestamp('expires_at').nullable()
|
|
14
|
+
table.timestamps()
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override async down(schema: SchemaBuilder): Promise<void> {
|
|
19
|
+
await schema.dropIfExists('oauth_access_tokens')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Migration } from '@mantiq/database'
|
|
2
|
+
import type { SchemaBuilder } from '@mantiq/database'
|
|
3
|
+
|
|
4
|
+
export class CreateOAuthAuthCodesTable extends Migration {
|
|
5
|
+
override async up(schema: SchemaBuilder): Promise<void> {
|
|
6
|
+
await schema.create('oauth_auth_codes', (table) => {
|
|
7
|
+
table.uuid('id').primary()
|
|
8
|
+
table.string('user_id')
|
|
9
|
+
table.uuid('client_id')
|
|
10
|
+
table.json('scopes').nullable()
|
|
11
|
+
table.boolean('revoked').default(false)
|
|
12
|
+
table.timestamp('expires_at').nullable()
|
|
13
|
+
table.string('code_challenge').nullable()
|
|
14
|
+
table.string('code_challenge_method').nullable()
|
|
15
|
+
table.timestamps()
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override async down(schema: SchemaBuilder): Promise<void> {
|
|
20
|
+
await schema.dropIfExists('oauth_auth_codes')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Migration } from '@mantiq/database'
|
|
2
|
+
import type { SchemaBuilder } from '@mantiq/database'
|
|
3
|
+
|
|
4
|
+
export class CreateOAuthClientsTable extends Migration {
|
|
5
|
+
override async up(schema: SchemaBuilder): Promise<void> {
|
|
6
|
+
await schema.create('oauth_clients', (table) => {
|
|
7
|
+
table.uuid('id').primary()
|
|
8
|
+
table.string('user_id').nullable()
|
|
9
|
+
table.string('name')
|
|
10
|
+
table.string('secret', 100).nullable()
|
|
11
|
+
table.string('redirect')
|
|
12
|
+
table.json('grant_types').nullable()
|
|
13
|
+
table.json('scopes').nullable()
|
|
14
|
+
table.boolean('personal_access_client').default(false)
|
|
15
|
+
table.boolean('password_client').default(false)
|
|
16
|
+
table.boolean('revoked').default(false)
|
|
17
|
+
table.timestamps()
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override async down(schema: SchemaBuilder): Promise<void> {
|
|
22
|
+
await schema.dropIfExists('oauth_clients')
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Migration } from '@mantiq/database'
|
|
2
|
+
import type { SchemaBuilder } from '@mantiq/database'
|
|
3
|
+
|
|
4
|
+
export class CreateOAuthRefreshTokensTable extends Migration {
|
|
5
|
+
override async up(schema: SchemaBuilder): Promise<void> {
|
|
6
|
+
await schema.create('oauth_refresh_tokens', (table) => {
|
|
7
|
+
table.uuid('id').primary()
|
|
8
|
+
table.uuid('access_token_id')
|
|
9
|
+
table.boolean('revoked').default(false)
|
|
10
|
+
table.timestamp('expires_at').nullable()
|
|
11
|
+
table.timestamps()
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override async down(schema: SchemaBuilder): Promise<void> {
|
|
16
|
+
await schema.dropIfExists('oauth_refresh_tokens')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Model } from '@mantiq/database'
|
|
2
|
+
|
|
3
|
+
export class AccessToken extends Model {
|
|
4
|
+
static override table = 'oauth_access_tokens'
|
|
5
|
+
static override keyType = 'string' as const
|
|
6
|
+
static override incrementing = false
|
|
7
|
+
static override fillable = [
|
|
8
|
+
'user_id',
|
|
9
|
+
'client_id',
|
|
10
|
+
'name',
|
|
11
|
+
'scopes',
|
|
12
|
+
'revoked',
|
|
13
|
+
'expires_at',
|
|
14
|
+
]
|
|
15
|
+
static override casts = {
|
|
16
|
+
scopes: 'json' as const,
|
|
17
|
+
revoked: 'boolean' as const,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if the token has the given scope.
|
|
22
|
+
*/
|
|
23
|
+
can(scope: string): boolean {
|
|
24
|
+
const scopes = this.getAttribute('scopes') as string[] | null
|
|
25
|
+
if (!scopes) return false
|
|
26
|
+
return scopes.includes('*') || scopes.includes(scope)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if the token does NOT have the given scope.
|
|
31
|
+
*/
|
|
32
|
+
cant(scope: string): boolean {
|
|
33
|
+
return !this.can(scope)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Revoke the access token.
|
|
38
|
+
*/
|
|
39
|
+
async revoke(): Promise<void> {
|
|
40
|
+
this.setAttribute('revoked', true)
|
|
41
|
+
await this.save()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if the token has expired.
|
|
46
|
+
*/
|
|
47
|
+
isExpired(): boolean {
|
|
48
|
+
const expiresAt = this.getAttribute('expires_at')
|
|
49
|
+
if (!expiresAt) return false
|
|
50
|
+
return new Date(expiresAt) < new Date()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Model } from '@mantiq/database'
|
|
2
|
+
|
|
3
|
+
export class AuthCode extends Model {
|
|
4
|
+
static override table = 'oauth_auth_codes'
|
|
5
|
+
static override keyType = 'string' as const
|
|
6
|
+
static override incrementing = false
|
|
7
|
+
static override fillable = [
|
|
8
|
+
'user_id',
|
|
9
|
+
'client_id',
|
|
10
|
+
'scopes',
|
|
11
|
+
'revoked',
|
|
12
|
+
'expires_at',
|
|
13
|
+
'code_challenge',
|
|
14
|
+
'code_challenge_method',
|
|
15
|
+
]
|
|
16
|
+
static override casts = {
|
|
17
|
+
scopes: 'json' as const,
|
|
18
|
+
revoked: 'boolean' as const,
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Model } from '@mantiq/database'
|
|
2
|
+
|
|
3
|
+
export class Client extends Model {
|
|
4
|
+
static override table = 'oauth_clients'
|
|
5
|
+
static override keyType = 'string' as const
|
|
6
|
+
static override incrementing = false
|
|
7
|
+
static override fillable = [
|
|
8
|
+
'name',
|
|
9
|
+
'secret',
|
|
10
|
+
'redirect',
|
|
11
|
+
'personal_access_client',
|
|
12
|
+
'password_client',
|
|
13
|
+
]
|
|
14
|
+
static override hidden = ['secret']
|
|
15
|
+
static override casts = {
|
|
16
|
+
grant_types: 'json' as const,
|
|
17
|
+
scopes: 'json' as const,
|
|
18
|
+
personal_access_client: 'boolean' as const,
|
|
19
|
+
password_client: 'boolean' as const,
|
|
20
|
+
revoked: 'boolean' as const,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if the client is a confidential client (has a secret).
|
|
25
|
+
*/
|
|
26
|
+
confidential(): boolean {
|
|
27
|
+
return !!this.getAttribute('secret')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if the client is a first-party (personal access) client.
|
|
32
|
+
*/
|
|
33
|
+
firstParty(): boolean {
|
|
34
|
+
return !!this.getAttribute('personal_access_client')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Model } from '@mantiq/database'
|
|
2
|
+
|
|
3
|
+
export class RefreshToken extends Model {
|
|
4
|
+
static override table = 'oauth_refresh_tokens'
|
|
5
|
+
static override keyType = 'string' as const
|
|
6
|
+
static override incrementing = false
|
|
7
|
+
static override fillable = [
|
|
8
|
+
'access_token_id',
|
|
9
|
+
'revoked',
|
|
10
|
+
'expires_at',
|
|
11
|
+
]
|
|
12
|
+
static override casts = {
|
|
13
|
+
revoked: 'boolean' as const,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Revoke the refresh token.
|
|
18
|
+
*/
|
|
19
|
+
async revoke(): Promise<void> {
|
|
20
|
+
this.setAttribute('revoked', true)
|
|
21
|
+
await this.save()
|
|
22
|
+
}
|
|
23
|
+
}
|