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