@stacksjs/auth 0.70.258 → 0.70.259
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/dist/authentication.js +1 -415
- package/dist/authenticator.js +1 -14
- package/dist/authorizable.js +1 -28
- package/dist/client.js +1 -26
- package/dist/cookie-auth.js +1 -65
- package/dist/email-verification.js +2 -110
- package/dist/gate.js +1 -203
- package/dist/index.js +1 -28
- package/dist/internal-constants.js +1 -1
- package/dist/middleware.js +1 -28
- package/dist/passkey.js +1 -76
- package/dist/password/reset.js +2 -155
- package/dist/permissions.js +1 -4
- package/dist/policy.js +1 -91
- package/dist/rate-limiter.js +1 -91
- package/dist/rbac-seed.js +1 -30
- package/dist/rbac-store-bqb.js +1 -180
- package/dist/rbac.js +1 -326
- package/dist/register.js +1 -43
- package/dist/session-auth.js +1 -151
- package/dist/team.js +1 -88
- package/dist/token.js +1 -21
- package/dist/tokens.js +42 -373
- package/dist/two-factor.js +1 -113
- package/dist/user.js +1 -38
- package/package.json +13 -6
package/dist/token.js
CHANGED
|
@@ -1,21 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createHmac, randomBytes } from "node:crypto";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
|
|
5
|
-
export class TokenManager {
|
|
6
|
-
static generateJWT(userId, expiresInSeconds = 3600) {
|
|
7
|
-
const appKey = process.env.APP_KEY;
|
|
8
|
-
if (!appKey)
|
|
9
|
-
throw Error("APP_KEY is not set. JWT tokens cannot be generated without a secure application key.");
|
|
10
|
-
const header = {
|
|
11
|
-
alg: "HS256",
|
|
12
|
-
typ: "JWT"
|
|
13
|
-
}, nowSeconds = Math.floor(Date.now() / 1000), payload = {
|
|
14
|
-
sub: userId,
|
|
15
|
-
iat: nowSeconds,
|
|
16
|
-
exp: nowSeconds + expiresInSeconds,
|
|
17
|
-
jti: randomBytes(16).toString("hex")
|
|
18
|
-
}, encodedHeader = Buffer.from(JSON.stringify(header)).toString("base64url"), encodedPayload = Buffer.from(JSON.stringify(payload)).toString("base64url"), signature = createHmac("sha256", appKey).update(`${encodedHeader}.${encodedPayload}`).digest("base64url");
|
|
19
|
-
return `${encodedHeader}.${encodedPayload}.${signature}`;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
1
|
+
import{Buffer}from"node:buffer";import{createHmac,randomBytes}from"node:crypto";import process from"node:process";export class TokenManager{static generateJWT(userId,expiresInSeconds=3600){const appKey=process.env.APP_KEY;if(!appKey)throw Error("APP_KEY is not set. JWT tokens cannot be generated without a secure application key.");const header={alg:"HS256",typ:"JWT"},nowSeconds=Math.floor(Date.now()/1000),payload={sub:userId,iat:nowSeconds,exp:nowSeconds+expiresInSeconds,jti:randomBytes(16).toString("hex")},encodedHeader=Buffer.from(JSON.stringify(header)).toString("base64url"),encodedPayload=Buffer.from(JSON.stringify(payload)).toString("base64url"),signature=createHmac("sha256",appKey).update(`${encodedHeader}.${encodedPayload}`).digest("base64url");return`${encodedHeader}.${encodedPayload}.${signature}`}}
|
package/dist/tokens.js
CHANGED
|
@@ -1,209 +1,35 @@
|
|
|
1
|
-
import process from
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { db } from "@stacksjs/database";
|
|
4
|
-
import { HttpError } from "@stacksjs/error-handling";
|
|
5
|
-
import { getCurrentRequest } from "@stacksjs/router";
|
|
6
|
-
import { env } from "@stacksjs/env";
|
|
7
|
-
import { parseSqlDateTime, sqlDateTime, sqlDateTimeLiteral, sqlHelpers } from "@stacksjs/database";
|
|
8
|
-
const dbDriver = env.DB_CONNECTION || "sqlite", sql = sqlHelpers(dbDriver), { isPostgres, isMysql, now, boolTrue, boolFalse } = sql;
|
|
9
|
-
function appNow() {
|
|
10
|
-
return sqlDateTimeLiteral();
|
|
11
|
-
}
|
|
12
|
-
function param(index) {
|
|
13
|
-
return sql.param(index);
|
|
14
|
-
}
|
|
15
|
-
function hashToken(token) {
|
|
16
|
-
return createHash("sha256").update(token).digest("hex");
|
|
17
|
-
}
|
|
18
|
-
function bearerLookupHash(bearer) {
|
|
19
|
-
const colonIdx = bearer.indexOf(":"), lookup = colonIdx === -1 ? bearer : bearer.substring(0, colonIdx);
|
|
20
|
-
return hashToken(lookup);
|
|
21
|
-
}
|
|
22
|
-
function generateSecureToken(bytes = 40) {
|
|
23
|
-
return randomBytes(bytes).toString("hex");
|
|
24
|
-
}
|
|
25
|
-
export async function getPasswordChangedAt(userId, q = db) {
|
|
26
|
-
if (userId === null || userId === void 0)
|
|
27
|
-
return null;
|
|
28
|
-
try {
|
|
29
|
-
const value = (await q.unsafe(`
|
|
1
|
+
import process from"node:process";import{createHash,randomBytes}from"node:crypto";import{db}from"@stacksjs/database";import{HttpError}from"@stacksjs/error-handling";import{getCurrentRequest}from"@stacksjs/router";import{env}from"@stacksjs/env";import{parseSqlDateTime,sqlDateTime,sqlDateTimeLiteral,sqlHelpers}from"@stacksjs/database";const dbDriver=env.DB_CONNECTION||"sqlite",sql=sqlHelpers(dbDriver),{isPostgres,isMysql,now,boolTrue,boolFalse}=sql;function appNow(){return sqlDateTimeLiteral()}function param(index){return sql.param(index)}function hashToken(token){return createHash("sha256").update(token).digest("hex")}function bearerLookupHash(bearer){const colonIdx=bearer.indexOf(":"),lookup=colonIdx===-1?bearer:bearer.substring(0,colonIdx);return hashToken(lookup)}function generateSecureToken(bytes=40){return randomBytes(bytes).toString("hex")}export async function getPasswordChangedAt(userId,q=db){if(userId===null||userId===void 0)return null;try{const value=(await q.unsafe(`
|
|
30
2
|
SELECT password_changed_at FROM users WHERE id = ${param(1)} LIMIT 1
|
|
31
|
-
`,
|
|
32
|
-
if (value === null || value === void 0)
|
|
33
|
-
return null;
|
|
34
|
-
return parseSqlDateTime(value);
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
export function isIssuedBeforePasswordChange(createdAt, changedAt) {
|
|
40
|
-
if (!changedAt)
|
|
41
|
-
return !1;
|
|
42
|
-
if (createdAt === null || createdAt === void 0)
|
|
43
|
-
return !1;
|
|
44
|
-
const created = parseSqlDateTime(createdAt);
|
|
45
|
-
if (!created)
|
|
46
|
-
return !1;
|
|
47
|
-
return created.getTime() < changedAt.getTime();
|
|
48
|
-
}
|
|
49
|
-
export async function tokens(userId) {
|
|
50
|
-
return (await db.unsafe(`
|
|
3
|
+
`,[userId]))[0]?.password_changed_at;if(value===null||value===void 0)return null;return parseSqlDateTime(value)}catch{return null}}export function isIssuedBeforePasswordChange(createdAt,changedAt){if(!changedAt)return!1;if(createdAt===null||createdAt===void 0)return!1;const created=parseSqlDateTime(createdAt);if(!created)return!1;return created.getTime()<changedAt.getTime()}export async function tokens(userId){return(await db.unsafe(`
|
|
51
4
|
SELECT t.*, c.provider as client_provider
|
|
52
5
|
FROM oauth_access_tokens t
|
|
53
6
|
LEFT JOIN oauth_clients c ON t.oauth_client_id = c.id
|
|
54
7
|
WHERE t.user_id = ${param(1)}
|
|
55
8
|
AND t.revoked = ${boolFalse}
|
|
56
9
|
ORDER BY t.created_at DESC
|
|
57
|
-
`,
|
|
58
|
-
id: row.id,
|
|
59
|
-
userId: row.user_id,
|
|
60
|
-
clientId: row.oauth_client_id,
|
|
61
|
-
name: row.name || "access-token",
|
|
62
|
-
scopes: parseScopes(row.scopes),
|
|
63
|
-
revoked: !!row.revoked,
|
|
64
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : null,
|
|
65
|
-
createdAt: new Date(row.created_at),
|
|
66
|
-
updatedAt: row.updated_at ? new Date(row.updated_at) : new Date
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
export async function findToken(plainTextToken) {
|
|
70
|
-
const hashedToken = bearerLookupHash(plainTextToken), row = (await db.unsafe(`
|
|
10
|
+
`,[userId])).map((row)=>({id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name||"access-token",scopes:parseScopes(row.scopes),revoked:!!row.revoked,expiresAt:row.expires_at?new Date(row.expires_at):null,createdAt:new Date(row.created_at),updatedAt:row.updated_at?new Date(row.updated_at):new Date}))}export async function findToken(plainTextToken){const hashedToken=bearerLookupHash(plainTextToken),row=(await db.unsafe(`
|
|
71
11
|
SELECT * FROM oauth_access_tokens
|
|
72
12
|
WHERE token = ${param(1)}
|
|
73
13
|
AND revoked = ${boolFalse}
|
|
74
14
|
AND (expires_at IS NULL OR expires_at > ${appNow()})
|
|
75
15
|
LIMIT 1
|
|
76
|
-
`,
|
|
77
|
-
if (!row)
|
|
78
|
-
return null;
|
|
79
|
-
if (isIssuedBeforePasswordChange(row.created_at, await getPasswordChangedAt(row.user_id)))
|
|
80
|
-
return null;
|
|
81
|
-
return {
|
|
82
|
-
id: row.id,
|
|
83
|
-
userId: row.user_id,
|
|
84
|
-
clientId: row.oauth_client_id,
|
|
85
|
-
name: row.name || "access-token",
|
|
86
|
-
scopes: parseScopes(row.scopes),
|
|
87
|
-
revoked: !!row.revoked,
|
|
88
|
-
expiresAt: row.expires_at ? new Date(row.expires_at) : null,
|
|
89
|
-
createdAt: new Date(row.created_at),
|
|
90
|
-
updatedAt: row.updated_at ? new Date(row.updated_at) : new Date
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
export async function currentAccessToken() {
|
|
94
|
-
const request = getCurrentRequest();
|
|
95
|
-
if (!request)
|
|
96
|
-
return null;
|
|
97
|
-
const attached = request._currentAccessToken;
|
|
98
|
-
if (attached)
|
|
99
|
-
return attached;
|
|
100
|
-
const bearerToken = request.bearerToken?.();
|
|
101
|
-
if (!bearerToken)
|
|
102
|
-
return null;
|
|
103
|
-
const token = await findToken(bearerToken);
|
|
104
|
-
if (token)
|
|
105
|
-
request._currentAccessToken = token;
|
|
106
|
-
return token;
|
|
107
|
-
}
|
|
108
|
-
export const token = currentAccessToken;
|
|
109
|
-
export async function tokenCan(scope) {
|
|
110
|
-
const accessToken = await currentAccessToken();
|
|
111
|
-
if (!accessToken)
|
|
112
|
-
return !1;
|
|
113
|
-
if (accessToken.scopes.includes("*"))
|
|
114
|
-
return !0;
|
|
115
|
-
return accessToken.scopes.includes(scope);
|
|
116
|
-
}
|
|
117
|
-
export async function tokenCant(scope) {
|
|
118
|
-
return !await tokenCan(scope);
|
|
119
|
-
}
|
|
120
|
-
export async function tokenCanAll(scopes) {
|
|
121
|
-
const accessToken = await currentAccessToken();
|
|
122
|
-
if (!accessToken)
|
|
123
|
-
return !1;
|
|
124
|
-
if (accessToken.scopes.includes("*"))
|
|
125
|
-
return !0;
|
|
126
|
-
return scopes.every((scope) => accessToken.scopes.includes(scope));
|
|
127
|
-
}
|
|
128
|
-
export async function tokenCanAny(scopes) {
|
|
129
|
-
const accessToken = await currentAccessToken();
|
|
130
|
-
if (!accessToken)
|
|
131
|
-
return !1;
|
|
132
|
-
if (accessToken.scopes.includes("*"))
|
|
133
|
-
return !0;
|
|
134
|
-
return scopes.some((scope) => accessToken.scopes.includes(scope));
|
|
135
|
-
}
|
|
136
|
-
export async function tokenAbilities() {
|
|
137
|
-
return (await currentAccessToken())?.scopes || [];
|
|
138
|
-
}
|
|
139
|
-
export async function createToken(userId, name = "access-token", scopes = ["*"], options = {}) {
|
|
140
|
-
const {
|
|
141
|
-
expiresInMinutes = 60,
|
|
142
|
-
withRefreshToken = !0,
|
|
143
|
-
refreshExpiresInDays = 30
|
|
144
|
-
} = options, client = (await db.unsafe(`
|
|
16
|
+
`,[hashedToken]))[0];if(!row)return null;if(isIssuedBeforePasswordChange(row.created_at,await getPasswordChangedAt(row.user_id)))return null;return{id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name||"access-token",scopes:parseScopes(row.scopes),revoked:!!row.revoked,expiresAt:row.expires_at?new Date(row.expires_at):null,createdAt:new Date(row.created_at),updatedAt:row.updated_at?new Date(row.updated_at):new Date}}export async function currentAccessToken(){const request=getCurrentRequest();if(!request)return null;const attached=request._currentAccessToken;if(attached)return attached;const bearerToken=request.bearerToken?.();if(!bearerToken)return null;const token=await findToken(bearerToken);if(token)request._currentAccessToken=token;return token}export const token=currentAccessToken;export async function tokenCan(scope){const accessToken=await currentAccessToken();if(!accessToken)return!1;if(accessToken.scopes.includes("*"))return!0;return accessToken.scopes.includes(scope)}export async function tokenCant(scope){return!await tokenCan(scope)}export async function tokenCanAll(scopes){const accessToken=await currentAccessToken();if(!accessToken)return!1;if(accessToken.scopes.includes("*"))return!0;return scopes.every((scope)=>accessToken.scopes.includes(scope))}export async function tokenCanAny(scopes){const accessToken=await currentAccessToken();if(!accessToken)return!1;if(accessToken.scopes.includes("*"))return!0;return scopes.some((scope)=>accessToken.scopes.includes(scope))}export async function tokenAbilities(){return(await currentAccessToken())?.scopes||[]}export async function createToken(userId,name="access-token",scopes=["*"],options={}){const{expiresInMinutes=60,withRefreshToken=!0,refreshExpiresInDays=30}=options,client=(await db.unsafe(`
|
|
145
17
|
SELECT id FROM oauth_clients WHERE personal_access_client = ${boolTrue} LIMIT 1
|
|
146
|
-
`))[0];
|
|
147
|
-
if (!client)
|
|
148
|
-
throw new HttpError(500, "No personal access client found. Run ./buddy auth:setup first.");
|
|
149
|
-
const plainTextToken = generateSecureToken(40), hashedToken = hashToken(plainTextToken), expiresAt = new Date;
|
|
150
|
-
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInMinutes);
|
|
151
|
-
if (isPostgres)
|
|
152
|
-
await db.unsafe(`
|
|
18
|
+
`))[0];if(!client)throw new HttpError(500,"No personal access client found. Run ./buddy auth:setup first.");const plainTextToken=generateSecureToken(40),hashedToken=hashToken(plainTextToken),expiresAt=new Date;expiresAt.setMinutes(expiresAt.getMinutes()+expiresInMinutes);if(isPostgres)await db.unsafe(`
|
|
153
19
|
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
154
20
|
VALUES ($1, $2, $3, $4, $5, false, $6, ${appNow()}, ${appNow()})
|
|
155
|
-
`,
|
|
156
|
-
else
|
|
157
|
-
await db.unsafe(`
|
|
21
|
+
`,[userId,client.id,hashedToken,name,JSON.stringify(scopes),sqlDateTime(expiresAt)]);else await db.unsafe(`
|
|
158
22
|
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
159
23
|
VALUES (?, ?, ?, ?, ?, 0, ?, ${appNow()}, ${appNow()})
|
|
160
|
-
`,
|
|
161
|
-
const row = (await db.unsafe(`
|
|
24
|
+
`,[userId,client.id,hashedToken,name,JSON.stringify(scopes),sqlDateTime(expiresAt)]);const row=(await db.unsafe(`
|
|
162
25
|
SELECT * FROM oauth_access_tokens WHERE token = ${param(1)} LIMIT 1
|
|
163
|
-
`,
|
|
164
|
-
if (!row)
|
|
165
|
-
throw new HttpError(500, "Failed to create access token \u2014 inserted row not found");
|
|
166
|
-
const accessToken = {
|
|
167
|
-
id: row.id,
|
|
168
|
-
userId: row.user_id,
|
|
169
|
-
clientId: row.oauth_client_id,
|
|
170
|
-
name: row.name,
|
|
171
|
-
scopes: parseScopes(row.scopes),
|
|
172
|
-
revoked: !1,
|
|
173
|
-
expiresAt,
|
|
174
|
-
createdAt: new Date(row.created_at),
|
|
175
|
-
updatedAt: new Date(row.updated_at)
|
|
176
|
-
};
|
|
177
|
-
let refreshTokenPlain;
|
|
178
|
-
if (withRefreshToken) {
|
|
179
|
-
refreshTokenPlain = generateSecureToken(40);
|
|
180
|
-
const hashedRefreshToken = hashToken(refreshTokenPlain), refreshExpiresAt = new Date;
|
|
181
|
-
refreshExpiresAt.setDate(refreshExpiresAt.getDate() + refreshExpiresInDays);
|
|
182
|
-
if (isPostgres)
|
|
183
|
-
await db.unsafe(`
|
|
26
|
+
`,[hashedToken]))[0];if(!row)throw new HttpError(500,"Failed to create access token \u2014 inserted row not found");const accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name,scopes:parseScopes(row.scopes),revoked:!1,expiresAt,createdAt:new Date(row.created_at),updatedAt:new Date(row.updated_at)};let refreshTokenPlain;if(withRefreshToken){refreshTokenPlain=generateSecureToken(40);const hashedRefreshToken=hashToken(refreshTokenPlain),refreshExpiresAt=new Date;refreshExpiresAt.setDate(refreshExpiresAt.getDate()+refreshExpiresInDays);if(isPostgres)await db.unsafe(`
|
|
184
27
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
185
28
|
VALUES ($1, $2, false, $3, ${appNow()})
|
|
186
|
-
`,
|
|
187
|
-
else
|
|
188
|
-
await db.unsafe(`
|
|
29
|
+
`,[accessToken.id,hashedRefreshToken,sqlDateTime(refreshExpiresAt)]);else await db.unsafe(`
|
|
189
30
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
190
31
|
VALUES (?, ?, 0, ?, ${appNow()})
|
|
191
|
-
`,
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
accessToken,
|
|
195
|
-
plainTextToken,
|
|
196
|
-
refreshToken: refreshTokenPlain,
|
|
197
|
-
expiresIn: expiresInMinutes * 60
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
export async function refreshToken(refreshTokenPlain, options = {}) {
|
|
201
|
-
const {
|
|
202
|
-
expiresInMinutes = 60,
|
|
203
|
-
refreshExpiresInDays = 30
|
|
204
|
-
} = options, hashedRefreshToken = hashToken(refreshTokenPlain);
|
|
205
|
-
return await db.transaction(async (rawTrx) => {
|
|
206
|
-
const trx = rawTrx, forUpdate = isPostgres || isMysql ? " FOR UPDATE" : "", refreshRow = (await trx.unsafe(`
|
|
32
|
+
`,[accessToken.id,hashedRefreshToken,sqlDateTime(refreshExpiresAt)])}return{accessToken,plainTextToken,refreshToken:refreshTokenPlain,expiresIn:expiresInMinutes*60}}export async function refreshToken(refreshTokenPlain,options={}){const{expiresInMinutes=60,refreshExpiresInDays=30}=options,hashedRefreshToken=hashToken(refreshTokenPlain);return await db.transaction(async(rawTrx)=>{const trx=rawTrx,forUpdate=isPostgres||isMysql?" FOR UPDATE":"",refreshRow=(await trx.unsafe(`
|
|
207
33
|
SELECT r.*, t.user_id, t.oauth_client_id, t.name, t.scopes
|
|
208
34
|
FROM oauth_refresh_tokens r
|
|
209
35
|
JOIN oauth_access_tokens t ON r.access_token_id = t.id
|
|
@@ -211,281 +37,124 @@ export async function refreshToken(refreshTokenPlain, options = {}) {
|
|
|
211
37
|
AND r.revoked = ${boolFalse}
|
|
212
38
|
AND (r.expires_at IS NULL OR r.expires_at > ${appNow()})
|
|
213
39
|
LIMIT 1${forUpdate}
|
|
214
|
-
`,
|
|
215
|
-
if (!refreshRow)
|
|
216
|
-
throw new HttpError(401, "Invalid or expired refresh token");
|
|
217
|
-
if (isIssuedBeforePasswordChange(refreshRow.created_at, await getPasswordChangedAt(refreshRow.user_id, trx)))
|
|
218
|
-
throw new HttpError(401, "Invalid or expired refresh token");
|
|
219
|
-
await trx.unsafe(`
|
|
40
|
+
`,[hashedRefreshToken]))[0];if(!refreshRow)throw new HttpError(401,"Invalid or expired refresh token");if(isIssuedBeforePasswordChange(refreshRow.created_at,await getPasswordChangedAt(refreshRow.user_id,trx)))throw new HttpError(401,"Invalid or expired refresh token");await trx.unsafe(`
|
|
220
41
|
UPDATE oauth_refresh_tokens
|
|
221
42
|
SET revoked = ${boolTrue}
|
|
222
43
|
WHERE id = ${param(1)}
|
|
223
|
-
`,
|
|
224
|
-
await trx.unsafe(`
|
|
44
|
+
`,[refreshRow.id]);await trx.unsafe(`
|
|
225
45
|
UPDATE oauth_access_tokens
|
|
226
46
|
SET revoked = ${boolTrue}
|
|
227
47
|
WHERE id = ${param(1)}
|
|
228
|
-
`,
|
|
229
|
-
const plainTextToken = generateSecureToken(40), hashedToken = hashToken(plainTextToken), expiresAt = new Date;
|
|
230
|
-
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInMinutes);
|
|
231
|
-
if (isPostgres)
|
|
232
|
-
await trx.unsafe(`
|
|
48
|
+
`,[refreshRow.access_token_id]);const plainTextToken=generateSecureToken(40),hashedToken=hashToken(plainTextToken),expiresAt=new Date;expiresAt.setMinutes(expiresAt.getMinutes()+expiresInMinutes);if(isPostgres)await trx.unsafe(`
|
|
233
49
|
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
234
50
|
VALUES ($1, $2, $3, $4, $5, false, $6, ${appNow()}, ${appNow()})
|
|
235
|
-
`,
|
|
236
|
-
else
|
|
237
|
-
await trx.unsafe(`
|
|
51
|
+
`,[refreshRow.user_id,refreshRow.oauth_client_id,hashedToken,refreshRow.name,refreshRow.scopes,sqlDateTime(expiresAt)]);else await trx.unsafe(`
|
|
238
52
|
INSERT INTO oauth_access_tokens (user_id, oauth_client_id, token, name, scopes, revoked, expires_at, created_at, updated_at)
|
|
239
53
|
VALUES (?, ?, ?, ?, ?, 0, ?, ${appNow()}, ${appNow()})
|
|
240
|
-
`,
|
|
241
|
-
const row = (await trx.unsafe(`
|
|
54
|
+
`,[refreshRow.user_id,refreshRow.oauth_client_id,hashedToken,refreshRow.name,refreshRow.scopes,sqlDateTime(expiresAt)]);const row=(await trx.unsafe(`
|
|
242
55
|
SELECT * FROM oauth_access_tokens WHERE token = ${param(1)} LIMIT 1
|
|
243
|
-
`,
|
|
244
|
-
id: row.id,
|
|
245
|
-
userId: row.user_id,
|
|
246
|
-
clientId: row.oauth_client_id,
|
|
247
|
-
name: row.name,
|
|
248
|
-
scopes: parseScopes(row.scopes),
|
|
249
|
-
revoked: !1,
|
|
250
|
-
expiresAt,
|
|
251
|
-
createdAt: new Date(row.created_at),
|
|
252
|
-
updatedAt: new Date(row.updated_at)
|
|
253
|
-
};
|
|
254
|
-
if (isIssuedBeforePasswordChange(row.created_at, await getPasswordChangedAt(refreshRow.user_id, trx)))
|
|
255
|
-
throw new HttpError(401, "Invalid or expired refresh token");
|
|
256
|
-
const newRefreshTokenPlain = generateSecureToken(40), newHashedRefreshToken = hashToken(newRefreshTokenPlain), refreshExpiresAt = new Date;
|
|
257
|
-
refreshExpiresAt.setDate(refreshExpiresAt.getDate() + refreshExpiresInDays);
|
|
258
|
-
if (isPostgres)
|
|
259
|
-
await trx.unsafe(`
|
|
56
|
+
`,[hashedToken]))[0],accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name,scopes:parseScopes(row.scopes),revoked:!1,expiresAt,createdAt:new Date(row.created_at),updatedAt:new Date(row.updated_at)};if(isIssuedBeforePasswordChange(row.created_at,await getPasswordChangedAt(refreshRow.user_id,trx)))throw new HttpError(401,"Invalid or expired refresh token");const newRefreshTokenPlain=generateSecureToken(40),newHashedRefreshToken=hashToken(newRefreshTokenPlain),refreshExpiresAt=new Date;refreshExpiresAt.setDate(refreshExpiresAt.getDate()+refreshExpiresInDays);if(isPostgres)await trx.unsafe(`
|
|
260
57
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
261
58
|
VALUES ($1, $2, false, $3, ${appNow()})
|
|
262
|
-
`,
|
|
263
|
-
else
|
|
264
|
-
await trx.unsafe(`
|
|
59
|
+
`,[accessToken.id,newHashedRefreshToken,sqlDateTime(refreshExpiresAt)]);else await trx.unsafe(`
|
|
265
60
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
266
61
|
VALUES (?, ?, 0, ?, ${appNow()})
|
|
267
|
-
`,
|
|
268
|
-
return {
|
|
269
|
-
accessToken,
|
|
270
|
-
plainTextToken,
|
|
271
|
-
refreshToken: newRefreshTokenPlain,
|
|
272
|
-
expiresIn: expiresInMinutes * 60
|
|
273
|
-
};
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
export async function validateRefreshToken(refreshTokenPlain) {
|
|
277
|
-
const hashedRefreshToken = hashToken(refreshTokenPlain);
|
|
278
|
-
return (await db.unsafe(`
|
|
62
|
+
`,[accessToken.id,newHashedRefreshToken,sqlDateTime(refreshExpiresAt)]);return{accessToken,plainTextToken,refreshToken:newRefreshTokenPlain,expiresIn:expiresInMinutes*60}})}export async function validateRefreshToken(refreshTokenPlain){const hashedRefreshToken=hashToken(refreshTokenPlain);return(await db.unsafe(`
|
|
279
63
|
SELECT id FROM oauth_refresh_tokens
|
|
280
64
|
WHERE token = ${param(1)}
|
|
281
65
|
AND revoked = ${boolFalse}
|
|
282
66
|
AND (expires_at IS NULL OR expires_at > ${appNow()})
|
|
283
67
|
LIMIT 1
|
|
284
|
-
`,
|
|
285
|
-
}
|
|
286
|
-
export async function revokeRefreshToken(refreshTokenPlain) {
|
|
287
|
-
const hashedRefreshToken = hashToken(refreshTokenPlain);
|
|
288
|
-
await db.unsafe(`
|
|
68
|
+
`,[hashedRefreshToken])).length>0}export async function revokeRefreshToken(refreshTokenPlain){const hashedRefreshToken=hashToken(refreshTokenPlain);await db.unsafe(`
|
|
289
69
|
UPDATE oauth_refresh_tokens
|
|
290
70
|
SET revoked = ${boolTrue}
|
|
291
71
|
WHERE token = ${param(1)}
|
|
292
|
-
`,
|
|
293
|
-
}
|
|
294
|
-
export async function revokeAllRefreshTokens(userId) {
|
|
295
|
-
await db.unsafe(`
|
|
72
|
+
`,[hashedRefreshToken])}export async function revokeAllRefreshTokens(userId){await db.unsafe(`
|
|
296
73
|
UPDATE oauth_refresh_tokens
|
|
297
74
|
SET revoked = ${boolTrue}
|
|
298
75
|
WHERE access_token_id IN (
|
|
299
76
|
SELECT id FROM oauth_access_tokens WHERE user_id = ${param(1)}
|
|
300
77
|
)
|
|
301
|
-
`,
|
|
302
|
-
}
|
|
303
|
-
export async function deleteExpiredRefreshTokens() {
|
|
304
|
-
const result = await db.unsafe(`
|
|
78
|
+
`,[userId])}export async function deleteExpiredRefreshTokens(){const result=await db.unsafe(`
|
|
305
79
|
DELETE FROM oauth_refresh_tokens
|
|
306
80
|
WHERE expires_at < ${appNow()}
|
|
307
|
-
`);
|
|
308
|
-
return result?.changes || result?.rowCount || 0;
|
|
309
|
-
}
|
|
310
|
-
export async function deleteRevokedRefreshTokens(daysOld = 7) {
|
|
311
|
-
const cutoffDate = new Date;
|
|
312
|
-
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
313
|
-
const result = await db.unsafe(`
|
|
81
|
+
`);return result?.changes||result?.rowCount||0}export async function deleteRevokedRefreshTokens(daysOld=7){const cutoffDate=new Date;cutoffDate.setDate(cutoffDate.getDate()-daysOld);const result=await db.unsafe(`
|
|
314
82
|
DELETE FROM oauth_refresh_tokens
|
|
315
83
|
WHERE revoked = ${boolTrue} AND created_at < ${param(1)}
|
|
316
|
-
`,
|
|
317
|
-
return result?.changes || result?.rowCount || 0;
|
|
318
|
-
}
|
|
319
|
-
export async function revokeToken(plainTextToken) {
|
|
320
|
-
const hashedToken = bearerLookupHash(plainTextToken);
|
|
321
|
-
await db.unsafe(`
|
|
84
|
+
`,[sqlDateTime(cutoffDate)]);return result?.changes||result?.rowCount||0}export async function revokeToken(plainTextToken){const hashedToken=bearerLookupHash(plainTextToken);await db.unsafe(`
|
|
322
85
|
UPDATE oauth_refresh_tokens
|
|
323
86
|
SET revoked = ${boolTrue}
|
|
324
87
|
WHERE access_token_id IN (
|
|
325
88
|
SELECT id FROM oauth_access_tokens WHERE token = ${param(1)}
|
|
326
89
|
)
|
|
327
|
-
`,
|
|
328
|
-
await db.unsafe(`
|
|
90
|
+
`,[hashedToken]);await db.unsafe(`
|
|
329
91
|
UPDATE oauth_access_tokens
|
|
330
92
|
SET revoked = ${boolTrue}, updated_at = ${appNow()}
|
|
331
93
|
WHERE token = ${param(1)}
|
|
332
|
-
`,
|
|
333
|
-
}
|
|
334
|
-
export async function revokeTokenById(tokenId) {
|
|
335
|
-
await db.unsafe(`
|
|
94
|
+
`,[hashedToken])}export async function revokeTokenById(tokenId){await db.unsafe(`
|
|
336
95
|
UPDATE oauth_refresh_tokens
|
|
337
96
|
SET revoked = ${boolTrue}
|
|
338
97
|
WHERE access_token_id = ${param(1)}
|
|
339
|
-
`,
|
|
340
|
-
await db.unsafe(`
|
|
98
|
+
`,[tokenId]);await db.unsafe(`
|
|
341
99
|
UPDATE oauth_access_tokens
|
|
342
100
|
SET revoked = ${boolTrue}, updated_at = ${appNow()}
|
|
343
101
|
WHERE id = ${param(1)}
|
|
344
|
-
`,
|
|
345
|
-
}
|
|
346
|
-
export async function revokeAllTokens(userId) {
|
|
347
|
-
await revokeAllRefreshTokens(userId);
|
|
348
|
-
await db.unsafe(`
|
|
102
|
+
`,[tokenId])}export async function revokeAllTokens(userId){await revokeAllRefreshTokens(userId);await db.unsafe(`
|
|
349
103
|
UPDATE oauth_access_tokens
|
|
350
104
|
SET revoked = ${boolTrue}, updated_at = ${appNow()}
|
|
351
105
|
WHERE user_id = ${param(1)}
|
|
352
|
-
`,
|
|
353
|
-
}
|
|
354
|
-
export async function revokeOtherTokens(userId) {
|
|
355
|
-
const current = await currentAccessToken();
|
|
356
|
-
if (!current)
|
|
357
|
-
return revokeAllTokens(userId);
|
|
358
|
-
if (isPostgres) {
|
|
359
|
-
await db.unsafe(`
|
|
106
|
+
`,[userId])}export async function revokeOtherTokens(userId){const current=await currentAccessToken();if(!current)return revokeAllTokens(userId);if(isPostgres){await db.unsafe(`
|
|
360
107
|
UPDATE oauth_refresh_tokens
|
|
361
108
|
SET revoked = true
|
|
362
109
|
WHERE access_token_id IN (
|
|
363
110
|
SELECT id FROM oauth_access_tokens WHERE user_id = $1 AND id != $2
|
|
364
111
|
)
|
|
365
|
-
`,
|
|
366
|
-
await db.unsafe(`
|
|
112
|
+
`,[userId,current.id]);await db.unsafe(`
|
|
367
113
|
UPDATE oauth_access_tokens
|
|
368
114
|
SET revoked = true, updated_at = ${appNow()}
|
|
369
115
|
WHERE user_id = $1 AND id != $2
|
|
370
|
-
`,
|
|
371
|
-
} else {
|
|
372
|
-
await db.unsafe(`
|
|
116
|
+
`,[userId,current.id])}else{await db.unsafe(`
|
|
373
117
|
UPDATE oauth_refresh_tokens
|
|
374
118
|
SET revoked = 1
|
|
375
119
|
WHERE access_token_id IN (
|
|
376
120
|
SELECT id FROM oauth_access_tokens WHERE user_id = ? AND id != ?
|
|
377
121
|
)
|
|
378
|
-
`,
|
|
379
|
-
await db.unsafe(`
|
|
122
|
+
`,[userId,current.id]);await db.unsafe(`
|
|
380
123
|
UPDATE oauth_access_tokens
|
|
381
124
|
SET revoked = 1, updated_at = ${appNow()}
|
|
382
125
|
WHERE user_id = ? AND id != ?
|
|
383
|
-
`,
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
export async function deleteExpiredTokens() {
|
|
387
|
-
await db.unsafe(`
|
|
126
|
+
`,[userId,current.id])}}export async function deleteExpiredTokens(){await db.unsafe(`
|
|
388
127
|
DELETE FROM oauth_refresh_tokens
|
|
389
128
|
WHERE access_token_id IN (
|
|
390
129
|
SELECT id FROM oauth_access_tokens WHERE expires_at < ${appNow()}
|
|
391
130
|
)
|
|
392
|
-
`);
|
|
393
|
-
const result = await db.unsafe(`
|
|
131
|
+
`);const result=await db.unsafe(`
|
|
394
132
|
DELETE FROM oauth_access_tokens
|
|
395
133
|
WHERE expires_at < ${appNow()}
|
|
396
|
-
`);
|
|
397
|
-
return result?.changes || result?.rowCount || 0;
|
|
398
|
-
}
|
|
399
|
-
export async function deleteRevokedTokens(daysOld = 7) {
|
|
400
|
-
const cutoffDate = new Date;
|
|
401
|
-
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
402
|
-
await db.unsafe(`
|
|
134
|
+
`);return result?.changes||result?.rowCount||0}export async function deleteRevokedTokens(daysOld=7){const cutoffDate=new Date;cutoffDate.setDate(cutoffDate.getDate()-daysOld);await db.unsafe(`
|
|
403
135
|
DELETE FROM oauth_refresh_tokens
|
|
404
136
|
WHERE access_token_id IN (
|
|
405
137
|
SELECT id FROM oauth_access_tokens WHERE revoked = ${boolTrue} AND updated_at < ${param(1)}
|
|
406
138
|
)
|
|
407
|
-
`,
|
|
408
|
-
const result = await db.unsafe(`
|
|
139
|
+
`,[sqlDateTime(cutoffDate)]);const result=await db.unsafe(`
|
|
409
140
|
DELETE FROM oauth_access_tokens
|
|
410
141
|
WHERE revoked = ${boolTrue} AND updated_at < ${param(1)}
|
|
411
|
-
`,
|
|
412
|
-
return result?.changes || result?.rowCount || 0;
|
|
413
|
-
}
|
|
414
|
-
export async function clients(userId) {
|
|
415
|
-
return (await db.unsafe(`
|
|
142
|
+
`,[sqlDateTime(cutoffDate)]);return result?.changes||result?.rowCount||0}export async function clients(userId){return(await db.unsafe(`
|
|
416
143
|
SELECT * FROM oauth_clients
|
|
417
144
|
WHERE user_id = ${param(1)} AND revoked = ${boolFalse}
|
|
418
145
|
ORDER BY created_at DESC
|
|
419
|
-
`,
|
|
420
|
-
}
|
|
421
|
-
export async function findClient(clientId) {
|
|
422
|
-
const row = (await db.unsafe(`
|
|
146
|
+
`,[userId])).map(mapToOAuthClient)}export async function findClient(clientId){const row=(await db.unsafe(`
|
|
423
147
|
SELECT * FROM oauth_clients WHERE id = ${param(1)} LIMIT 1
|
|
424
|
-
`,
|
|
425
|
-
return row ? mapToOAuthClient(row) : null;
|
|
426
|
-
}
|
|
427
|
-
export async function createClient(options) {
|
|
428
|
-
const secret = generateSecureToken(40);
|
|
429
|
-
if (isPostgres)
|
|
430
|
-
await db.unsafe(`
|
|
148
|
+
`,[clientId]))[0];return row?mapToOAuthClient(row):null}export async function createClient(options){const secret=generateSecureToken(40);if(isPostgres)await db.unsafe(`
|
|
431
149
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
432
150
|
VALUES ($1, $2, 'local', $3, $4, $5, false, ${appNow()})
|
|
433
|
-
`,
|
|
434
|
-
options.name,
|
|
435
|
-
secret,
|
|
436
|
-
options.redirect,
|
|
437
|
-
options.personalAccessClient || !1,
|
|
438
|
-
options.passwordClient || !1
|
|
439
|
-
]);
|
|
440
|
-
else
|
|
441
|
-
await db.unsafe(`
|
|
151
|
+
`,[options.name,secret,options.redirect,options.personalAccessClient||!1,options.passwordClient||!1]);else await db.unsafe(`
|
|
442
152
|
INSERT INTO oauth_clients (name, secret, provider, redirect, personal_access_client, password_client, revoked, created_at)
|
|
443
153
|
VALUES (?, ?, 'local', ?, ?, ?, 0, ${appNow()})
|
|
444
|
-
`,
|
|
445
|
-
options.name,
|
|
446
|
-
secret,
|
|
447
|
-
options.redirect,
|
|
448
|
-
options.personalAccessClient ? 1 : 0,
|
|
449
|
-
options.passwordClient ? 1 : 0
|
|
450
|
-
]);
|
|
451
|
-
const inserted = await db.unsafe(`
|
|
154
|
+
`,[options.name,secret,options.redirect,options.personalAccessClient?1:0,options.passwordClient?1:0]);const inserted=await db.unsafe(`
|
|
452
155
|
SELECT * FROM oauth_clients WHERE secret = ${param(1)} LIMIT 1
|
|
453
|
-
`,
|
|
454
|
-
return {
|
|
455
|
-
client: mapToOAuthClient(inserted[0]),
|
|
456
|
-
plainTextSecret: secret
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
export async function revokeClient(clientId) {
|
|
460
|
-
await db.unsafe(`
|
|
156
|
+
`,[secret]);return{client:mapToOAuthClient(inserted[0]),plainTextSecret:secret}}export async function revokeClient(clientId){await db.unsafe(`
|
|
461
157
|
UPDATE oauth_clients
|
|
462
158
|
SET revoked = ${boolTrue}, updated_at = ${appNow()}
|
|
463
159
|
WHERE id = ${param(1)}
|
|
464
|
-
`,
|
|
465
|
-
}
|
|
466
|
-
export function parseScopes(scopes) {
|
|
467
|
-
if (!scopes)
|
|
468
|
-
return [];
|
|
469
|
-
if (Array.isArray(scopes))
|
|
470
|
-
return scopes;
|
|
471
|
-
try {
|
|
472
|
-
const parsed = JSON.parse(scopes);
|
|
473
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
474
|
-
} catch {
|
|
475
|
-
return [];
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
function mapToOAuthClient(row) {
|
|
479
|
-
return {
|
|
480
|
-
id: row.id,
|
|
481
|
-
name: row.name,
|
|
482
|
-
secret: row.secret,
|
|
483
|
-
provider: row.provider,
|
|
484
|
-
redirect: row.redirect,
|
|
485
|
-
personalAccessClient: Boolean(row.personal_access_client),
|
|
486
|
-
passwordClient: Boolean(row.password_client),
|
|
487
|
-
revoked: Boolean(row.revoked),
|
|
488
|
-
createdAt: row.created_at ? new Date(row.created_at) : new Date,
|
|
489
|
-
updatedAt: row.updated_at ? new Date(row.updated_at) : null
|
|
490
|
-
};
|
|
491
|
-
}
|
|
160
|
+
`,[clientId])}export function parseScopes(scopes){if(!scopes)return[];if(Array.isArray(scopes))return scopes;try{const parsed=JSON.parse(scopes);return Array.isArray(parsed)?parsed:[]}catch{return[]}}function mapToOAuthClient(row){return{id:row.id,name:row.name,secret:row.secret,provider:row.provider,redirect:row.redirect,personalAccessClient:Boolean(row.personal_access_client),passwordClient:Boolean(row.password_client),revoked:Boolean(row.revoked),createdAt:row.created_at?new Date(row.created_at):new Date,updatedAt:row.updated_at?new Date(row.updated_at):null}}
|