@vatts/auth 2.1.3-canary.1.0.2 → 2.1.3-canary.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/LICENSE +12 -12
- package/README.md +58 -58
- package/dist/core.d.ts +2 -1
- package/dist/core.js +19 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/jwt.d.ts +4 -0
- package/dist/jwt.js +3 -0
- package/dist/providers/passkey.d.ts +59 -0
- package/dist/providers/passkey.js +379 -0
- package/dist/providers.d.ts +1 -0
- package/dist/providers.js +3 -1
- package/dist/react/react.d.ts +8 -0
- package/dist/react/react.js +73 -1
- package/dist/routes.js +72 -71
- package/dist/types.d.ts +4 -4
- package/dist/vue/component.vue +29 -29
- package/dist/vue/session.d.ts +8 -0
- package/dist/vue/session.js +73 -1
- package/package.json +6 -2
package/LICENSE
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
Copyright 2026 mfraz
|
|
2
|
-
|
|
3
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
you may not use this file except in compliance with the License.
|
|
5
|
-
You may obtain a copy of the License at
|
|
6
|
-
|
|
7
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
|
|
9
|
-
Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
See the License for the specific language governing permissions and
|
|
1
|
+
Copyright 2026 mfraz
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
13
|
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png">
|
|
4
|
-
<img alt="Vatts.js logo" src="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png" width="128">
|
|
5
|
-
</picture>
|
|
6
|
-
<h1>@vatts/auth</h1>
|
|
7
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/@vatts/auth)
|
|
9
|
-
[](../../LICENSE)
|
|
10
|
-
[](https://github.com/mfrazlab/vatts.js)
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
___
|
|
14
|
-
|
|
15
|
-
## Getting Started
|
|
16
|
-
|
|
17
|
-
**@vatts/auth** is the official authentication package for the **Vatts.js** framework, providing secure, flexible, and production-ready authentication with JWT, multiple OAuth providers and session management.
|
|
18
|
-
|
|
19
|
-
- Secure by default — robust JWT implementation
|
|
20
|
-
- Multiple providers — OAuth, credentials, and custom
|
|
21
|
-
- Ready for React and Vue — integrated hooks and components
|
|
22
|
-
- Strong typing — 100% TypeScript
|
|
23
|
-
- Zero configuration — works out-of-the-box
|
|
24
|
-
- Production-ready — CSRF protection, and more
|
|
25
|
-
|
|
26
|
-
___
|
|
27
|
-
|
|
28
|
-
## Documentation
|
|
29
|
-
|
|
30
|
-
Visit [https://vatts.mfraz.ovh](https://vatts.mfraz.ovh) for full documentation of Vatts.js and its official packages.
|
|
31
|
-
|
|
32
|
-
___
|
|
33
|
-
|
|
34
|
-
## Community
|
|
35
|
-
|
|
36
|
-
Join the Vatts.js community on [GitHub Discussions](https://github.com/mfraz/vatts.js) to ask questions, share ideas, and showcase your projects.
|
|
37
|
-
|
|
38
|
-
___
|
|
39
|
-
|
|
40
|
-
## Security
|
|
41
|
-
|
|
42
|
-
Vatts.js uses a security-first hybrid architecture in which Node.js coordinates the application layer and a high-performance Go networking engine manages HTTP connections and transport protocols.
|
|
43
|
-
By delegating network handling to Go, the platform achieves strong isolation, predictable request processing, and native support for modern transports like HTTP/3 under SSL — all while maintaining consistent security enforcement across environments, including local and non-TLS setups.
|
|
44
|
-
|
|
45
|
-
If you believe you have found a security vulnerability in Vatts.js, we encourage you to **responsibly disclose it and NOT open a public issue**.
|
|
46
|
-
|
|
47
|
-
To participate in our vulnerability disclosure program, please email [contact@mfraz.ovh](mailto:contact@mfraz.ovh). We will add you to the program and provide further instructions for submitting your report.
|
|
48
|
-
|
|
49
|
-
___
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## License
|
|
53
|
-
|
|
54
|
-
Copyright 2026 mfraz
|
|
55
|
-
|
|
56
|
-
This project is licensed under the [Apache License 2.0](./LICENSE).
|
|
57
|
-
|
|
58
|
-
___
|
|
1
|
+
<div align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png">
|
|
4
|
+
<img alt="Vatts.js logo" src="https://raw.githubusercontent.com/mfrazlab/vatts-docs/master/public/logo.png" width="128">
|
|
5
|
+
</picture>
|
|
6
|
+
<h1>@vatts/auth</h1>
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@vatts/auth)
|
|
9
|
+
[](../../LICENSE)
|
|
10
|
+
[](https://github.com/mfrazlab/vatts.js)
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
___
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
**@vatts/auth** is the official authentication package for the **Vatts.js** framework, providing secure, flexible, and production-ready authentication with JWT, multiple OAuth providers and session management.
|
|
18
|
+
|
|
19
|
+
- Secure by default — robust JWT implementation
|
|
20
|
+
- Multiple providers — OAuth, credentials, and custom
|
|
21
|
+
- Ready for React and Vue — integrated hooks and components
|
|
22
|
+
- Strong typing — 100% TypeScript
|
|
23
|
+
- Zero configuration — works out-of-the-box
|
|
24
|
+
- Production-ready — CSRF protection, and more
|
|
25
|
+
|
|
26
|
+
___
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
Visit [https://vatts.mfraz.ovh](https://vatts.mfraz.ovh) for full documentation of Vatts.js and its official packages.
|
|
31
|
+
|
|
32
|
+
___
|
|
33
|
+
|
|
34
|
+
## Community
|
|
35
|
+
|
|
36
|
+
Join the Vatts.js community on [GitHub Discussions](https://github.com/mfraz/vatts.js) to ask questions, share ideas, and showcase your projects.
|
|
37
|
+
|
|
38
|
+
___
|
|
39
|
+
|
|
40
|
+
## Security
|
|
41
|
+
|
|
42
|
+
Vatts.js uses a security-first hybrid architecture in which Node.js coordinates the application layer and a high-performance Go networking engine manages HTTP connections and transport protocols.
|
|
43
|
+
By delegating network handling to Go, the platform achieves strong isolation, predictable request processing, and native support for modern transports like HTTP/3 under SSL — all while maintaining consistent security enforcement across environments, including local and non-TLS setups.
|
|
44
|
+
|
|
45
|
+
If you believe you have found a security vulnerability in Vatts.js, we encourage you to **responsibly disclose it and NOT open a public issue**.
|
|
46
|
+
|
|
47
|
+
To participate in our vulnerability disclosure program, please email [contact@mfraz.ovh](mailto:contact@mfraz.ovh). We will add you to the program and provide further instructions for submitting your report.
|
|
48
|
+
|
|
49
|
+
___
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
Copyright 2026 mfraz
|
|
55
|
+
|
|
56
|
+
This project is licensed under the [Apache License 2.0](./LICENSE).
|
|
57
|
+
|
|
58
|
+
___
|
package/dist/core.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare class VattsAuth {
|
|
|
11
11
|
/**
|
|
12
12
|
* Autentica um usuário usando um provider específico
|
|
13
13
|
*/
|
|
14
|
-
signIn(providerId: string, credentials: Record<string, string
|
|
14
|
+
signIn(providerId: string, credentials: Record<string, string>, req: VattsRequest): Promise<{
|
|
15
15
|
session: Session;
|
|
16
16
|
token: string;
|
|
17
17
|
} | {
|
|
@@ -44,6 +44,7 @@ export declare class VattsAuth {
|
|
|
44
44
|
provider: string;
|
|
45
45
|
route: any;
|
|
46
46
|
}>;
|
|
47
|
+
updateSession(request: VattsRequest, data: any): VattsResponse;
|
|
47
48
|
/**
|
|
48
49
|
* Cria resposta com cookie de autenticação - Secure implementation
|
|
49
50
|
*/
|
package/dist/core.js
CHANGED
|
@@ -45,7 +45,7 @@ class VattsAuth {
|
|
|
45
45
|
/**
|
|
46
46
|
* Autentica um usuário usando um provider específico
|
|
47
47
|
*/
|
|
48
|
-
async signIn(providerId, credentials) {
|
|
48
|
+
async signIn(providerId, credentials, req) {
|
|
49
49
|
const provider = this.config.providers.find(p => p.id === providerId);
|
|
50
50
|
if (!provider) {
|
|
51
51
|
console.error(`[vatts-auth] Provider not found: ${providerId}`);
|
|
@@ -53,7 +53,7 @@ class VattsAuth {
|
|
|
53
53
|
}
|
|
54
54
|
try {
|
|
55
55
|
// Usa o método handleSignIn do provider
|
|
56
|
-
const result = await provider.handleSignIn(credentials);
|
|
56
|
+
const result = await provider.handleSignIn(credentials, req);
|
|
57
57
|
if (!result)
|
|
58
58
|
return null;
|
|
59
59
|
// Se resultado é string, é URL de redirecionamento OAuth
|
|
@@ -61,18 +61,18 @@ class VattsAuth {
|
|
|
61
61
|
return { redirectUrl: result };
|
|
62
62
|
}
|
|
63
63
|
// Se resultado é User, cria sessão
|
|
64
|
-
|
|
64
|
+
let user = result;
|
|
65
65
|
// Callback de signIn se definido
|
|
66
66
|
if (this.config.callbacks?.signIn) {
|
|
67
67
|
const allowed = await this.config.callbacks.signIn(user, { provider: providerId }, {});
|
|
68
68
|
if (!allowed)
|
|
69
69
|
return null;
|
|
70
70
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
sessionResult.session = await this.config.callbacks.session({ session: sessionResult.session, user, provider: providerId });
|
|
71
|
+
if (this.config.callbacks?.user) {
|
|
72
|
+
const updatedUser = await this.config.callbacks.user({ user, provider: providerId });
|
|
73
|
+
user = updatedUser;
|
|
75
74
|
}
|
|
75
|
+
const sessionResult = this.sessionManager.createSession(user);
|
|
76
76
|
return sessionResult;
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
@@ -152,6 +152,18 @@ class VattsAuth {
|
|
|
152
152
|
}
|
|
153
153
|
return routes;
|
|
154
154
|
}
|
|
155
|
+
updateSession(request, data) {
|
|
156
|
+
const token = this.getTokenFromRequest(request);
|
|
157
|
+
if (!token) {
|
|
158
|
+
return vatts_1.VattsResponse.json({ error: "No session token found" }, { status: 400 });
|
|
159
|
+
}
|
|
160
|
+
const session = this.sessionManager.verifySession(token);
|
|
161
|
+
if (!session) {
|
|
162
|
+
throw new Error("Session not found");
|
|
163
|
+
}
|
|
164
|
+
const update = this.sessionManager.updateUserSession(data);
|
|
165
|
+
return this.createAuthResponse(update.token, { session: update.session });
|
|
166
|
+
}
|
|
155
167
|
/**
|
|
156
168
|
* Cria resposta com cookie de autenticação - Secure implementation
|
|
157
169
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export * from './providers';
|
|
|
3
3
|
export * from './core';
|
|
4
4
|
export * from './routes';
|
|
5
5
|
export * from './jwt';
|
|
6
|
-
export { CredentialsProvider, DiscordProvider, GoogleProvider } from './providers';
|
|
6
|
+
export { CredentialsProvider, DiscordProvider, GoogleProvider, PasskeyStorage, PasskeysProvider } from './providers';
|
|
7
7
|
export { createAuthRoutes } from './routes';
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.createAuthRoutes = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
17
|
+
exports.createAuthRoutes = exports.PasskeysProvider = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
18
18
|
/*
|
|
19
19
|
* This file is part of the Vatts.js Project.
|
|
20
20
|
* Copyright (c) 2026 mfraz
|
|
@@ -41,5 +41,6 @@ var providers_1 = require("./providers");
|
|
|
41
41
|
Object.defineProperty(exports, "CredentialsProvider", { enumerable: true, get: function () { return providers_1.CredentialsProvider; } });
|
|
42
42
|
Object.defineProperty(exports, "DiscordProvider", { enumerable: true, get: function () { return providers_1.DiscordProvider; } });
|
|
43
43
|
Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return providers_1.GoogleProvider; } });
|
|
44
|
+
Object.defineProperty(exports, "PasskeysProvider", { enumerable: true, get: function () { return providers_1.PasskeysProvider; } });
|
|
44
45
|
var routes_1 = require("./routes");
|
|
45
46
|
Object.defineProperty(exports, "createAuthRoutes", { enumerable: true, get: function () { return routes_1.createAuthRoutes; } });
|
package/dist/jwt.d.ts
CHANGED
package/dist/jwt.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AuthProviderClass, AuthRoute, User } from '../types';
|
|
2
|
+
import { VattsRequest } from 'vatts';
|
|
3
|
+
/**
|
|
4
|
+
* Interface que o desenvolvedor final DEVE implementar para conectar o próprio Banco de Dados.
|
|
5
|
+
* Como Passkeys exigem guardar "Challenges" temporários e as Chaves Públicas,
|
|
6
|
+
* abstraímos isso para dar liberdade total.
|
|
7
|
+
*/
|
|
8
|
+
export interface PasskeyStorage {
|
|
9
|
+
saveChallenge: (username: string, challenge: string) => Promise<void>;
|
|
10
|
+
getChallenge: (username: string) => Promise<string | null>;
|
|
11
|
+
deleteChallenge: (username: string) => Promise<void>;
|
|
12
|
+
saveCredential: (username: string, credential: any) => Promise<void>;
|
|
13
|
+
getUserCredentials: (username: string) => Promise<any[]>;
|
|
14
|
+
updateCredentialCounter: (credentialID: string, newCounter: number) => Promise<void>;
|
|
15
|
+
getUserByUsername: (username: string) => Promise<User | null>;
|
|
16
|
+
}
|
|
17
|
+
export interface PasskeysConfig {
|
|
18
|
+
id?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
rpName: string;
|
|
21
|
+
rpID?: string;
|
|
22
|
+
origin?: string;
|
|
23
|
+
storage: PasskeyStorage;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Provider para autenticação sem senha usando Passkeys (WebAuthn / FIDO2)
|
|
27
|
+
* * Fluxo de Registro:
|
|
28
|
+
* 1. POST /api/auth/passkeys/register/start -> Gera opções pro dispositivo
|
|
29
|
+
* 2. POST /api/auth/passkeys/register/finish -> Valida e salva no DB (via Storage)
|
|
30
|
+
* * Fluxo de Login:
|
|
31
|
+
* 1. POST /api/auth/passkeys/login/start -> Gera opções de login
|
|
32
|
+
* 2. POST /api/auth/signin -> O Vatts direciona para o `handleSignIn` deste provider
|
|
33
|
+
*/
|
|
34
|
+
export declare class PasskeysProvider implements AuthProviderClass {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly type: string;
|
|
38
|
+
private config;
|
|
39
|
+
private loginChallenges;
|
|
40
|
+
constructor(config: PasskeysConfig);
|
|
41
|
+
/**
|
|
42
|
+
* Função auxiliar para pegar a origem e o RPID dinamicamente pela requisição,
|
|
43
|
+
* se não foram informados estaticamente na configuração.
|
|
44
|
+
*/
|
|
45
|
+
private getRequestInfo;
|
|
46
|
+
/**
|
|
47
|
+
* O Vatts chama este método quando o front-end envia as credenciais para o endpoint genérico de login.
|
|
48
|
+
* Para Passkeys, o "credentials" conterá o username e o response JSON gerado pelo navegador.
|
|
49
|
+
*/
|
|
50
|
+
handleSignIn(credentials: Record<string, string>, req?: VattsRequest): Promise<User | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Retorna configuração pública do provider
|
|
53
|
+
*/
|
|
54
|
+
getConfig(): any;
|
|
55
|
+
/**
|
|
56
|
+
* Rotas adicionais para lidar com a geração de chaves (Registro) e inicio de Login
|
|
57
|
+
*/
|
|
58
|
+
additionalRoutes: AuthRoute[];
|
|
59
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PasskeysProvider = void 0;
|
|
4
|
+
const vatts_1 = require("vatts");
|
|
5
|
+
const server_1 = require("@simplewebauthn/server");
|
|
6
|
+
/**
|
|
7
|
+
* Funções auxiliares para serializar/desserializar Buffer
|
|
8
|
+
*/
|
|
9
|
+
function bufferToBase64(buffer) {
|
|
10
|
+
if (typeof buffer === 'string')
|
|
11
|
+
return buffer; // Já é string
|
|
12
|
+
if (buffer instanceof Uint8Array || Buffer.isBuffer(buffer)) {
|
|
13
|
+
return Buffer.from(buffer).toString('base64');
|
|
14
|
+
}
|
|
15
|
+
return String(buffer);
|
|
16
|
+
}
|
|
17
|
+
function toUint8Array(input) {
|
|
18
|
+
if (input == null)
|
|
19
|
+
throw new TypeError("toUint8Array: input is null/undefined");
|
|
20
|
+
// Uint8Array / Buffer
|
|
21
|
+
if (input instanceof Uint8Array) {
|
|
22
|
+
const ab = input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength);
|
|
23
|
+
return new Uint8Array(ab);
|
|
24
|
+
}
|
|
25
|
+
// ArrayBuffer
|
|
26
|
+
if (input instanceof ArrayBuffer) {
|
|
27
|
+
return new Uint8Array(input);
|
|
28
|
+
}
|
|
29
|
+
// number[]
|
|
30
|
+
if (Array.isArray(input) && input.every(n => typeof n === "number")) {
|
|
31
|
+
const u8 = Uint8Array.from(input);
|
|
32
|
+
const ab = u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
|
|
33
|
+
return new Uint8Array(ab);
|
|
34
|
+
}
|
|
35
|
+
// base64/base64url string
|
|
36
|
+
if (typeof input === "string") {
|
|
37
|
+
const b64 = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
38
|
+
const pad = b64.length % 4 ? "=".repeat(4 - (b64.length % 4)) : "";
|
|
39
|
+
const buf = Buffer.from(b64 + pad, "base64");
|
|
40
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
41
|
+
return new Uint8Array(ab);
|
|
42
|
+
}
|
|
43
|
+
// object cases
|
|
44
|
+
if (typeof input === "object") {
|
|
45
|
+
const obj = input;
|
|
46
|
+
// { type:"Buffer", data:[...] }
|
|
47
|
+
if (obj.type === "Buffer" && Array.isArray(obj.data)) {
|
|
48
|
+
return toUint8Array(obj.data);
|
|
49
|
+
}
|
|
50
|
+
// { data:[...] }
|
|
51
|
+
if (Array.isArray(obj.data)) {
|
|
52
|
+
return toUint8Array(obj.data);
|
|
53
|
+
}
|
|
54
|
+
// { bytes:[...] }
|
|
55
|
+
if (Array.isArray(obj.bytes)) {
|
|
56
|
+
return toUint8Array(obj.bytes);
|
|
57
|
+
}
|
|
58
|
+
// { buffer: ... } wrapper
|
|
59
|
+
if (obj.buffer != null) {
|
|
60
|
+
// às vezes vem { buffer: { type:"Buffer", data:[...] } }
|
|
61
|
+
return toUint8Array(obj.buffer);
|
|
62
|
+
}
|
|
63
|
+
// array-like: {0:12,1:34,length:2}
|
|
64
|
+
if (typeof obj.length === "number") {
|
|
65
|
+
const arr = Array.from({ length: obj.length }, (_, i) => obj[i]);
|
|
66
|
+
if (arr.every(n => typeof n === "number")) {
|
|
67
|
+
return toUint8Array(arr);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// numeric-key object without length: {"0":165,"1":1,...}
|
|
71
|
+
const numericKeys = Object.keys(obj).filter(key => /^\d+$/.test(key));
|
|
72
|
+
if (numericKeys.length > 0) {
|
|
73
|
+
const arr = numericKeys
|
|
74
|
+
.sort((a, b) => Number(a) - Number(b))
|
|
75
|
+
.map(key => obj[key]);
|
|
76
|
+
if (arr.every(n => typeof n === "number")) {
|
|
77
|
+
return toUint8Array(arr);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
throw new TypeError(`toUint8Array: unsupported input type: ${Object.prototype.toString.call(input)}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Provider para autenticação sem senha usando Passkeys (WebAuthn / FIDO2)
|
|
85
|
+
* * Fluxo de Registro:
|
|
86
|
+
* 1. POST /api/auth/passkeys/register/start -> Gera opções pro dispositivo
|
|
87
|
+
* 2. POST /api/auth/passkeys/register/finish -> Valida e salva no DB (via Storage)
|
|
88
|
+
* * Fluxo de Login:
|
|
89
|
+
* 1. POST /api/auth/passkeys/login/start -> Gera opções de login
|
|
90
|
+
* 2. POST /api/auth/signin -> O Vatts direciona para o `handleSignIn` deste provider
|
|
91
|
+
*/
|
|
92
|
+
class PasskeysProvider {
|
|
93
|
+
constructor(config) {
|
|
94
|
+
this.type = 'passkeys';
|
|
95
|
+
// Map para armazenar challenges temporários de login (não-persistentes)
|
|
96
|
+
this.loginChallenges = new Map();
|
|
97
|
+
/**
|
|
98
|
+
* Rotas adicionais para lidar com a geração de chaves (Registro) e inicio de Login
|
|
99
|
+
*/
|
|
100
|
+
this.additionalRoutes = [
|
|
101
|
+
// ==========================================
|
|
102
|
+
// REGISTRO - PASSO 1 (START)
|
|
103
|
+
// ==========================================
|
|
104
|
+
{
|
|
105
|
+
method: 'POST',
|
|
106
|
+
path: '/api/auth/passkeys/register/start',
|
|
107
|
+
handler: async (req) => {
|
|
108
|
+
try {
|
|
109
|
+
const body = await req.json();
|
|
110
|
+
const { username } = body;
|
|
111
|
+
const { rpID } = this.getRequestInfo(req);
|
|
112
|
+
if (!username)
|
|
113
|
+
return vatts_1.VattsResponse.json({ error: 'Username required' }, { status: 400 });
|
|
114
|
+
// Usando buffer corretamente para V13+
|
|
115
|
+
const options = await (0, server_1.generateRegistrationOptions)({
|
|
116
|
+
rpName: this.config.rpName,
|
|
117
|
+
rpID,
|
|
118
|
+
userID: new Uint8Array(Buffer.from(username)),
|
|
119
|
+
userName: username,
|
|
120
|
+
attestationType: 'none',
|
|
121
|
+
authenticatorSelection: {
|
|
122
|
+
residentKey: 'required', // Obrigatório para discoverable credentials
|
|
123
|
+
userVerification: 'preferred',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
// Salva desafio no banco de dados customizado do usuário
|
|
127
|
+
await this.config.storage.saveChallenge(username, options.challenge);
|
|
128
|
+
return vatts_1.VattsResponse.json(options);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
// ==========================================
|
|
136
|
+
// REGISTRO - PASSO 2 (FINISH)
|
|
137
|
+
// ==========================================
|
|
138
|
+
{
|
|
139
|
+
method: 'POST',
|
|
140
|
+
path: '/api/auth/passkeys/register/finish',
|
|
141
|
+
handler: async (req) => {
|
|
142
|
+
try {
|
|
143
|
+
const body = await req.json();
|
|
144
|
+
const { origin, rpID } = this.getRequestInfo(req);
|
|
145
|
+
// Agora aceitamos um "name" opcional no body (Ex: "PC do Trabalho")
|
|
146
|
+
const { username, response, name } = body;
|
|
147
|
+
const expectedChallenge = await this.config.storage.getChallenge(username);
|
|
148
|
+
if (!expectedChallenge)
|
|
149
|
+
return vatts_1.VattsResponse.json({ error: 'Challenge expired/not found' }, { status: 400 });
|
|
150
|
+
const verification = await (0, server_1.verifyRegistrationResponse)({
|
|
151
|
+
response,
|
|
152
|
+
expectedChallenge,
|
|
153
|
+
expectedOrigin: origin,
|
|
154
|
+
expectedRPID: rpID,
|
|
155
|
+
});
|
|
156
|
+
if (verification.verified && verification.registrationInfo) {
|
|
157
|
+
const { credential } = verification.registrationInfo;
|
|
158
|
+
// Formata a credencial e salva no DB do dev
|
|
159
|
+
await this.config.storage.saveCredential(username, {
|
|
160
|
+
credentialID: credential.id,
|
|
161
|
+
credentialPublicKey: bufferToBase64(credential.publicKey),
|
|
162
|
+
counter: credential.counter,
|
|
163
|
+
transports: response.response.transports,
|
|
164
|
+
name: name || 'Chave de Acesso', // Salva o nome ou um padrão
|
|
165
|
+
});
|
|
166
|
+
await this.config.storage.deleteChallenge(username);
|
|
167
|
+
return vatts_1.VattsResponse.json({ success: true });
|
|
168
|
+
}
|
|
169
|
+
return vatts_1.VattsResponse.json({ success: false }, { status: 400 });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
// ==========================================
|
|
177
|
+
// LOGIN - PASSO 1 (START)
|
|
178
|
+
// ==========================================
|
|
179
|
+
{
|
|
180
|
+
method: 'POST',
|
|
181
|
+
path: '/api/auth/passkeys/login/start',
|
|
182
|
+
handler: async (req) => {
|
|
183
|
+
try {
|
|
184
|
+
const { rpID } = this.getRequestInfo(req);
|
|
185
|
+
// Não precisa de username - usa discoverable credentials
|
|
186
|
+
// O navegador mostrará as keys disponíveis automaticamente
|
|
187
|
+
const options = await (0, server_1.generateAuthenticationOptions)({
|
|
188
|
+
rpID,
|
|
189
|
+
userVerification: 'preferred',
|
|
190
|
+
// Omitindo allowCredentials para permitir discoverable credentials
|
|
191
|
+
});
|
|
192
|
+
// Gera um sessionId único para esta tentativa de login
|
|
193
|
+
const sessionId = typeof crypto !== 'undefined' && crypto.randomUUID
|
|
194
|
+
? crypto.randomUUID()
|
|
195
|
+
: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
196
|
+
// Salva o challenge no Map temporário (não usa storage)
|
|
197
|
+
this.loginChallenges.set(`passkey:${sessionId}`, {
|
|
198
|
+
challenge: options.challenge,
|
|
199
|
+
timestamp: Date.now()
|
|
200
|
+
});
|
|
201
|
+
return vatts_1.VattsResponse.json({ options, sessionId });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
// ==========================================
|
|
209
|
+
// LISTAR CREDENCIAIS (Para mostrar na UI)
|
|
210
|
+
// ==========================================
|
|
211
|
+
{
|
|
212
|
+
method: 'POST', // Usando POST para facilitar o envio do username no body
|
|
213
|
+
path: '/api/auth/passkeys/list',
|
|
214
|
+
handler: async (req) => {
|
|
215
|
+
try {
|
|
216
|
+
const body = await req.json();
|
|
217
|
+
const { username } = body;
|
|
218
|
+
if (!username)
|
|
219
|
+
return vatts_1.VattsResponse.json({ error: 'Username required' }, { status: 400 });
|
|
220
|
+
const credentials = await this.config.storage.getUserCredentials(username);
|
|
221
|
+
// Retorna as credenciais limpando a chave pública (boa prática de segurança para o Front-end)
|
|
222
|
+
const safeCredentials = credentials.map(cred => ({
|
|
223
|
+
id: cred.credentialID,
|
|
224
|
+
name: cred.name || 'Chave de Acesso',
|
|
225
|
+
transports: cred.transports,
|
|
226
|
+
createdAt: cred.createdAt // Caso o dev salve a data no banco dele
|
|
227
|
+
}));
|
|
228
|
+
return vatts_1.VattsResponse.json({ success: true, credentials: safeCredentials });
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
return vatts_1.VattsResponse.json({ error: error.message }, { status: 500 });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
this.config = config;
|
|
237
|
+
this.id = config.id || 'passkeys';
|
|
238
|
+
this.name = config.name || 'Passkeys';
|
|
239
|
+
// Limpa challenges expirados a cada 5 minutos (TTL de 10 minutos)
|
|
240
|
+
setInterval(() => {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
for (const [key, value] of this.loginChallenges.entries()) {
|
|
243
|
+
if (now - value.timestamp > 10 * 60 * 1000) {
|
|
244
|
+
this.loginChallenges.delete(key);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}, 5 * 60 * 1000);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Função auxiliar para pegar a origem e o RPID dinamicamente pela requisição,
|
|
251
|
+
* se não foram informados estaticamente na configuração.
|
|
252
|
+
*/
|
|
253
|
+
getRequestInfo(req) {
|
|
254
|
+
let origin = this.config.origin;
|
|
255
|
+
if (!origin && req) {
|
|
256
|
+
// Lida tanto com API Fetch nativa (headers.get) quanto Node.js plain (headers.origin)
|
|
257
|
+
if (typeof req.headers?.get === 'function') {
|
|
258
|
+
const originHeader = req.headers['origin'] || req.headers['host'];
|
|
259
|
+
origin = Array.isArray(originHeader) ? originHeader[0] : originHeader;
|
|
260
|
+
if (!origin) {
|
|
261
|
+
const hostHeader = req.headers['host'];
|
|
262
|
+
const host = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader;
|
|
263
|
+
if (host)
|
|
264
|
+
origin = `http://${host}`; // Fallback para host
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (req.headers) {
|
|
268
|
+
// @ts-ignore
|
|
269
|
+
origin = req.headers.origin || (req.headers.host ? `http://${req.headers.host}` : undefined);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Fallback final
|
|
273
|
+
origin = origin || 'http://localhost:3000';
|
|
274
|
+
let rpID = this.config.rpID;
|
|
275
|
+
if (!rpID) {
|
|
276
|
+
try {
|
|
277
|
+
rpID = new URL(origin).hostname;
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
rpID = 'localhost';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { origin, rpID };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* O Vatts chama este método quando o front-end envia as credenciais para o endpoint genérico de login.
|
|
287
|
+
* Para Passkeys, o "credentials" conterá o username e o response JSON gerado pelo navegador.
|
|
288
|
+
*/
|
|
289
|
+
async handleSignIn(credentials, req) {
|
|
290
|
+
try {
|
|
291
|
+
const { response: responseString, sessionId } = credentials;
|
|
292
|
+
if (!responseString || !sessionId) {
|
|
293
|
+
console.error(`[${this.id} Provider] Missing response payload or sessionId`);
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const { origin, rpID } = this.getRequestInfo(req);
|
|
297
|
+
const response = JSON.parse(responseString);
|
|
298
|
+
// Recupera o challenge do Map de login (temporário, não do storage)
|
|
299
|
+
const challengeData = this.loginChallenges.get(`passkey:${sessionId}`);
|
|
300
|
+
const expectedChallenge = challengeData?.challenge || null;
|
|
301
|
+
if (!expectedChallenge) {
|
|
302
|
+
console.error(`[${this.id} Provider] No active challenge found`);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
// O response.id é a credentialID. Precisamos encontrar qual usuário possui essa chave
|
|
306
|
+
// Como não sabemos o username, a implementação do storage deve permitir buscar por credentialID
|
|
307
|
+
// Para isso, você pode adicionar um método como findUsernameByCredentialID
|
|
308
|
+
//
|
|
309
|
+
// Alternativa: Como o userHandle é enviado no response da discoverable credential,
|
|
310
|
+
// podemos decodificá-lo para obter o username original
|
|
311
|
+
let username = null;
|
|
312
|
+
// Tenta extrair username do userHandle (armazenado durante registro)
|
|
313
|
+
if (response.response.userHandle) {
|
|
314
|
+
try {
|
|
315
|
+
username = Buffer.from(response.response.userHandle, 'base64').toString('utf-8');
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
console.error(`[${this.id} Provider] Could not decode userHandle`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (!username) {
|
|
322
|
+
console.error(`[${this.id} Provider] Could not determine username from credential`);
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
// Agora que temos o username, busca a chave específica
|
|
326
|
+
const userPasskeys = await this.config.storage.getUserCredentials(username);
|
|
327
|
+
const passkey = userPasskeys.find(key => key.credentialID === response.id);
|
|
328
|
+
if (!passkey) {
|
|
329
|
+
console.error(`[${this.id} Provider] Credential not found for user`);
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
// Valida a resposta com a biblioteca (V13+)
|
|
333
|
+
const verification = await (0, server_1.verifyAuthenticationResponse)({
|
|
334
|
+
response,
|
|
335
|
+
expectedChallenge,
|
|
336
|
+
expectedOrigin: origin,
|
|
337
|
+
expectedRPID: rpID,
|
|
338
|
+
credential: {
|
|
339
|
+
id: passkey.credentialID,
|
|
340
|
+
publicKey: toUint8Array(passkey.credentialPublicKey),
|
|
341
|
+
counter: passkey.counter,
|
|
342
|
+
transports: passkey.transports,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
if (verification.verified) {
|
|
346
|
+
// Atualiza o contador de segurança e deleta o challenge do Map
|
|
347
|
+
await this.config.storage.updateCredentialCounter(passkey.credentialID, verification.authenticationInfo.newCounter);
|
|
348
|
+
this.loginChallenges.delete(`passkey:${sessionId}`);
|
|
349
|
+
// Busca o objeto completo do usuário no banco para o Vatts.js iniciar a sessão
|
|
350
|
+
const user = await this.config.storage.getUserByUsername(username);
|
|
351
|
+
if (!user)
|
|
352
|
+
return null;
|
|
353
|
+
return {
|
|
354
|
+
...user,
|
|
355
|
+
provider: this.id,
|
|
356
|
+
providerId: username
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
console.error(`[${this.id} Provider] Error during sign in (WebAuthn):`, error);
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Retorna configuração pública do provider
|
|
368
|
+
*/
|
|
369
|
+
getConfig() {
|
|
370
|
+
return {
|
|
371
|
+
id: this.id,
|
|
372
|
+
name: this.name,
|
|
373
|
+
type: this.type,
|
|
374
|
+
rpName: this.config.rpName,
|
|
375
|
+
rpID: this.config.rpID, // Se omitido, continuará extraindo on-the-fly
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
exports.PasskeysProvider = PasskeysProvider;
|
package/dist/providers.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { CredentialsProvider } from './providers/credentials';
|
|
|
2
2
|
export { DiscordProvider } from './providers/discord';
|
|
3
3
|
export { GoogleProvider } from './providers/google';
|
|
4
4
|
export { GithubProvider } from "./providers/github";
|
|
5
|
+
export { PasskeyStorage, PasskeysProvider } from "./providers/passkey";
|
package/dist/providers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GithubProvider = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
3
|
+
exports.PasskeysProvider = exports.GithubProvider = exports.GoogleProvider = exports.DiscordProvider = exports.CredentialsProvider = void 0;
|
|
4
4
|
/*
|
|
5
5
|
* This file is part of the Vatts.js Project.
|
|
6
6
|
* Copyright (c) 2026 mfraz
|
|
@@ -26,3 +26,5 @@ var google_1 = require("./providers/google");
|
|
|
26
26
|
Object.defineProperty(exports, "GoogleProvider", { enumerable: true, get: function () { return google_1.GoogleProvider; } });
|
|
27
27
|
var github_1 = require("./providers/github");
|
|
28
28
|
Object.defineProperty(exports, "GithubProvider", { enumerable: true, get: function () { return github_1.GithubProvider; } });
|
|
29
|
+
var passkey_1 = require("./providers/passkey");
|
|
30
|
+
Object.defineProperty(exports, "PasskeysProvider", { enumerable: true, get: function () { return passkey_1.PasskeysProvider; } });
|
package/dist/react/react.d.ts
CHANGED
|
@@ -19,4 +19,12 @@ export declare function useAuth(): {
|
|
|
19
19
|
isAuthenticated: boolean;
|
|
20
20
|
isLoading: boolean;
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Função utilitária para registrar uma nova Passkey (WebAuthn).
|
|
24
|
+
* Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerPasskey(username: string, name?: string, basePath?: string): Promise<{
|
|
27
|
+
success: boolean;
|
|
28
|
+
error?: string;
|
|
29
|
+
}>;
|
|
22
30
|
export {};
|
package/dist/react/react.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SessionProvider = SessionProvider;
|
|
4
4
|
exports.useSession = useSession;
|
|
5
5
|
exports.useAuth = useAuth;
|
|
6
|
+
exports.registerPasskey = registerPasskey;
|
|
6
7
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
8
|
/*
|
|
8
9
|
* This file is part of the Vatts.js Project.
|
|
@@ -121,6 +122,38 @@ function SessionProvider({ children, basePath = '/api/auth', refetchInterval = 0
|
|
|
121
122
|
const signIn = (0, react_1.useCallback)(async (provider = 'credentials', options = {}) => {
|
|
122
123
|
try {
|
|
123
124
|
const { redirect = true, callbackUrl, popup = false, ...credentials } = options;
|
|
125
|
+
// --- INÍCIO DA INTERCEPTAÇÃO PASSKEYS ---
|
|
126
|
+
if (provider === 'passkeys') {
|
|
127
|
+
try {
|
|
128
|
+
// 1. Pede ao backend as opções de desafio para login (SEM username)
|
|
129
|
+
// Com discoverable credentials, o navegador mostra as keys disponíveis
|
|
130
|
+
const startRes = await fetch(`${basePath}/passkeys/login/start`, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'Content-Type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({})
|
|
134
|
+
});
|
|
135
|
+
if (!startRes.ok) {
|
|
136
|
+
const errData = await startRes.json().catch(() => ({}));
|
|
137
|
+
return { error: errData.error || 'Failed to start passkey login', status: startRes.status, ok: false };
|
|
138
|
+
}
|
|
139
|
+
const { options, sessionId } = await startRes.json();
|
|
140
|
+
if (!sessionId) {
|
|
141
|
+
return { error: 'No session ID received from server', status: 500, ok: false };
|
|
142
|
+
}
|
|
143
|
+
// 2. Importa dinamicamente para não quebrar em SSR e chama o navegador (FaceID, TouchID, etc)
|
|
144
|
+
// O navegador mostrará as credenciais disponíveis automaticamente
|
|
145
|
+
const { startAuthentication } = await import('@simplewebauthn/browser');
|
|
146
|
+
const asseResp = await startAuthentication(options);
|
|
147
|
+
// 3. Injeta a resposta assinada e o sessionId no credentials para seguir o fluxo normal do signIn
|
|
148
|
+
credentials.response = JSON.stringify(asseResp);
|
|
149
|
+
credentials.sessionId = sessionId;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
console.error('[vatts-auth] Error during passkey login flow:', error);
|
|
153
|
+
return { error: error.message || 'Passkey authentication failed', status: 400, ok: false };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// --- FIM DA INTERCEPTAÇÃO PASSKEYS ---
|
|
124
157
|
const response = await fetch(`${basePath}/signin`, {
|
|
125
158
|
method: 'POST',
|
|
126
159
|
headers: {
|
|
@@ -150,7 +183,7 @@ function SessionProvider({ children, basePath = '/api/auth', refetchInterval = 0
|
|
|
150
183
|
url: data.redirectUrl
|
|
151
184
|
};
|
|
152
185
|
}
|
|
153
|
-
// Se é sessão (credentials), atualiza e redireciona
|
|
186
|
+
// Se é sessão (credentials/passkeys), atualiza e redireciona
|
|
154
187
|
await fetchSession();
|
|
155
188
|
if (data.type === 'session') {
|
|
156
189
|
if (redirect && typeof window !== 'undefined') {
|
|
@@ -263,3 +296,42 @@ function useAuth() {
|
|
|
263
296
|
isLoading: status === 'loading'
|
|
264
297
|
};
|
|
265
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Função utilitária para registrar uma nova Passkey (WebAuthn).
|
|
301
|
+
* Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
|
|
302
|
+
*/
|
|
303
|
+
async function registerPasskey(username, name, basePath = '/api/auth') {
|
|
304
|
+
try {
|
|
305
|
+
const { startRegistration } = await import('@simplewebauthn/browser');
|
|
306
|
+
// 1. Pega as opções do servidor
|
|
307
|
+
const startRes = await fetch(`${basePath}/passkeys/register/start`, {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
headers: { 'Content-Type': 'application/json' },
|
|
310
|
+
body: JSON.stringify({ username })
|
|
311
|
+
});
|
|
312
|
+
if (!startRes.ok) {
|
|
313
|
+
const errData = await startRes.json().catch(() => ({}));
|
|
314
|
+
throw new Error(errData.error || 'Failed to start passkey registration');
|
|
315
|
+
}
|
|
316
|
+
const options = await startRes.json();
|
|
317
|
+
// 2. Chama a biometria/PIN do dispositivo
|
|
318
|
+
const attResp = await startRegistration(options);
|
|
319
|
+
// 3. Envia o resultado de volta pro servidor validar e salvar
|
|
320
|
+
const finishRes = await fetch(`${basePath}/passkeys/register/finish`, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: { 'Content-Type': 'application/json' },
|
|
323
|
+
body: JSON.stringify({ username, response: attResp, name })
|
|
324
|
+
});
|
|
325
|
+
const result = await finishRes.json();
|
|
326
|
+
if (result.success) {
|
|
327
|
+
return { success: true };
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
return { success: false, error: result.error || 'Verification failed on server' };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
console.error('[vatts-auth] Error during passkey registration:', error);
|
|
335
|
+
return { success: false, error: error.message || 'Passkey registration failed' };
|
|
336
|
+
}
|
|
337
|
+
}
|
package/dist/routes.js
CHANGED
|
@@ -127,7 +127,7 @@ async function handleSignIn(req, auth) {
|
|
|
127
127
|
const credentialsWithPopup = popup !== undefined
|
|
128
128
|
? { ...credentials, popup: String(popup) }
|
|
129
129
|
: credentials;
|
|
130
|
-
const result = await auth.signIn(provider, credentialsWithPopup);
|
|
130
|
+
const result = await auth.signIn(provider, credentialsWithPopup, req);
|
|
131
131
|
if (!result) {
|
|
132
132
|
return vatts_1.VattsResponse.json({ error: 'Invalid credentials' }, { status: 401 });
|
|
133
133
|
}
|
|
@@ -139,6 +139,7 @@ async function handleSignIn(req, auth) {
|
|
|
139
139
|
type: 'oauth'
|
|
140
140
|
});
|
|
141
141
|
}
|
|
142
|
+
console.log('result:', result);
|
|
142
143
|
// Se tem session, é credentials - retorna sessão
|
|
143
144
|
return auth.createAuthResponse(result.token, {
|
|
144
145
|
success: true,
|
|
@@ -161,76 +162,76 @@ function handlePopupCallback(req) {
|
|
|
161
162
|
const error = url.searchParams.get('error');
|
|
162
163
|
const provider = url.searchParams.get('provider') || 'unknown';
|
|
163
164
|
const callbackUrl = url.searchParams.get('callbackUrl') || '/';
|
|
164
|
-
const html = `
|
|
165
|
-
<!DOCTYPE html>
|
|
166
|
-
<html>
|
|
167
|
-
<head>
|
|
168
|
-
<meta charset="UTF-8">
|
|
169
|
-
<title>Authenticating...</title>
|
|
170
|
-
<style>
|
|
171
|
-
body {
|
|
172
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
173
|
-
display: flex;
|
|
174
|
-
justify-content: center;
|
|
175
|
-
align-items: center;
|
|
176
|
-
height: 100vh;
|
|
177
|
-
margin: 0;
|
|
178
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
179
|
-
color: white;
|
|
180
|
-
}
|
|
181
|
-
.container {
|
|
182
|
-
text-align: center;
|
|
183
|
-
}
|
|
184
|
-
.spinner {
|
|
185
|
-
border: 4px solid rgba(255,255,255,0.3);
|
|
186
|
-
border-radius: 50%;
|
|
187
|
-
border-top: 4px solid white;
|
|
188
|
-
width: 40px;
|
|
189
|
-
height: 40px;
|
|
190
|
-
animation: spin 1s linear infinite;
|
|
191
|
-
margin: 0 auto 20px;
|
|
192
|
-
}
|
|
193
|
-
@keyframes spin {
|
|
194
|
-
0% { transform: rotate(0deg); }
|
|
195
|
-
100% { transform: rotate(360deg); }
|
|
196
|
-
}
|
|
197
|
-
h2 {
|
|
198
|
-
margin: 0;
|
|
199
|
-
font-size: 24px;
|
|
200
|
-
}
|
|
201
|
-
p {
|
|
202
|
-
margin: 10px 0 0;
|
|
203
|
-
opacity: 0.9;
|
|
204
|
-
}
|
|
205
|
-
</style>
|
|
206
|
-
</head>
|
|
207
|
-
<body>
|
|
208
|
-
<div class="container">
|
|
209
|
-
<div class="spinner"></div>
|
|
210
|
-
<h2>${success ? '✓ Autenticação bem-sucedida' : '✗ Erro na autenticação'}</h2>
|
|
211
|
-
<p>${success ? 'Fechando janela...' : (error || 'Algo deu errado')}</p>
|
|
212
|
-
</div>
|
|
213
|
-
<script>
|
|
214
|
-
(function() {
|
|
215
|
-
try {
|
|
216
|
-
if (window.opener) {
|
|
217
|
-
console.log('Enviando mensagem para janela pai:')
|
|
218
|
-
window.opener.postMessage({
|
|
219
|
-
type: ${success ? "'oauth-success'" : "'oauth-error'"},
|
|
220
|
-
provider: "${provider}",
|
|
221
|
-
${success ? `callbackUrl: "${callbackUrl}"` : `error: "${error || 'Authentication failed'}"`}
|
|
222
|
-
}, window.location.origin);
|
|
223
|
-
}
|
|
224
|
-
setTimeout(() => {
|
|
225
|
-
window.close();
|
|
226
|
-
}, 1000);
|
|
227
|
-
} catch (e) {
|
|
228
|
-
console.error('Error communicating with parent window:', e);
|
|
229
|
-
}
|
|
230
|
-
})();
|
|
231
|
-
</script>
|
|
232
|
-
</body>
|
|
233
|
-
</html>
|
|
165
|
+
const html = `
|
|
166
|
+
<!DOCTYPE html>
|
|
167
|
+
<html>
|
|
168
|
+
<head>
|
|
169
|
+
<meta charset="UTF-8">
|
|
170
|
+
<title>Authenticating...</title>
|
|
171
|
+
<style>
|
|
172
|
+
body {
|
|
173
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: center;
|
|
176
|
+
align-items: center;
|
|
177
|
+
height: 100vh;
|
|
178
|
+
margin: 0;
|
|
179
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
180
|
+
color: white;
|
|
181
|
+
}
|
|
182
|
+
.container {
|
|
183
|
+
text-align: center;
|
|
184
|
+
}
|
|
185
|
+
.spinner {
|
|
186
|
+
border: 4px solid rgba(255,255,255,0.3);
|
|
187
|
+
border-radius: 50%;
|
|
188
|
+
border-top: 4px solid white;
|
|
189
|
+
width: 40px;
|
|
190
|
+
height: 40px;
|
|
191
|
+
animation: spin 1s linear infinite;
|
|
192
|
+
margin: 0 auto 20px;
|
|
193
|
+
}
|
|
194
|
+
@keyframes spin {
|
|
195
|
+
0% { transform: rotate(0deg); }
|
|
196
|
+
100% { transform: rotate(360deg); }
|
|
197
|
+
}
|
|
198
|
+
h2 {
|
|
199
|
+
margin: 0;
|
|
200
|
+
font-size: 24px;
|
|
201
|
+
}
|
|
202
|
+
p {
|
|
203
|
+
margin: 10px 0 0;
|
|
204
|
+
opacity: 0.9;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
207
|
+
</head>
|
|
208
|
+
<body>
|
|
209
|
+
<div class="container">
|
|
210
|
+
<div class="spinner"></div>
|
|
211
|
+
<h2>${success ? '✓ Autenticação bem-sucedida' : '✗ Erro na autenticação'}</h2>
|
|
212
|
+
<p>${success ? 'Fechando janela...' : (error || 'Algo deu errado')}</p>
|
|
213
|
+
</div>
|
|
214
|
+
<script>
|
|
215
|
+
(function() {
|
|
216
|
+
try {
|
|
217
|
+
if (window.opener) {
|
|
218
|
+
console.log('Enviando mensagem para janela pai:')
|
|
219
|
+
window.opener.postMessage({
|
|
220
|
+
type: ${success ? "'oauth-success'" : "'oauth-error'"},
|
|
221
|
+
provider: "${provider}",
|
|
222
|
+
${success ? `callbackUrl: "${callbackUrl}"` : `error: "${error || 'Authentication failed'}"`}
|
|
223
|
+
}, window.location.origin);
|
|
224
|
+
}
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
window.close();
|
|
227
|
+
}, 1000);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error('Error communicating with parent window:', e);
|
|
230
|
+
}
|
|
231
|
+
})();
|
|
232
|
+
</script>
|
|
233
|
+
</body>
|
|
234
|
+
</html>
|
|
234
235
|
`;
|
|
235
236
|
return vatts_1.VattsResponse.html(html);
|
|
236
237
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VattsRequest } from "vatts";
|
|
1
2
|
export type User = Record<string, any>;
|
|
2
3
|
export interface Session {
|
|
3
4
|
user: User;
|
|
@@ -35,7 +36,7 @@ export interface AuthProviderClass {
|
|
|
35
36
|
name: string;
|
|
36
37
|
type: string;
|
|
37
38
|
handleOauth?(credentials: Record<string, string>): Promise<string> | string;
|
|
38
|
-
handleSignIn(credentials: Record<string, string
|
|
39
|
+
handleSignIn(credentials: Record<string, string>, _req?: VattsRequest): Promise<User | string | null>;
|
|
39
40
|
handleSignOut?(): Promise<void>;
|
|
40
41
|
additionalRoutes?: AuthRoute[];
|
|
41
42
|
getConfig?(): any;
|
|
@@ -49,11 +50,10 @@ export interface AuthConfig {
|
|
|
49
50
|
};
|
|
50
51
|
callbacks?: {
|
|
51
52
|
signIn?: (user: User, account: any, profile: any) => boolean | Promise<boolean>;
|
|
52
|
-
|
|
53
|
-
session: Session;
|
|
53
|
+
user?: ({ user, provider }: {
|
|
54
54
|
user: User;
|
|
55
55
|
provider: string;
|
|
56
|
-
}) =>
|
|
56
|
+
}) => User | Promise<User>;
|
|
57
57
|
jwt?: (token: any, user: User, account: any, profile: any) => any | Promise<any>;
|
|
58
58
|
};
|
|
59
59
|
session?: {
|
package/dist/vue/component.vue
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
/**
|
|
3
|
-
* This file is part of the Vatts.js Project.
|
|
4
|
-
* Copyright (c) 2026 mfraz
|
|
5
|
-
*/
|
|
6
|
-
import { useSessionProviderLogic } from './session';
|
|
7
|
-
|
|
8
|
-
// Definição das props com valores padrão (Sintaxe JavaScript)
|
|
9
|
-
const props = defineProps({
|
|
10
|
-
basePath: {
|
|
11
|
-
type: String,
|
|
12
|
-
default: '/api/auth'
|
|
13
|
-
},
|
|
14
|
-
refetchInterval: {
|
|
15
|
-
type: Number,
|
|
16
|
-
default: 0
|
|
17
|
-
},
|
|
18
|
-
refetchOnWindowFocus: {
|
|
19
|
-
type: Boolean,
|
|
20
|
-
default: true
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// Inicializa a lógica do provider passando as props reativas
|
|
25
|
-
useSessionProviderLogic(props);
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<template>
|
|
29
|
-
<slot />
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* This file is part of the Vatts.js Project.
|
|
4
|
+
* Copyright (c) 2026 mfraz
|
|
5
|
+
*/
|
|
6
|
+
import { useSessionProviderLogic } from './session';
|
|
7
|
+
|
|
8
|
+
// Definição das props com valores padrão (Sintaxe JavaScript)
|
|
9
|
+
const props = defineProps({
|
|
10
|
+
basePath: {
|
|
11
|
+
type: String,
|
|
12
|
+
default: '/api/auth'
|
|
13
|
+
},
|
|
14
|
+
refetchInterval: {
|
|
15
|
+
type: Number,
|
|
16
|
+
default: 0
|
|
17
|
+
},
|
|
18
|
+
refetchOnWindowFocus: {
|
|
19
|
+
type: Boolean,
|
|
20
|
+
default: true
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Inicializa a lógica do provider passando as props reativas
|
|
25
|
+
useSessionProviderLogic(props);
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<slot />
|
|
30
30
|
</template>
|
package/dist/vue/session.d.ts
CHANGED
|
@@ -34,3 +34,11 @@ export declare function useAuth(): {
|
|
|
34
34
|
isAuthenticated: boolean;
|
|
35
35
|
isLoading: boolean;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Função utilitária para registrar uma nova Passkey (WebAuthn).
|
|
39
|
+
* Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
|
|
40
|
+
*/
|
|
41
|
+
export declare function registerPasskey(username: string, name?: string, basePath?: string): Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
error?: string;
|
|
44
|
+
}>;
|
package/dist/vue/session.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.SessionKey = void 0;
|
|
|
4
4
|
exports.useSessionProviderLogic = useSessionProviderLogic;
|
|
5
5
|
exports.useSession = useSession;
|
|
6
6
|
exports.useAuth = useAuth;
|
|
7
|
+
exports.registerPasskey = registerPasskey;
|
|
7
8
|
/*
|
|
8
9
|
* This file is part of the Vatts.js Project.
|
|
9
10
|
* Copyright (c) 2026 mfraz
|
|
@@ -128,6 +129,38 @@ function useSessionProviderLogic(props) {
|
|
|
128
129
|
const signIn = async (provider = 'credentials', options = {}) => {
|
|
129
130
|
try {
|
|
130
131
|
const { redirect = true, callbackUrl, popup = false, ...credentials } = options;
|
|
132
|
+
// --- INÍCIO DA INTERCEPTAÇÃO PASSKEYS ---
|
|
133
|
+
if (provider === 'passkeys') {
|
|
134
|
+
try {
|
|
135
|
+
// 1. Pede ao backend as opções de desafio para login (SEM username)
|
|
136
|
+
// Com discoverable credentials, o navegador mostra as keys disponíveis
|
|
137
|
+
const startRes = await fetch(`${props.basePath}/passkeys/login/start`, {
|
|
138
|
+
method: 'POST',
|
|
139
|
+
headers: { 'Content-Type': 'application/json' },
|
|
140
|
+
body: JSON.stringify({})
|
|
141
|
+
});
|
|
142
|
+
if (!startRes.ok) {
|
|
143
|
+
const errData = await startRes.json().catch(() => ({}));
|
|
144
|
+
return { error: errData.error || 'Failed to start passkey login', status: startRes.status, ok: false };
|
|
145
|
+
}
|
|
146
|
+
const { options, sessionId } = await startRes.json();
|
|
147
|
+
if (!sessionId) {
|
|
148
|
+
return { error: 'No session ID received from server', status: 500, ok: false };
|
|
149
|
+
}
|
|
150
|
+
// 2. Importa dinamicamente para não quebrar em SSR e chama o navegador (FaceID, TouchID, etc)
|
|
151
|
+
// O navegador mostrará as credenciais disponíveis automaticamente
|
|
152
|
+
const { startAuthentication } = await import('@simplewebauthn/browser');
|
|
153
|
+
const asseResp = await startAuthentication(options);
|
|
154
|
+
// 3. Injeta a resposta assinada e o sessionId no credentials para seguir o fluxo normal do signIn
|
|
155
|
+
credentials.response = JSON.stringify(asseResp);
|
|
156
|
+
credentials.sessionId = sessionId;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error('[vatts-auth] Error during passkey login flow:', error);
|
|
160
|
+
return { error: error.message || 'Passkey authentication failed', status: 400, ok: false };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// --- FIM DA INTERCEPTAÇÃO PASSKEYS ---
|
|
131
164
|
const response = await fetch(`${props.basePath}/signin`, {
|
|
132
165
|
method: 'POST',
|
|
133
166
|
headers: {
|
|
@@ -151,7 +184,7 @@ function useSessionProviderLogic(props) {
|
|
|
151
184
|
}
|
|
152
185
|
return { ok: true, status: 200, url: data.redirectUrl };
|
|
153
186
|
}
|
|
154
|
-
// Se é sessão (credentials), atualiza e redireciona
|
|
187
|
+
// Se é sessão (credentials/passkeys), atualiza e redireciona
|
|
155
188
|
await fetchSession();
|
|
156
189
|
if (data.type === 'session') {
|
|
157
190
|
const finalUrl = callbackUrl || '/';
|
|
@@ -276,3 +309,42 @@ function useAuth() {
|
|
|
276
309
|
isLoading: statusVal === 'loading'
|
|
277
310
|
};
|
|
278
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Função utilitária para registrar uma nova Passkey (WebAuthn).
|
|
314
|
+
* Pode ser usada em painéis de "Configurações de Conta" pelo desenvolvedor.
|
|
315
|
+
*/
|
|
316
|
+
async function registerPasskey(username, name, basePath = '/api/auth') {
|
|
317
|
+
try {
|
|
318
|
+
const { startRegistration } = await import('@simplewebauthn/browser');
|
|
319
|
+
// 1. Pega as opções do servidor
|
|
320
|
+
const startRes = await fetch(`${basePath}/passkeys/register/start`, {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
headers: { 'Content-Type': 'application/json' },
|
|
323
|
+
body: JSON.stringify({ username })
|
|
324
|
+
});
|
|
325
|
+
if (!startRes.ok) {
|
|
326
|
+
const errData = await startRes.json().catch(() => ({}));
|
|
327
|
+
throw new Error(errData.error || 'Failed to start passkey registration');
|
|
328
|
+
}
|
|
329
|
+
const options = await startRes.json();
|
|
330
|
+
// 2. Chama a biometria/PIN do dispositivo
|
|
331
|
+
const attResp = await startRegistration(options);
|
|
332
|
+
// 3. Envia o resultado de volta pro servidor validar e salvar
|
|
333
|
+
const finishRes = await fetch(`${basePath}/passkeys/register/finish`, {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
body: JSON.stringify({ username, response: attResp, name })
|
|
337
|
+
});
|
|
338
|
+
const result = await finishRes.json();
|
|
339
|
+
if (result.success) {
|
|
340
|
+
return { success: true };
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
return { success: false, error: result.error || 'Verification failed on server' };
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
console.error('[vatts-auth] Error during passkey registration:', error);
|
|
348
|
+
return { success: false, error: error.message || 'Passkey registration failed' };
|
|
349
|
+
}
|
|
350
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vatts/auth",
|
|
3
|
-
"version": "2.1.3-canary.1.0.
|
|
3
|
+
"version": "2.1.3-canary.1.0.4",
|
|
4
4
|
"description": "Authentication package for Vatts.js framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -50,13 +50,17 @@
|
|
|
50
50
|
"react": "^19.2.0",
|
|
51
51
|
"react-dom": "^19.2.0",
|
|
52
52
|
"vue": "^3.5.27",
|
|
53
|
-
"vatts": "^2.1.3-canary.1.0.
|
|
53
|
+
"vatts": "^2.1.3-canary.1.0.4"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"react-dom": {
|
|
57
57
|
"optional": true
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@simplewebauthn/browser": "^13.2.2",
|
|
62
|
+
"@simplewebauthn/server": "^13.2.3"
|
|
63
|
+
},
|
|
60
64
|
"scripts": {
|
|
61
65
|
"build": "tsc && copyfiles -u 1 \"src/**/*.vue\" dist",
|
|
62
66
|
"build:watch": "tsc --watch",
|