@purecore/one-jwt-4-all 1.2.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,86 @@
1
+ import * as crypto from 'node:crypto';
2
+ import { SignJWT, jwtVerify, generateKeyPair } from './eddsa_jwt'; // Importando nossa lib
3
+
4
+ // --- Cenário: Setup Inicial ---
5
+ const { privateKey: AUTH_SERVER_PRIVATE_KEY, publicKey: API_PUBLIC_KEY } = generateKeyPair();
6
+
7
+ const ISSUER = 'https://meu-auth-server.com';
8
+
9
+ // Audiences do nosso sistema
10
+ const AUDIENCE_FINANCEIRO = 'https://api.financeira.com';
11
+ const MCP_ECOSYSTEM_AUD = 'urn:mcp:ecosystem'; // Audiência que representa TODOS os seus MCPs
12
+
13
+ // URLs específicas de alguns servidores MCP
14
+ const MCP_SERVER_ALPHA = 'https://mcp-alpha.internal';
15
+ const MCP_SERVER_BETA = 'https://mcp-beta.internal';
16
+ const MCP_SERVER_GAMA = 'https://mcp-gama.internal';
17
+ const MCP_SERVER_DELTA = 'https://mcp-delta.internal'; // Este ficará de fora do grupo
18
+
19
+ // --- 1. O Authorization Server (Emissor) ---
20
+ // Agora aceita 'audience' dinâmico (pode ser string única ou array)
21
+ async function generateAccessToken(userId: string, scopes: string[], audience: string | string[]) {
22
+
23
+ const jwt = await new SignJWT({
24
+ sub: userId,
25
+ scope: scopes.join(' '),
26
+ role: 'user'
27
+ })
28
+ .setProtectedHeader({ alg: 'EdDSA' })
29
+ .setIssuedAt()
30
+ .setIssuer(ISSUER)
31
+ .setAudience(audience) // Aceita string ou array de strings
32
+ .setExpirationTime('1h')
33
+ .setJti(crypto.randomUUID())
34
+ .sign(AUTH_SERVER_PRIVATE_KEY);
35
+
36
+ return jwt;
37
+ }
38
+
39
+ // --- 2. O Resource Server (Simulação de um MCP Server) ---
40
+ // Cada servidor sabe QUEM ELE É (sua própria URL ou ID de audiência)
41
+ async function protectMCPServer(serverName: string, myAudienceId: string, tokenRecebido: string) {
42
+ try {
43
+ // Ao verificar, o servidor passa 'myAudienceId'.
44
+ // A validação passa se 'myAudienceId' estiver presente na lista 'aud' do token.
45
+ const { payload } = await jwtVerify(tokenRecebido, API_PUBLIC_KEY, {
46
+ issuer: ISSUER,
47
+ audience: myAudienceId
48
+ });
49
+
50
+ console.log(`✅ [${serverName}] Acesso permitido! (Token aud: ${JSON.stringify(payload.aud)})`);
51
+ return true;
52
+
53
+ } catch (error) {
54
+ console.error(`❌ [${serverName}] Acesso negado: ${(error as Error).message}`);
55
+ return false;
56
+ }
57
+ }
58
+
59
+ // --- Simulação dos Cenários ---
60
+ (async () => {
61
+ console.log("--- Cenário A: Token para um Ecossistema Inteiro ---\n");
62
+ // Útil se você tem muitos microserviços e todos confiam no mesmo token 'geral'
63
+
64
+ const tokenEcosystem = await generateAccessToken('user-A', ['mcp:read'], MCP_ECOSYSTEM_AUD);
65
+
66
+ // O servidor Alpha aceita tokens do ecossistema
67
+ // Nota: O servidor precisa estar configurado para aceitar 'urn:mcp:ecosystem' como audiência válida
68
+ await protectMCPServer('MCP Alpha (Modo Ecossistema)', MCP_ECOSYSTEM_AUD, tokenEcosystem);
69
+
70
+
71
+ console.log("\n--- Cenário B: Token Restrito a um Grupo Específico (3 Servers) ---\n");
72
+ // O usuário quer acessar Alpha, Beta e Gama, mas NÃO o Delta.
73
+
74
+ const targetGroup = [MCP_SERVER_ALPHA, MCP_SERVER_BETA, MCP_SERVER_GAMA];
75
+ const tokenGroup = await generateAccessToken('user-B', ['mcp:write'], targetGroup);
76
+
77
+ // 1. MCP Alpha tenta validar (Ele espera ver sua URL no token)
78
+ await protectMCPServer('MCP Alpha', MCP_SERVER_ALPHA, tokenGroup);
79
+
80
+ // 2. MCP Gama tenta validar
81
+ await protectMCPServer('MCP Gama', MCP_SERVER_GAMA, tokenGroup);
82
+
83
+ // 3. MCP Delta tenta validar (Ele NÃO está na lista do token)
84
+ await protectMCPServer('MCP Delta', MCP_SERVER_DELTA, tokenGroup);
85
+
86
+ })();
@@ -0,0 +1,81 @@
1
+ import { SignJWT, jwtVerify, generateKeyPair } from './index'; // Importando nossa lib
2
+
3
+ // --- Cenário: Setup Inicial ---
4
+ // Em um cenário real, estas chaves seriam geradas uma vez e salvas em variáveis de ambiente ou KMS.
5
+ // O Auth Server tem a PRIVADA. As APIs têm a PÚBLICA.
6
+ const { privateKey: AUTH_SERVER_PRIVATE_KEY, publicKey: API_PUBLIC_KEY } = generateKeyPair();
7
+
8
+ // Identificadores do nosso sistema
9
+ const ISSUER = 'https://meu-auth-server.com'; // Quem gerou o token
10
+ const AUDIENCE_FINANCEIRO = 'https://api.financeira.com'; // API de Destino
11
+
12
+ // --- 1. O Authorization Server (Emissor) ---
13
+ // Função que gera um Access Token quando o usuário loga com sucesso
14
+ async function generateAccessToken(userId: string, scopes: string[]) {
15
+
16
+ // No OAuth 2.1, o token geralmente tem validade curta (ex: 1 hora)
17
+ const jwt = await new SignJWT({
18
+ // Claims padrão do OAuth/OIDC
19
+ sub: userId, // Subject: Quem é o usuário (ID)
20
+ scope: scopes.join(' '), // Scopes: O que ele pode fazer
21
+ // Claims personalizadas
22
+ role: 'admin'
23
+ })
24
+ .setProtectedHeader({ alg: 'EdDSA' })
25
+ .setIssuedAt()
26
+ .setIssuer(ISSUER)
27
+ .setAudience(AUDIENCE_FINANCEIRO) // DISS: "Este token é SÓ para o Financeiro"
28
+ .setExpirationTime('1h') // Token de acesso de curta duração
29
+ .setJti(crypto.randomUUID()) // ID único do token (para revogação/blacklist se necessário)
30
+ .sign(AUTH_SERVER_PRIVATE_KEY);
31
+
32
+ return jwt;
33
+ }
34
+
35
+ // --- 2. O Resource Server (API Financeira) ---
36
+ // Middleware que protege a rota da API
37
+ async function protectFinanceRoute(tokenRecebido: string) {
38
+ try {
39
+ const { payload } = await jwtVerify(tokenRecebido, API_PUBLIC_KEY, {
40
+ issuer: ISSUER, // Valida se veio do nosso Auth Server confiável
41
+ audience: AUDIENCE_FINANCEIRO // Valida se o token foi feito PARA NÓS
42
+ });
43
+
44
+ // Se passou daqui, a assinatura é válida e a audiência está correta.
45
+ console.log(`✅ Acesso permitido ao usuário ${payload.sub}`);
46
+ console.log(`Escopos permitidos: ${payload.scope}`);
47
+
48
+ return true;
49
+
50
+ } catch (error) {
51
+ console.error(`❌ Acesso negado: ${(error as Error).message}`);
52
+ return false;
53
+ }
54
+ }
55
+
56
+ // --- Simulação do Fluxo ---
57
+ (async () => {
58
+ console.log("--- Iniciando Fluxo OAuth 2.1 com EdDSA ---\n");
59
+
60
+ // 1. Usuário loga e ganha token
61
+ console.log("1. Gerando Access Token no Auth Server...");
62
+ const token = await generateAccessToken('user-123', ['read:invoices', 'write:payments']);
63
+ console.log("Token gerado:\n", token);
64
+
65
+ // 2. Usuário tenta acessar a API Financeira com o token
66
+ console.log("\n2. Tentando acessar API Financeira...");
67
+ await protectFinanceRoute(token);
68
+
69
+ // 3. Teste de Audiência Inválida (Hacker tenta usar token em outra API)
70
+ console.log("\n3. Teste de Segurança (Audiência Errada)...");
71
+ // Vamos simular que a verificação espera 'api-de-chat' mas o token é para 'financeiro'
72
+ try {
73
+ await jwtVerify(token, API_PUBLIC_KEY, {
74
+ issuer: ISSUER,
75
+ audience: 'https://api.chat.com' // <--- Audiência diferente da que está no token
76
+ });
77
+ } catch (e) {
78
+ console.log(`Bloqueio esperado: ${(e as Error).message}`);
79
+ }
80
+
81
+ })();
package/src/index.ts ADDED
@@ -0,0 +1,286 @@
1
+ import * as crypto from 'node:crypto';
2
+
3
+ // --- Interfaces & Types (Compatíveis com 'jose') ---
4
+
5
+ export interface JWTPayload {
6
+ iss?: string;
7
+ sub?: string;
8
+ aud?: string | string[];
9
+ exp?: number;
10
+ nbf?: number;
11
+ iat?: number;
12
+ jti?: string;
13
+ [key: string]: any;
14
+ }
15
+
16
+ export interface JWTHeaderParameters {
17
+ alg?: string;
18
+ typ?: string;
19
+ kid?: string;
20
+ [key: string]: any;
21
+ }
22
+
23
+ export interface JWTVerifyResult {
24
+ payload: JWTPayload;
25
+ protectedHeader: JWTHeaderParameters;
26
+ }
27
+
28
+ export interface JWTVerifyOptions {
29
+ issuer?: string | string[];
30
+ audience?: string | string[];
31
+ algorithms?: string[];
32
+ currentDate?: Date; // Para mockar tempo em testes
33
+ maxTokenAge?: string | number; // Ex: '2h' ou segundos
34
+ }
35
+
36
+ // --- Utilitários Internos ---
37
+
38
+ const Encoder = new TextEncoder();
39
+ const Decoder = new TextDecoder();
40
+
41
+ /**
42
+ * Converte strings de tempo (ex: "2h", "1d", "30m") para segundos.
43
+ * Se for número, assume que já são segundos.
44
+ */
45
+ function parseTime(time: string | number | undefined): number {
46
+ if (typeof time === 'number') return time;
47
+ if (!time) return 0;
48
+
49
+ const regex = /^(\d+)([smhdwy])$/;
50
+ const match = time.match(regex);
51
+
52
+ if (!match) throw new Error(`Formato de tempo inválido: ${time}`);
53
+
54
+ const value = parseInt(match[1], 10);
55
+ const unit = match[2];
56
+
57
+ switch (unit) {
58
+ case 's': return value;
59
+ case 'm': return value * 60;
60
+ case 'h': return value * 60 * 60;
61
+ case 'd': return value * 24 * 60 * 60;
62
+ case 'w': return value * 7 * 24 * 60 * 60;
63
+ case 'y': return value * 365.25 * 24 * 60 * 60;
64
+ default: return value;
65
+ }
66
+ }
67
+
68
+ function base64UrlEncode(input: Uint8Array | string | object): string {
69
+ let buffer: Buffer;
70
+ if (typeof input === 'string') {
71
+ buffer = Buffer.from(input, 'utf-8');
72
+ } else if (Buffer.isBuffer(input)) {
73
+ buffer = input;
74
+ } else if (input instanceof Uint8Array) {
75
+ buffer = Buffer.from(input);
76
+ } else {
77
+ buffer = Buffer.from(JSON.stringify(input), 'utf-8');
78
+ }
79
+
80
+ return buffer.toString('base64url'); // Node.js moderno suporta 'base64url' nativamente
81
+ }
82
+
83
+ function base64UrlDecode(str: string): string {
84
+ return Buffer.from(str, 'base64url').toString('utf-8');
85
+ }
86
+
87
+ // --- Classe SignJWT (Builder Pattern) ---
88
+
89
+ export class SignJWT {
90
+ private _payload: JWTPayload;
91
+ private _protectedHeader: JWTHeaderParameters = { alg: 'EdDSA', typ: 'JWT' };
92
+
93
+ constructor(payload: JWTPayload) {
94
+ this._payload = { ...payload };
95
+ }
96
+
97
+ setProtectedHeader(protectedHeader: JWTHeaderParameters): this {
98
+ this._protectedHeader = { ...this._protectedHeader, ...protectedHeader };
99
+ return this;
100
+ }
101
+
102
+ setIssuer(issuer: string): this {
103
+ this._payload.iss = issuer;
104
+ return this;
105
+ }
106
+
107
+ setSubject(subject: string): this {
108
+ this._payload.sub = subject;
109
+ return this;
110
+ }
111
+
112
+ setAudience(audience: string | string[]): this {
113
+ this._payload.aud = audience;
114
+ return this;
115
+ }
116
+
117
+ setJti(jwtId: string): this {
118
+ this._payload.jti = jwtId;
119
+ return this;
120
+ }
121
+
122
+ setNotBefore(input: number | string): this {
123
+ const now = Math.floor(Date.now() / 1000);
124
+ this._payload.nbf = now + parseTime(input);
125
+ return this;
126
+ }
127
+
128
+ setIssuedAt(input?: number): this {
129
+ this._payload.iat = input ?? Math.floor(Date.now() / 1000);
130
+ return this;
131
+ }
132
+
133
+ setExpirationTime(input: number | string): this {
134
+ const now = this._payload.iat ?? Math.floor(Date.now() / 1000);
135
+ const offset = typeof input === 'number' ? input - now : parseTime(input);
136
+ // Nota: 'jose' geralmente trata string como duração relativa (ex: "2h" a partir de agora/iat)
137
+ // Se o input for string, somamos ao 'iat' ou 'now'.
138
+ // Se for number, assume-se timestamp absoluto na maioria das libs, mas 'jose' string é duração.
139
+ // Aqui simplifico: string = duração, number = timestamp absoluto.
140
+
141
+ if (typeof input === 'string') {
142
+ this._payload.exp = now + parseTime(input);
143
+ } else {
144
+ this._payload.exp = input;
145
+ }
146
+
147
+ return this;
148
+ }
149
+
150
+ async sign(privateKey: crypto.KeyObject | string): Promise<string> {
151
+ if (this._protectedHeader.alg !== 'EdDSA') {
152
+ throw new Error('Apenas o algoritmo EdDSA é suportado por esta implementação.');
153
+ }
154
+
155
+ const encodedHeader = base64UrlEncode(this._protectedHeader);
156
+ const encodedPayload = base64UrlEncode(this._payload);
157
+ const data = `${encodedHeader}.${encodedPayload}`;
158
+
159
+ const signature = crypto.sign(
160
+ null,
161
+ Buffer.from(data),
162
+ (typeof privateKey === 'string') ? crypto.createPrivateKey(privateKey) : privateKey
163
+ );
164
+
165
+ const encodedSignature = base64UrlEncode(signature);
166
+
167
+ return `${data}.${encodedSignature}`;
168
+ }
169
+ }
170
+
171
+ // --- Função jwtVerify (Funcional) ---
172
+
173
+ export async function jwtVerify(
174
+ jwt: string,
175
+ key: crypto.KeyObject | string,
176
+ options?: JWTVerifyOptions
177
+ ): Promise<JWTVerifyResult> {
178
+ const parts = jwt.split('.');
179
+ if (parts.length !== 3) {
180
+ throw new Error('JWT inválido: Formato deve ser header.payload.signature');
181
+ }
182
+
183
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
184
+ const data = `${encodedHeader}.${encodedPayload}`;
185
+
186
+ // 1. Validar Assinatura Criptográfica
187
+ const publicKey = (typeof key === 'string') ? crypto.createPublicKey(key) : key;
188
+
189
+ // No Node moderno, crypto.verify infere Ed25519 pela chave
190
+ const verified = crypto.verify(
191
+ null,
192
+ Buffer.from(data),
193
+ publicKey,
194
+ Buffer.from(encodedSignature, 'base64url')
195
+ );
196
+
197
+ if (!verified) {
198
+ throw new Error('Assinatura do JWT inválida.');
199
+ }
200
+
201
+ // 2. Parse do Conteúdo
202
+ const protectedHeader = JSON.parse(base64UrlDecode(encodedHeader)) as JWTHeaderParameters;
203
+ const payload = JSON.parse(base64UrlDecode(encodedPayload)) as JWTPayload;
204
+
205
+ // 3. Validações de Claims (exp, nbf, iss, aud)
206
+ const now = options?.currentDate
207
+ ? Math.floor(options.currentDate.getTime() / 1000)
208
+ : Math.floor(Date.now() / 1000);
209
+
210
+ // Clock tolerance padrão de 0s (pode ser adicionado se quiser)
211
+
212
+ if (payload.exp && now > payload.exp) {
213
+ throw new Error(`Token expirado (exp). Expirou em ${new Date(payload.exp * 1000).toISOString()}`);
214
+ }
215
+
216
+ if (payload.nbf && now < payload.nbf) {
217
+ throw new Error(`Token ainda não ativo (nbf). Válido a partir de ${new Date(payload.nbf * 1000).toISOString()}`);
218
+ }
219
+
220
+ if (options?.issuer) {
221
+ const issuers = Array.isArray(options.issuer) ? options.issuer : [options.issuer];
222
+ if (!payload.iss || !issuers.includes(payload.iss)) {
223
+ throw new Error(`Issuer inválido. Esperado: ${issuers.join(' ou ')}, Recebido: ${payload.iss}`);
224
+ }
225
+ }
226
+
227
+ if (options?.audience) {
228
+ const audiences = Array.isArray(options.audience) ? options.audience : [options.audience];
229
+ const payloadAud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
230
+
231
+ // Verifica se há intersecção entre as audiências
232
+ const hasValidAud = payloadAud.some(a => a && audiences.includes(a));
233
+ if (!hasValidAud) {
234
+ throw new Error(`Audience inválida. Esperado: ${audiences.join(', ')}, Recebido: ${payload.aud}`);
235
+ }
236
+ }
237
+
238
+ if (options?.maxTokenAge && payload.iat) {
239
+ const maxAge = parseTime(options.maxTokenAge);
240
+ if (now - payload.iat > maxAge) {
241
+ throw new Error(`Token excedeu a idade máxima permitida de ${options.maxTokenAge}.`);
242
+ }
243
+ }
244
+
245
+ return { payload, protectedHeader };
246
+ }
247
+
248
+ // --- Utilitário de Geração de Chaves (Bônus) ---
249
+ export function generateKeyPair() {
250
+ return crypto.generateKeyPairSync('ed25519', {
251
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
252
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
253
+ });
254
+ }
255
+
256
+ // --- Exemplo de Uso estilo 'jose' (Descomente para rodar) ---
257
+ /*
258
+ (async () => {
259
+ try {
260
+ const { publicKey, privateKey } = generateKeyPair();
261
+
262
+ // 1. Assinatura (Builder Pattern)
263
+ const jwt = await new SignJWT({ 'urn:example:claim': true, userID: 123 })
264
+ .setProtectedHeader({ alg: 'EdDSA' })
265
+ .setIssuedAt()
266
+ .setIssuer('urn:system:issuer')
267
+ .setAudience('urn:system:audience')
268
+ .setExpirationTime('2h') // Expira em 2 horas
269
+ .sign(privateKey);
270
+
271
+ console.log('Token Gerado:', jwt);
272
+
273
+ // 2. Verificação
274
+ const { payload, protectedHeader } = await jwtVerify(jwt, publicKey, {
275
+ issuer: 'urn:system:issuer',
276
+ audience: 'urn:system:audience',
277
+ });
278
+
279
+ console.log('Header Verificado:', protectedHeader);
280
+ console.log('Payload Verificado:', payload);
281
+
282
+ } catch (err) {
283
+ console.error('Falha:', err);
284
+ }
285
+ })();
286
+ */
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": [
7
+ "ES2022"
8
+ ],
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "noImplicitAny": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true
16
+ },
17
+ "include": [
18
+ "src/**/*"
19
+ ],
20
+ "exclude": [
21
+ "node_modules",
22
+ "**/*.spec.ts"
23
+ ]
24
+ }