@riligar/auth-elysia 1.6.2 → 1.6.4

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/src/index.js CHANGED
@@ -1,21 +1,10 @@
1
1
  /**
2
2
  * @module @riligar/auth-elysia
3
- * @description Auth SDK for ElysiaJS with JWT and JWKS
4
- * @version See package.json
5
- * @since 1.0.0
6
- * @copyright 2024-2026 Riligar
7
- *
8
- * @license MIT
9
- * @author Riligar
10
- * @see https://github.com/riligar-solutions/auth
11
- * @see https://www.npmjs.com/package/@riligar/auth-elysia
3
+ * @description Auth SDK for ElysiaJS
12
4
  */
13
5
 
14
6
  import { Elysia } from 'elysia'
15
7
 
16
- /**
17
- * Configuração padrão do plugin
18
- */
19
8
  const DEFAULT_CONFIG = {
20
9
  prefix: '/auth',
21
10
  secretKey: process.env.AUTH_SECRET_KEY || 'your-secret-key',
@@ -25,396 +14,160 @@ const DEFAULT_CONFIG = {
25
14
  httpOnly: true,
26
15
  secure: process.env.NODE_ENV === 'production',
27
16
  sameSite: 'lax',
28
- maxAge: 604800, // 7 dias (igual ao JWT)
17
+ maxAge: 604800,
29
18
  },
30
19
  excludePaths: ['/auth/login', '/auth/register', '/auth/session'],
31
20
  onUnauthorized: set => {
32
21
  set.status = 401
33
- return { error: 'Unauthorized', message: 'Token inválido ou expirado' }
22
+ set.headers = { ...set.headers, 'Content-Type': 'application/json' }
23
+ return {
24
+ success: false,
25
+ error: {
26
+ code: 'UNAUTHORIZED',
27
+ message: 'Sessão inválida ou expirada. Por favor, faça login novamente.',
28
+ },
29
+ }
34
30
  },
35
31
  }
36
32
 
37
- /**
38
- * Cliente Auth com verificação JWT local + JWKS
39
- */
40
33
  class RiLiGarAuthClient {
41
34
  constructor(baseUrl, secretKey) {
42
35
  this.baseUrl = baseUrl
43
36
  this.secretKey = secretKey
44
- // 🚀 Cache para JWKS (chaves públicas)
45
37
  this.jwksCache = null
46
38
  this.jwksCacheExpiry = 0
47
- this.jwksCacheTTL = 3600000 // 1 hora em ms
48
39
  }
49
40
 
50
- // 🔑 Buscar e cachear JWKS
51
41
  async getJWKS() {
52
42
  const now = Date.now()
53
-
54
- // Retorna cache se ainda válido
55
- if (this.jwksCache && now < this.jwksCacheExpiry) {
56
- return this.jwksCache
57
- }
58
-
43
+ if (this.jwksCache && now < this.jwksCacheExpiry) return this.jwksCache
59
44
  try {
60
45
  const response = await fetch(`${this.baseUrl}/.well-known/jwks.json`)
61
46
  if (response.ok) {
62
47
  this.jwksCache = await response.json()
63
- this.jwksCacheExpiry = now + this.jwksCacheTTL
48
+ this.jwksCacheExpiry = now + 3600000
64
49
  return this.jwksCache
65
50
  }
66
- } catch (error) {
67
- console.warn('JWKS fetch failed, falling back to remote verification:', error.message)
68
- }
69
-
51
+ } catch (e) {}
70
52
  return null
71
53
  }
72
54
 
