@stacksjs/auth 0.75.182 → 0.75.184
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/tokens.d.ts +73 -0
- package/dist/tokens.js +11 -7
- package/package.json +13 -13
package/dist/tokens.d.ts
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
import type { AccessToken, CreateClientOptions, CreateClientResult, OAuthClient, PersonalAccessTokenResult, RefreshTokenResult, TokenScopes } from '@stacksjs/types';
|
|
2
|
+
/**
|
|
3
|
+
* The row that came back is the row that was written.
|
|
4
|
+
*
|
|
5
|
+
* `expires_at` is checked by the database, through `expires_at_matches`, and
|
|
6
|
+
* not by comparing a parsed Date against the instant asked for. A zoneless
|
|
7
|
+
* column carries no offset, so the value a driver hands back is its own
|
|
8
|
+
* reading of it: the same naive literal came back as a Date eight hours early
|
|
9
|
+
* in one API process while a standalone script against the same column and the
|
|
10
|
+
* same `db` export round-tripped exactly, which rejected correctly stored
|
|
11
|
+
* tokens and 500'd every login and register on that host
|
|
12
|
+
* (stacksjs/stacks#2902).
|
|
13
|
+
*
|
|
14
|
+
* Asking the database compares the stored value against the literal that was
|
|
15
|
+
* written, in the type system that stored it, which is the question this check
|
|
16
|
+
* was always asking. Nothing in the path then depends on how a driver chose to
|
|
17
|
+
* localize a column that names no zone.
|
|
18
|
+
*
|
|
19
|
+
* One consequence worth stating: on a column Postgres holds as `timestamptz`
|
|
20
|
+
* this now passes. The literal is read through the session zone on the way in
|
|
21
|
+
* and again on the way out, so it equals itself while the instant stored is
|
|
22
|
+
* not the instant the app asked for. That is a schema fault rather than a
|
|
23
|
+
* failed insert, and it has its own louder report in
|
|
24
|
+
* `ensureUtcDatetimeColumns`, which names the column and prints the statement
|
|
25
|
+
* that converts it. This check used to fail on it by accident, while also
|
|
26
|
+
* failing on correctly stored rows, which is what took logins down.
|
|
27
|
+
*
|
|
28
|
+
* Exported for tests.
|
|
29
|
+
*/
|
|
30
|
+
export declare function verifyAccessTokenInsert(row: AccessTokenRow, expected: {
|
|
31
|
+
ownerId: number
|
|
32
|
+
ownerType: string
|
|
33
|
+
clientId: number
|
|
34
|
+
scopes: TokenScopes
|
|
35
|
+
expiresAt: Date
|
|
36
|
+
}): void;
|
|
2
37
|
export declare function getTokenOwnerCredentialState(ownerId: unknown, q?: { unsafe: (sql: string, params?: any[]) => any }, ownerType?: string): Promise<TokenOwnerCredentialState>;
|
|
3
38
|
export declare function getPasswordChangedAt(ownerId: unknown, q?: { unsafe: (sql: string, params?: any[]) => any }, ownerType?: string): Promise<Date | null>;
|
|
4
39
|
/**
|
|
@@ -302,3 +337,41 @@ export declare interface TokenOwnerCredentialState {
|
|
|
302
337
|
exists: boolean
|
|
303
338
|
passwordChangedAt: Date | null
|
|
304
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* The token tables, as these queries select them.
|
|
342
|
+
*
|
|
343
|
+
* `db.unsafe` answers `Record<string, unknown>`, so every field read off a
|
|
344
|
+
* result was `unknown` and eleven call sites cast the whole array to `any[]`
|
|
345
|
+
* to get at it - which meant a column renamed in a migration, or misspelt in
|
|
346
|
+
* one of the SQL strings below, read as `undefined` and flowed silently into a
|
|
347
|
+
* token record.
|
|
348
|
+
*
|
|
349
|
+
* These tables are not in the generated `database/types.d.ts`, so the shapes
|
|
350
|
+
* are written here, beside the queries that select them. Fields are typed as
|
|
351
|
+
* the drivers return them: sqlite gives 0/1 for booleans and strings for
|
|
352
|
+
* dates, which is why the code coerces at each use.
|
|
353
|
+
*/
|
|
354
|
+
declare interface AccessTokenRow {
|
|
355
|
+
id: number
|
|
356
|
+
tokenable_type: string
|
|
357
|
+
tokenable_id: number
|
|
358
|
+
user_id: number
|
|
359
|
+
name: string
|
|
360
|
+
scopes: string | null
|
|
361
|
+
revoked: number | boolean | null
|
|
362
|
+
expires_at: string | null
|
|
363
|
+
expires_at_matches?: boolean | number | null
|
|
364
|
+
created_at: string
|
|
365
|
+
updated_at: string | null
|
|
366
|
+
ip_address: string | null
|
|
367
|
+
user_agent: string | null
|
|
368
|
+
oauth_client_id: number
|
|
369
|
+
oauth_grant_id: string | null
|
|
370
|
+
resources: string | null
|
|
371
|
+
audiences: string | null
|
|
372
|
+
workspace_id: string | null
|
|
373
|
+
grant_scopes: string | null
|
|
374
|
+
password_changed_at: string | null
|
|
375
|
+
access_token_id: number | null
|
|
376
|
+
provider: string | null
|
|
377
|
+
}
|
package/dist/tokens.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import{createHash,randomBytes}from"node:crypto";import{config}from"@stacksjs/config";import{db,getDatabaseDialect,markContextWrote,mutationCount}from"@stacksjs/database/runtime";import{HttpError}from"@stacksjs/error-handling";import{getCurrentRequest}from"@stacksjs/router";import{makeHash}from"@stacksjs/security";import{revokeTokenPair,revokeTokenPairs}from"./token-revocation";import{requestToken}from"./request-token";import{tokenDate,tokenTimestamps}from"./token-dates";import{parseSqlDateTime,sqlDateTime,sqlDateTimeLiteral,sqlHelpers}from"@stacksjs/database/runtime";function tokenSql(){return sqlHelpers(getDatabaseDialect())}function appNow(){return sqlDateTimeLiteral()}function validTokenExpiry(value){if(value==null)return!0;const expiry=parseSqlDateTime(value);return expiry!=null&&expiry.getTime()>Date.now()}async function verifyRefreshTokenInsert(trx,hash,accessTokenId,expiresAt){const{param}=tokenSql(),rows=await trx.unsafe(`
|
|
2
2
|
SELECT access_token_id, revoked, expires_at FROM oauth_refresh_tokens WHERE token = ${param(1)}
|
|
3
|
-
`,[hash]),row=rows[0],storedExpiry=parseSqlDateTime(row?.expires_at)?.getTime(),expectedExpiry=expiresAt.getTime();if(rows.length!==1||!row||String(row.access_token_id)!==String(accessTokenId)||row.revoked!==!1&&row.revoked!==0||storedExpiry!==expectedExpiry)throw new HttpError(500,"Failed to persist the requested refresh token")}function verifyAccessTokenInsert(row,expected){if(String(row.tokenable_id)!==String(expected.ownerId)||String(row.user_id)!==String(expected.ownerId)||row.tokenable_type!==expected.ownerType||String(row.oauth_client_id)!==String(expected.clientId)||row.revoked!==!1&&row.revoked!==0
|
|
3
|
+
`,[hash]),row=rows[0],storedExpiry=parseSqlDateTime(row?.expires_at)?.getTime(),expectedExpiry=expiresAt.getTime();if(rows.length!==1||!row||String(row.access_token_id)!==String(accessTokenId)||row.revoked!==!1&&row.revoked!==0||storedExpiry!==expectedExpiry)throw new HttpError(500,"Failed to persist the requested refresh token")}function storedAsWritten(matches){return matches===!0||matches===1}export function verifyAccessTokenInsert(row,expected){if(String(row.tokenable_id)!==String(expected.ownerId)||String(row.user_id)!==String(expected.ownerId)||row.tokenable_type!==expected.ownerType||String(row.oauth_client_id)!==String(expected.clientId)||row.revoked!==!1&&row.revoked!==0||!storedAsWritten(row.expires_at_matches)||JSON.stringify(parseScopes(row.scopes))!==JSON.stringify(expected.scopes))throw new HttpError(500,"Failed to persist the requested access token")}function refreshDeadline(days){const{isMysql}=tokenSql(),deadline=new Date(Date.now()+days*24*60*60*1000);if(isMysql)deadline.setUTCMilliseconds(0);return deadline}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 const DEFAULT_TOKENABLE_TYPE="users";function databaseBoolean(value){return value===!0||value===1||value===1n||value==="1"||value==="t"}function delegatedContext(row){return{grantId:row.oauth_grant_id??null,resources:row.oauth_grant_id?parseScopes(row.resources):[],audiences:row.oauth_grant_id?parseScopes(row.audiences):[],workspaceId:row.oauth_grant_id?row.workspace_id??null:null}}export async function getTokenOwnerCredentialState(ownerId,q=db,ownerType=DEFAULT_TOKENABLE_TYPE){const{isMysql,isPostgres,param}=tokenSql();if(ownerId===null||ownerId===void 0)return{exists:!1,passwordChangedAt:null};const quote=isMysql?"`":'"',table=`${quote}${ownerType.replaceAll(quote,quote+quote)}${quote}`,readOwner=async(includePasswordStamp)=>{const row=(await q.unsafe(`
|
|
4
4
|
SELECT ${includePasswordStamp?"id, password_changed_at":"id"} FROM ${table} WHERE id = ${param(1)} LIMIT 1
|
|
5
5
|
`,[ownerId]))[0];if(!row)return{exists:!1,passwordChangedAt:null};const value=row.password_changed_at;return{exists:!0,passwordChangedAt:value===null||value===void 0?null:parseSqlDateTime(value)}};try{if(isMysql){const schema=(await q.unsafe(`
|
|
6
6
|
SELECT
|
|
@@ -65,9 +65,11 @@ import{createHash,randomBytes}from"node:crypto";import{config}from"@stacksjs/con
|
|
|
65
65
|
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ${appNow()}, ${appNow()})
|
|
66
66
|
`,[tokenableType,userId,userId,client.id,hashedToken,name,JSON.stringify(scopes),sqlDateTime(expiresAt),agent,address]);const row=(await trx.unsafe(`
|
|
67
67
|
SELECT id, user_id, tokenable_type, tokenable_id, oauth_client_id,
|
|
68
|
-
name, scopes, revoked, expires_at,
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
name, scopes, revoked, expires_at,
|
|
69
|
+
(expires_at = ${param(1)}) AS expires_at_matches,
|
|
70
|
+
created_at, updated_at
|
|
71
|
+
FROM oauth_access_tokens WHERE token = ${param(2)} LIMIT 1
|
|
72
|
+
`,[sqlDateTime(expiresAt),hashedToken]))[0];if(!row)throw new HttpError(500,"Failed to create access token - inserted row not found");verifyAccessTokenInsert(row,{ownerId:userId,ownerType:tokenableType,clientId:client.id,scopes,expiresAt});const accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name,scopes:parseScopes(row.scopes),revoked:!1,expiresAt,...tokenTimestamps(row)};let refreshTokenPlain;if(withRefreshToken){refreshTokenPlain=generateSecureToken(40);const hashedRefreshToken=hashToken(refreshTokenPlain),refreshExpiresAt=refreshDeadline(refreshExpiresInDays);if(isPostgres)await trx.unsafe(`
|
|
71
73
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
72
74
|
VALUES ($1, $2, false, $3, ${appNow()})
|
|
73
75
|
`,[accessToken.id,hashedRefreshToken,sqlDateTime(refreshExpiresAt)]);else await trx.unsafe(`
|
|
@@ -98,9 +100,11 @@ import{createHash,randomBytes}from"node:crypto";import{config}from"@stacksjs/con
|
|
|
98
100
|
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ${appNow()}, ${appNow()})
|
|
99
101
|
`,[refreshRow.tokenable_type,refreshRow.tokenable_id,refreshRow.tokenable_id,refreshRow.oauth_client_id,hashedToken,refreshRow.name,refreshRow.scopes,sqlDateTime(expiresAt)]);const row=(await trx.unsafe(`
|
|
100
102
|
SELECT id, user_id, tokenable_type, tokenable_id, oauth_client_id,
|
|
101
|
-
name, scopes, revoked, expires_at,
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
name, scopes, revoked, expires_at,
|
|
104
|
+
(expires_at = ${param(1)}) AS expires_at_matches,
|
|
105
|
+
created_at, updated_at
|
|
106
|
+
FROM oauth_access_tokens WHERE token = ${param(2)} LIMIT 1
|
|
107
|
+
`,[sqlDateTime(expiresAt),hashedToken]))[0];if(!row)throw new HttpError(500,"Failed to read back the access token that was just created.");verifyAccessTokenInsert(row,{ownerId:refreshRow.tokenable_id,ownerType:refreshRow.tokenable_type,clientId:refreshRow.oauth_client_id,scopes:parseScopes(refreshRow.scopes),expiresAt});const accessToken={id:row.id,userId:row.user_id,clientId:row.oauth_client_id,name:row.name||"access-token",scopes:parseScopes(row.scopes),revoked:!1,expiresAt,...tokenTimestamps(row)};if(isIssuedBeforePasswordChange(row.created_at,await getPasswordChangedAt(refreshRow.tokenable_id,trx,refreshRow.tokenable_type??DEFAULT_TOKENABLE_TYPE)))throw new HttpError(401,"Invalid or expired refresh token");const newRefreshTokenPlain=generateSecureToken(40),newHashedRefreshToken=hashToken(newRefreshTokenPlain),refreshExpiresAt=refreshDeadline(refreshExpiresInDays);if(isPostgres)await trx.unsafe(`
|
|
104
108
|
INSERT INTO oauth_refresh_tokens (access_token_id, token, revoked, expires_at, created_at)
|
|
105
109
|
VALUES ($1, $2, false, $3, ${appNow()})
|
|
106
110
|
`,[accessToken.id,newHashedRefreshToken,sqlDateTime(refreshExpiresAt)]);else await trx.unsafe(`
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/auth",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.75.
|
|
5
|
+
"version": "0.75.184",
|
|
6
6
|
"description": "A more simplistic way to authenticate.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -58,18 +58,18 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@stacksjs/bun-router": "^0.1.24",
|
|
61
|
-
"@stacksjs/cache": "0.75.
|
|
62
|
-
"@stacksjs/config": "0.75.
|
|
63
|
-
"@stacksjs/database": "0.75.
|
|
64
|
-
"@stacksjs/email": "0.75.
|
|
65
|
-
"@stacksjs/env": "0.75.
|
|
66
|
-
"@stacksjs/error-handling": "0.75.
|
|
67
|
-
"@stacksjs/logging": "0.75.
|
|
68
|
-
"@stacksjs/orm": "0.75.
|
|
69
|
-
"@stacksjs/path": "0.75.
|
|
70
|
-
"@stacksjs/router": "0.75.
|
|
71
|
-
"@stacksjs/security": "0.75.
|
|
72
|
-
"@stacksjs/storage": "0.75.
|
|
61
|
+
"@stacksjs/cache": "0.75.184",
|
|
62
|
+
"@stacksjs/config": "0.75.184",
|
|
63
|
+
"@stacksjs/database": "0.75.184",
|
|
64
|
+
"@stacksjs/email": "0.75.184",
|
|
65
|
+
"@stacksjs/env": "0.75.184",
|
|
66
|
+
"@stacksjs/error-handling": "0.75.184",
|
|
67
|
+
"@stacksjs/logging": "0.75.184",
|
|
68
|
+
"@stacksjs/orm": "0.75.184",
|
|
69
|
+
"@stacksjs/path": "0.75.184",
|
|
70
|
+
"@stacksjs/router": "0.75.184",
|
|
71
|
+
"@stacksjs/security": "0.75.184",
|
|
72
|
+
"@stacksjs/storage": "0.75.184",
|
|
73
73
|
"@stacksjs/ts-auth": "^0.4.5",
|
|
74
74
|
"ts-qr-codes": "^0.1.8"
|
|
75
75
|
},
|