@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.
- package/.turbo/turbo-build.log +18 -0
- package/.turbo/turbo-check.log +13 -0
- package/.turbo/turbo-lint.log +5 -0
- package/.turbo/turbo-prepack.log +26 -0
- package/README.md +245 -0
- package/build/index.d.ts +173 -0
- package/build/index.js +783 -0
- package/build/index.js.map +1 -0
- package/eslint.config.mjs +99 -0
- package/package.json +34 -0
- package/src/errors.ts +22 -0
- package/src/extract.ts +23 -0
- package/src/index.ts +9 -0
- package/src/metadata.ts +17 -0
- package/src/provider/google.ts +41 -0
- package/src/provider/index.ts +6 -0
- package/src/provider/proxy.ts +444 -0
- package/src/provider/store.ts +60 -0
- package/src/provider/types.ts +23 -0
- package/src/provider/uri.ts +13 -0
- package/src/store/json-store.ts +97 -0
- package/src/store/memory-store.ts +50 -0
- package/src/store/redis-store.ts +69 -0
- package/src/types.ts +21 -0
- package/src/validate.ts +87 -0
- package/tsconfig.json +21 -0
- package/tsup.config.ts +13 -0
|
@@ -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
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -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