73
- // ⚡ Verificação JWT local (usando crypto nativo do Bun)
74
55
  async verifyJWTLocal(token) {
75
56
  try {
76
57
  const jwks = await this.getJWKS()
77
- if (!jwks?.keys?.length) return null
78
-
79
- // Decode JWT header para pegar kid e alg
80
- const [headerB64] = token.split('.')
81
- const header = JSON.parse(atob(headerB64))
82
-
83
- // Encontrar a chave correspondente
58
+ if (!jwks) return null
59
+ const [h, p, s] = token.split('.')
60
+ const header = JSON.parse(atob(h))
84
61
  const jwk = jwks.keys.find(k => k.kid === header.kid || k.alg === header.alg)
85
62
  if (!jwk) return null
86
-
87
- // Preparar dados para verificação
88
- const [headerB64Url, payloadB64Url, signatureB64Url] = token.split('.')
89
- const data = new TextEncoder().encode(`${headerB64Url}.${payloadB64Url}`)
90
-
91
- const signature = new Uint8Array(Array.from(atob(signatureB64Url.replace(/-/g, '+').replace(/_/g, '/'))).map(c => c.charCodeAt(0)))
92
-
93
- let key
94
- let algorithm
95
-
96
- // Suporte para diferentes algoritmos
97
- if (header.alg === 'HS256' || jwk.kty === 'oct') {
98
- // HMAC com secret key
99
- const secret = new TextEncoder().encode(this.secretKey)
100
- key = await crypto.subtle.importKey('raw', secret, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])
101
- algorithm = 'HMAC'
102
- } else if (header.alg === 'RS256' || jwk.kty === 'RSA') {
103
- // RSA com chave pública JWKS
104
- if (!jwk.n || !jwk.e) return null
105
-
106
- // Converter base64url para ArrayBuffer
107
- const nBuffer = this.base64urlToArrayBuffer(jwk.n)
108
- const eBuffer = this.base64urlToArrayBuffer(jwk.e)
109
-
110
- key = await crypto.subtle.importKey(
111
- 'jwk',
112
- {
113
- kty: 'RSA',
114
- n: jwk.n,
115
- e: jwk.e,
116
- alg: 'RS256',
117
- use: 'sig',
118
- },
119
- { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
120
- false,
121
- ['verify']
122
- )
123
- algorithm = 'RSASSA-PKCS1-v1_5'
63
+ const data = new TextEncoder().encode(`${h}.${p}`)
64
+ const sig = Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
65
+ let key, algo
66
+ if (header.alg === 'HS256') {
67
+ key = await crypto.subtle.importKey('raw', new TextEncoder().encode(this.secretKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])
68
+ algo = 'HMAC'
124
69
  } else {
125
- console.warn('Algoritmo JWT não suportado:', header.alg)
126
- return null
127
- }
128
-
129
- // Verificar assinatura
130
- const isValid = await crypto.subtle.verify(algorithm, key, signature, data)
131
- if (!isValid) return null
132
-
133
- // Decode payload
134
- const payload = JSON.parse(atob(payloadB64Url))
135
-
136
- // Verificar expiração
137
- if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
138
- return null
139
- }
140
-
141
- // Map JWT standard claims to user object format
142
- // JWT uses 'sub' for subject (user ID), but clients expect 'id'
143
- return {
144
- id: payload.sub,
145
- ...payload,
70
+ key = await crypto.subtle.importKey('jwk', { kty: 'RSA', n: jwk.n, e: jwk.e, alg: 'RS256', use: 'sig' }, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify'])
71
+ algo = 'RSASSA-PKCS1-v1_5'
146
72
  }
147
- } catch (error) {
148
- console.warn('JWT local verification failed:', error.message)
73
+ if (!(await crypto.subtle.verify(algo, key, sig, data))) return null
74
+ const payload = JSON.parse(atob(p))
75
+ if (payload.exp && payload.exp < Date.now() / 1000) return null
76
+ return { id: payload.sub, ...payload }
77
+ } catch (e) {
149
78
  return null
150
79
  }
151
80
  }
152
81
 
153
- // Utilitário para converter base64url para ArrayBuffer
154
- base64urlToArrayBuffer(base64url) {
155
- const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/')
156
- const padding = '='.repeat((4 - (base64.length % 4)) % 4)
157
- const base64WithPadding = base64 + padding
158
-
159
- const binaryString = atob(base64WithPadding)
160
- const bytes = new Uint8Array(binaryString.length)
161
- for (let i = 0; i < binaryString.length; i++) {
162
- bytes[i] = binaryString.charCodeAt(i)
163
- }
164
- return bytes.buffer
165
- }
166
-
167
- // ⚡ Verificar sessão OTIMIZADA (local + fallback remoto)
168
- async verifySession(sessionToken) {
169
- // 🚀 Primeira tentativa: Verificação local com JWKS
170
- const localResult = await this.verifyJWTLocal(sessionToken)
171
- if (localResult) {
172
- return {
173
- user: localResult,
174
- verified_locally: true,
175
- cached: true,
176
- }
177
- }
178
-
179
- // 🔄 Fallback: Verificação remota
82
+ async verifySession(token) {
83
+ const local = await this.verifyJWTLocal(token)
84
+ if (local) return { user: local }
180
85
  try {
181
- const response = await fetch(`${this.baseUrl}/auth/session`, {
182
- headers: {
183
- Authorization: `Bearer ${sessionToken}`,
184
- 'X-API-Key': this.secretKey,
185
- },
86
+ const res = await fetch(`${this.baseUrl}/auth/session`, {
87
+ headers: { Authorization: `Bearer ${token}`, 'X-API-Key': this.secretKey },
186
88
  })
187
-
188
- if (response.ok) {
189
- const result = await response.json()
190
- return {
191
- ...result,
192
- verified_locally: false,
193
- cached: false,
194
- }
195
- }
196
- } catch (error) {
197
- console.warn('Remote session verification failed:', error.message)
198
- }
199
-
89
+ if (res.ok) return await res.json()
90
+ } catch (e) {}
200
91
  return null
201
92
  }
202
93
  }
203
94
 
204
- /**
205
- * Utilitário para fazer requisições HTTP
206
- */
207
95
  async function fetchAuth(url, options = {}) {
208
- try {
209
- const response = await fetch(url, {
210
- headers: {
211
- 'Content-Type': 'application/json',
212
- ...options.headers,
213
- },
214
- ...options,
215
- })
216
-
217
- if (!response.ok) {
218
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
219
- }
220
-
221
- return await response.json()
222
- } catch (error) {
223
- console.error('Auth fetch error:', error)
224
- throw error
225
- }
96
+ const res = await fetch(url, {
97
+ headers: { 'Content-Type': 'application/json', ...options.headers },
98
+ ...options,
99
+ })
100
+ if (!res.ok) throw new Error(res.status)
101
+ return await res.json()
226
102
  }
227
103
 
228
- /**
229
- * Plugin principal de autenticação
230
- */
231
104
  export function authPlugin(userConfig = {}) {
232
105
  const config = { ...DEFAULT_CONFIG, ...userConfig }
233
-
234
- // Instanciar cliente Auth
235
106
  const authClient = new RiLiGarAuthClient(config.apiUrl, config.secretKey)
236
107
 
237
- return app => {
238
- app.derive(async ({ request, cookie }) => {
239
- const path = new URL(request.url).pathname
240
-
241
- // Buscar token no cookie ou header Authorization
242
- let token = cookie[config.cookieName]?.value
243
-
244
- if (!token) {
245
- const authHeader = request.headers.get('authorization')
246
- if (authHeader?.startsWith('Bearer ')) {
247
- token = authHeader.substring(7)
248
- }
249
- }
250
-
251
- if (!token) {
252
- return { user: null, authMeta: null }
253
- }
254
-
255
- try {
256
- // ⚡ Verificação otimizada com JWKS local + fallback remoto
257
- const userSession = await authClient.verifySession(token)
258
-
259
- if (!userSession) {
260
- return { user: null, authMeta: null }
261
- }
262
-
263
- return {
264
- user: userSession.user,
265
- authMeta: {
266
- verified_locally: userSession.verified_locally,
267
- cached: userSession.cached,
268
- },
108
+ return app =>
109
+ app
110
+ .derive({ as: 'global' }, async ({ request, cookie }) => {
111
+ const token = cookie[config.cookieName]?.value || request.headers.get('authorization')?.split(' ')[1]
112
+ if (!token) return { user: null }
113
+ const session = await authClient.verifySession(token)
114
+ return { user: session?.user || null }
115
+ })
116
+ .onBeforeHandle({ as: 'global' }, ({ user, set, request }) => {
117
+ const path = new URL(request.url).pathname
118
+ if (config.excludePaths.some(p => path.startsWith(p))) return
119
+ if (!user) return config.onUnauthorized(set)
120
+ })
121
+ .error(({ error, set }) => {
122
+ const errorStr = String(error)
123
+ if (errorStr.includes('user.id') || errorStr.includes('of null') || errorStr.includes('of undefined')) {
124
+ set.status = 401
125
+ set.headers['Content-Type'] = 'application/json'
126
+ return {
127
+ success: false,
128
+ error: {
129
+ code: 'UNAUTHORIZED',
130
+ message: 'Sessão inválida ou expirada. Por favor, faça login novamente.',
131
+ },
132
+ }
269
133
  }
270
- } catch (error) {
271
- console.warn('Auth verification error:', error.message)
272
- return { user: null, authMeta: null }
273
- }
274
- }).onBeforeHandle(({ user, set, request }) => {
275
- const path = new URL(request.url).pathname
276
-
277
- // Verificar se a rota deve ser excluída da autenticação
278
- const isExcluded = config.excludePaths.some(excluded => path.startsWith(excluded))
279
- if (isExcluded) return
280
-
281
- // Bloquear se não houver usuário
282
- if (!user) {
283
- return config.onUnauthorized(set)
284
- }
285
- })
286
-
287
- return app.group(config.prefix, app =>
288
- app
289
- // Rota de login
290
- .post('/login', async ({ body, set, cookie }) => {
291
- try {
292
- const { email, password } = body
293
-
294
- if (!email || !password) {
295
- set.status = 400
296
- return { error: 'Email e senha são obrigatórios' }
297
- }
298
-
299
- const response = await fetchAuth(`${config.apiUrl}/auth/sign-in/email`, {
300
- method: 'POST',
301
- body: JSON.stringify({ email, password }),
302
- headers: {
303
- 'X-API-Key': config.secretKey,
304
- },
305
- })
306
-
307
- if (response.token) {
308
- cookie[config.cookieName].set({
309
- value: response.token,
310
- ...config.cookieOptions,
134
+ })
135
+ .group(config.prefix, app =>
136
+ app
137
+ .post('/login', async ({ body, set, cookie }) => {
138
+ try {
139
+ const res = await fetchAuth(`${config.apiUrl}/auth/sign-in/email`, {
140
+ method: 'POST',
141
+ body: JSON.stringify(body),
142
+ headers: { 'X-API-Key': config.secretKey },
311
143
  })
144
+ cookie[config.cookieName].set({ value: res.token, ...config.cookieOptions })
145
+ return { message: 'OK', user: res.user, token: res.token }
146
+ } catch (e) {
147
+ set.status = 401
148
+ return 'Falha no login.'
312
149
  }
313
-
314
- return {
315
- message: 'Login realizado com sucesso!',
316
- user: response.user,
317
- token: response.token,
318
- session: response.session,
319
- }
320
- } catch (error) {
321
- set.status = 401
322
- return { error: 'Login failed', message: error.message }
323
- }
324
- })
325
-
326
- // Rota de registro
327
- .post('/register', async ({ body, set }) => {
328
- try {
329
- const { email, password, name, organizationId } = body
330
-
331
- if (!email || !password || !name) {
332
- set.status = 400
333
- return { error: 'Email, senha e nome são obrigatórios' }
334
- }
335
-
336
- const response = await fetchAuth(`${config.apiUrl}/auth/sign-up/email`, {
337
- method: 'POST',
338
- body: JSON.stringify({
339
- email,
340
- password,
341
- name,
342
- ...(organizationId && { organizationId }),
343
- }),
344
- headers: {
345
- 'X-API-Key': config.secretKey,
346
- },
347
- })
348
-
349
- return {
350
- message: 'Usuário registrado com sucesso!',
351
- user: {
352
- id: response.user?.id,
353
- email: response.user?.email,
354
- name: response.user?.name,
355
- },
356
- }
357
- } catch (error) {
358
- set.status = 400
359
- return { error: 'Registration failed', message: error.message }
360
- }
361
- })
362
-
363
- // Rota de logout
364
- .post('/logout', async ({ cookie, set, headers }) => {
365
- try {
366
- const token = headers.authorization?.replace('Bearer ', '') || cookie[config.cookieName]?.value
367
-
368
- if (token) {
369
- // Fazer logout no servidor de auth
370
- await fetchAuth(`${config.apiUrl}/auth/sign-out`, {
150
+ })
151
+ .post('/register', async ({ body, set }) => {
152
+ try {
153
+ const res = await fetchAuth(`${config.apiUrl}/auth/sign-up/email`, {
371
154
  method: 'POST',
372
- headers: {
373
- Authorization: `Bearer ${token}`,
374
- 'X-API-Key': config.secretKey,
375
- },
376
- }).catch(() => {
377
- // Ignorar erros do logout remoto
155
+ body: JSON.stringify(body),
156
+ headers: { 'X-API-Key': config.secretKey },
378
157
  })
158
+ return { message: 'OK', user: res.user }
159
+ } catch (e) {
160
+ set.status = 400
161
+ return 'Falha no registro.'
379
162
  }
380
-
381
- // Remover cookie local
163
+ })
164
+ .post('/logout', async ({ cookie }) => {
382
165
  cookie[config.cookieName].remove()
383
-
384
- return { message: 'Logout realizado com sucesso!' }
385
- } catch (error) {
386
- set.status = 400
387
- return { error: 'Erro ao fazer logout', message: error.message }
388
- }
389
- })
390
-
391
- // Verificar sessão (substitui o /refresh)
392
- .get('/session', ({ user, authMeta, set }) => {
393
- if (!user) {
394
- set.status = 401
395
- return { error: 'Not authenticated' }
396
- }
397
- return {
398
- user,
399
- meta: authMeta,
400
- verified_at: new Date().toISOString(),
401
- }
402
- })
403
-
404
- // Status do usuário atual
405
- .get('/me', ({ user, set }) => {
406
- if (!user) {
407
- set.status = 401
408
- return { error: 'Not authenticated' }
409
- }
410
- return { user }
411
- })
412
- )
413
- }
166
+ return 'Sessão encerrada.'
167
+ })
168
+ .get('/me', ({ user, set }) => user || config.onUnauthorized(set))
169
+ )
414
170
  }
415
171
 
416
- // Exportar configuração padrão e utilitários
417
172
  export { DEFAULT_CONFIG, fetchAuth, RiLiGarAuthClient }
418
-
419
- // Export default para compatibilidade
420
173
  export default authPlugin