@silkweave/auth 1.3.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.
@@ -0,0 +1,69 @@
1
+ import type { OAuthStore } from '../provider/store.js'
2
+
3
+ export interface RedisClient {
4
+ get(key: string): Promise<unknown>
5
+ set(key: string, value: string, options?: { ex?: number }): Promise<unknown>
6
+ del(key: string): Promise<unknown>
7
+ }
8
+
9
+ export interface RedisStoreOptions {
10
+ client: RedisClient
11
+ prefix?: string
12
+ }
13
+
14
+ export function createRedisStore(options: RedisStoreOptions): OAuthStore {
15
+ const { client, prefix = 'silkweave:oauth:' } = options
16
+
17
+ function key(namespace: string, id: string) {
18
+ return `${prefix}${namespace}:${id}`
19
+ }
20
+
21
+ function ttlFromExpiry(expiresAt: number): number | undefined {
22
+ const seconds = Math.floor(expiresAt - Date.now() / 1000)
23
+ return seconds > 0 ? seconds : undefined
24
+ }
25
+
26
+ async function save<T>(namespace: string, id: string, data: T, expiresAt?: number) {
27
+ const opts: { ex?: number } = {}
28
+ const ttl = expiresAt != null ? ttlFromExpiry(expiresAt) : undefined
29
+ if (ttl != null) { opts.ex = ttl }
30
+ await client.set(key(namespace, id), JSON.stringify(data), opts)
31
+ }
32
+
33
+ async function load<T>(namespace: string, id: string): Promise<T | undefined> {
34
+ const raw = await client.get(key(namespace, id))
35
+ if (raw == null) { return undefined }
36
+ return (typeof raw === 'string' ? JSON.parse(raw) : raw) as T
37
+ }
38
+
39
+ async function remove(namespace: string, id: string) {
40
+ await client.del(key(namespace, id))
41
+ }
42
+
43
+ return {
44
+ async saveAuthCode(code, data) { await save('auth-code', code, data, data.expiresAt) },
45
+ async getAuthCode(code) { return load('auth-code', code) },
46
+ async deleteAuthCode(code) { await remove('auth-code', code) },
47
+
48
+ async savePendingAuth(state, data) { await save('pending-auth', state, data, data.expiresAt) },
49
+ async getPendingAuth(state) { return load('pending-auth', state) },
50
+ async deletePendingAuth(state) { await remove('pending-auth', state) },
51
+
52
+ async savePkceVerifier(state, verifier) {
53
+ const expiresAt = Date.now() / 1000 + 600
54
+ await save('pkce-verifier', state, { verifier, expiresAt }, expiresAt)
55
+ },
56
+ async getPkceVerifier(state) {
57
+ const item = await load<{ verifier: string; expiresAt: number }>('pkce-verifier', state)
58
+ return item?.verifier
59
+ },
60
+ async deletePkceVerifier(state) { await remove('pkce-verifier', state) },
61
+
62
+ async saveClient(clientId, data) { await save('client', clientId, data) },
63
+ async getClient(clientId) { return load('client', clientId) },
64
+
65
+ async saveRefreshToken(token, data) { await save('refresh-token', token, data, data.expiresAt) },
66
+ async getRefreshToken(token) { return load('refresh-token', token) },
67
+ async deleteRefreshToken(token) { await remove('refresh-token', token) }
68
+ }
69
+ }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { OAuthProvider } from './provider/types.js'
2
+
3
+ export interface AuthInfo {
4
+ token: string
5
+ clientId?: string
6
+ scopes?: string[]
7
+ expiresAt?: number
8
+ [key: string]: unknown
9
+ }
10
+
11
+ export type VerifyToken = (token: string) => Promise<AuthInfo | undefined>
12
+
13
+ export interface AuthConfig {
14
+ verifyToken: VerifyToken
15
+ required?: boolean
16
+ resourceUrl?: string
17
+ authorizationServers?: string[]
18
+ requiredScopes?: string[]
19
+ provider?: OAuthProvider
20
+ callbackPath?: string
21
+ }
@@ -0,0 +1,87 @@
1
+ import { AuthError, insufficientScope, invalidToken } from './errors.js'
2
+ import { extractBearerToken, buildWWWAuthenticate } from './extract.js'
3
+ import { AuthConfig, AuthInfo } from './types.js'
4
+
5
+ export interface ValidateResult {
6
+ auth?: AuthInfo
7
+ error?: {
8
+ statusCode: number
9
+ headers: Record<string, string>
10
+ body: { error: string; error_description: string }
11
+ }
12
+ }
13
+
14
+ export async function validateToken(
15
+ authorizationHeader: string | null | undefined,
16
+ config: AuthConfig
17
+ ): Promise<ValidateResult> {
18
+ const required = config.required ?? true
19
+ const resourceMetadataUrl = config.resourceUrl
20
+ ? `${config.resourceUrl}/.well-known/oauth-protected-resource`
21
+ : undefined
22
+
23
+ const token = extractBearerToken(authorizationHeader)
24
+
25
+ if (!token) {
26
+ if (!required) { return {} }
27
+ return buildChallengeResult(resourceMetadataUrl)
28
+ }
29
+
30
+ let authInfo: AuthInfo | undefined
31
+ try {
32
+ authInfo = await config.verifyToken(token)
33
+ } catch (_e) {
34
+ const err = invalidToken()
35
+ return buildErrorResult(err, resourceMetadataUrl)
36
+ }
37
+
38
+ if (!authInfo) {
39
+ const err = invalidToken()
40
+ return buildErrorResult(err, resourceMetadataUrl)
41
+ }
42
+
43
+ if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) {
44
+ const err = invalidToken('Token has expired')
45
+ return buildErrorResult(err, resourceMetadataUrl)
46
+ }
47
+
48
+ if (config.requiredScopes?.length) {
49
+ const scopes = authInfo.scopes ?? []
50
+ const hasAll = config.requiredScopes.every((s) => scopes.includes(s))
51
+ if (!hasAll) {
52
+ const err = insufficientScope()
53
+ return buildErrorResult(err, resourceMetadataUrl)
54
+ }
55
+ }
56
+
57
+ return { auth: authInfo }
58
+ }
59
+
60
+ function buildChallengeResult(resourceMetadataUrl?: string): ValidateResult {
61
+ return {
62
+ error: {
63
+ statusCode: 401,
64
+ headers: {
65
+ 'WWW-Authenticate': buildWWWAuthenticate(undefined, undefined, resourceMetadataUrl),
66
+ 'Content-Type': 'application/json'
67
+ },
68
+ body: { error: 'missing_token', error_description: 'No authorization provided' }
69
+ }
70
+ }
71
+ }
72
+
73
+ function buildErrorResult(
74
+ err: AuthError,
75
+ resourceMetadataUrl?: string
76
+ ): ValidateResult {
77
+ return {
78
+ error: {
79
+ statusCode: err.statusCode,
80
+ headers: {
81
+ 'WWW-Authenticate': buildWWWAuthenticate(err.code, err.message, resourceMetadataUrl),
82
+ 'Content-Type': 'application/json'
83
+ },
84
+ body: { error: err.code, error_description: err.message }
85
+ }
86
+ }
87
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "build",
8
+ "rootDir": ".",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "allowSyntheticDefaultImports": true,
14
+ "resolveJsonModule": true,
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "sourceMap": true
18
+ },
19
+ "include": ["src"],
20
+ "exclude": ["node_modules", "build"]
21
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ outDir: 'build',
6
+ format: ['esm'],
7
+ target: 'node18',
8
+ dts: true,
9
+ clean: true,
10
+ sourcemap: true,
11
+ splitting: false,
12
+ external: [/^[^./]/]
13
+ })