better_auth-passkey 0.8.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +1 -1
- data/lib/better_auth/passkey/credentials.rb +11 -0
- data/lib/better_auth/passkey/routes/authentication.rb +16 -7
- data/lib/better_auth/passkey/routes/management.rb +31 -14
- data/lib/better_auth/passkey/routes/registration.rb +31 -17
- data/lib/better_auth/passkey/routes.rb +176 -0
- data/lib/better_auth/passkey/utils.rb +32 -2
- data/lib/better_auth/passkey/version.rb +1 -1
- metadata +76 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0bc0ba7fd7e24d51fa66e69af69a9765451d92dd8cb87d5a3a5c23e6734dc258
|
|
4
|
+
data.tar.gz: 32d5ac99eb08d923dda471be6cb66b08b5c6c660ff80998e3f3e9cb2132f367c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec4ec014d9a35ed4ee62ed3e65748d8f87e3fdc2f286cf34285ad6a9fd7a5d9ad5ed5ad48f40b95ddff9ca1805a044090d71c75033bdcef7d6692b286898e29d
|
|
7
|
+
data.tar.gz: 0f90a06468a408976910c81fc490124695be43e6f7c9e2d4e5b81efd6a553a026efef296e059d3ab02365ba74b42fdbc05457ad92774db0e51f9708dd0360c9b
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -127,7 +127,7 @@ The Ruby plugin tracks Better Auth `v1.6.9` upstream behavior. A few wire-shape
|
|
|
127
127
|
- `rp_id` resolution falls back to `URI.parse(base_url).host` (port stripped). When `base_url` is empty or unparseable, `rp_id` defaults to `"localhost"`.
|
|
128
128
|
- For passkey-first registration, the `after_verification` callback may return `{ user_id: nil }` or `{ user_id: "" }` to leave the resolved user unchanged. Returning any other non-empty-string value (integer, boolean, etc.) raises `RESOLVED_USER_INVALID`.
|
|
129
129
|
- `update_passkey` accepts an empty-string `name` to match upstream `z.string()`. Missing or non-string `name` still raises `VALIDATION_ERROR`.
|
|
130
|
-
- Cross-user `delete_passkey`
|
|
130
|
+
- Cross-user `delete_passkey` and `update_passkey` raise `NOT_FOUND` with the `PASSKEY_NOT_FOUND` message. This is an intentional Ruby hardening to avoid user/passkey enumeration while preserving the upstream error message.
|
|
131
131
|
- Existing databases should deduplicate historical `credential_id` values before adding the unique constraint during migration.
|
|
132
132
|
|
|
133
133
|
## Notes
|
|
@@ -47,6 +47,17 @@ module BetterAuth
|
|
|
47
47
|
record["credentialID"] || record["credentialId"] || record[:credentialID] || record[:credential_id]
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
def response_credential_id(response)
|
|
51
|
+
return nil unless response.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
response["id"] || response[:id]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def duplicate_credential_error?(error)
|
|
57
|
+
message = "#{error.class.name} #{error.message}".downcase
|
|
58
|
+
message.include?("credential") && (message.include?("unique") || message.include?("duplicate") || message.include?("constraint"))
|
|
59
|
+
end
|
|
60
|
+
|
|
50
61
|
def credential_descriptor(record, kind: :allow)
|
|
51
62
|
descriptor = {id: credential_id(record)}
|
|
52
63
|
descriptor[:type] = "public-key" if kind == :allow
|
|
@@ -10,7 +10,7 @@ module BetterAuth
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def generate_passkey_authentication_options_endpoint(config)
|
|
13
|
-
Endpoint.new(path: "/passkey/generate-authenticate-options", method: "GET") do |ctx|
|
|
13
|
+
Endpoint.new(path: "/passkey/generate-authenticate-options", method: "GET", metadata: Routes.openapi_for(:generate_authentication_options)) do |ctx|
|
|
14
14
|
session = BetterAuth::Routes.current_session(ctx, allow_nil: true)
|
|
15
15
|
relying_party = Utils.relying_party(config, ctx)
|
|
16
16
|
passkeys = if session
|
|
@@ -39,7 +39,7 @@ module BetterAuth
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def verify_passkey_authentication_endpoint(config)
|
|
42
|
-
Endpoint.new(path: "/passkey/verify-authentication", method: "POST") do |ctx|
|
|
42
|
+
Endpoint.new(path: "/passkey/verify-authentication", method: "POST", metadata: Routes.openapi_for(:verify_authentication)) do |ctx|
|
|
43
43
|
body = Utils.normalize_hash(ctx.body)
|
|
44
44
|
Utils.require_key!(body, :response)
|
|
45
45
|
origin = Utils.origin(config, ctx)
|
|
@@ -54,7 +54,12 @@ module BetterAuth
|
|
|
54
54
|
begin
|
|
55
55
|
response = Credentials.webauthn_response(body[:response])
|
|
56
56
|
credential_id = response.fetch("id")
|
|
57
|
-
passkey =
|
|
57
|
+
passkey = begin
|
|
58
|
+
ctx.context.adapter.find_one(model: "passkey", where: [{field: "credentialID", value: credential_id}])
|
|
59
|
+
rescue => error
|
|
60
|
+
ctx.context.logger&.error("Failed to find passkey", error)
|
|
61
|
+
raise APIError.new("BAD_REQUEST", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("AUTHENTICATION_FAILED"))
|
|
62
|
+
end
|
|
58
63
|
raise APIError.new("UNAUTHORIZED", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PASSKEY_NOT_FOUND")) unless passkey
|
|
59
64
|
|
|
60
65
|
relying_party = Utils.relying_party(config, ctx, origin: origin)
|
|
@@ -70,17 +75,21 @@ module BetterAuth
|
|
|
70
75
|
verification: credential,
|
|
71
76
|
client_data: response
|
|
72
77
|
})
|
|
73
|
-
ctx.context.adapter.update(
|
|
78
|
+
updated_passkey = ctx.context.adapter.update(
|
|
74
79
|
model: "passkey",
|
|
75
80
|
where: [{field: "id", value: passkey.fetch("id")}],
|
|
76
|
-
update: {counter: credential.sign_count}
|
|
81
|
+
update: {counter: credential.sign_count.to_i}
|
|
77
82
|
)
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
unless updated_passkey
|
|
84
|
+
raise APIError.new("BAD_REQUEST", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("AUTHENTICATION_FAILED"))
|
|
85
|
+
end
|
|
80
86
|
|
|
81
87
|
user = ctx.context.internal_adapter.find_user_by_id(passkey.fetch("userId"))
|
|
82
88
|
raise APIError.new("INTERNAL_SERVER_ERROR", message: "User not found") unless user
|
|
83
89
|
|
|
90
|
+
session = ctx.context.internal_adapter.create_session(passkey.fetch("userId"))
|
|
91
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("UNABLE_TO_CREATE_SESSION")) unless session
|
|
92
|
+
|
|
84
93
|
Cookies.set_session_cookie(ctx, {session: session, user: user})
|
|
85
94
|
ctx.context.internal_adapter.delete_verification_by_identifier(verification_token)
|
|
86
95
|
ctx.json({session: session, user: user})
|
|
@@ -7,7 +7,7 @@ module BetterAuth
|
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
9
|
def list_passkeys_endpoint
|
|
10
|
-
Endpoint.new(path: "/passkey/list-user-passkeys", method: "GET") do |ctx|
|
|
10
|
+
Endpoint.new(path: "/passkey/list-user-passkeys", method: "GET", metadata: Routes.openapi_for(:list_passkeys)) do |ctx|
|
|
11
11
|
session = BetterAuth::Routes.current_session(ctx)
|
|
12
12
|
passkeys = ctx.context.adapter.find_many(model: "passkey", where: [{field: "userId", value: session.fetch(:user).fetch("id")}])
|
|
13
13
|
ctx.json(passkeys.map { |passkey| Credentials.wire(passkey) })
|
|
@@ -15,23 +15,34 @@ module BetterAuth
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def delete_passkey_endpoint
|
|
18
|
-
Endpoint.new(path: "/passkey/delete-passkey", method: "POST") do |ctx|
|
|
18
|
+
Endpoint.new(path: "/passkey/delete-passkey", method: "POST", metadata: Routes.openapi_for(:delete_passkey)) do |ctx|
|
|
19
19
|
session = BetterAuth::Routes.current_session(ctx)
|
|
20
20
|
body = Utils.normalize_hash(ctx.body)
|
|
21
21
|
Utils.require_string!(body, :id)
|
|
22
|
-
passkey = ctx.context.adapter.find_one(
|
|
22
|
+
passkey = ctx.context.adapter.find_one(
|
|
23
|
+
model: "passkey",
|
|
24
|
+
where: [
|
|
25
|
+
{field: "id", value: body[:id]},
|
|
26
|
+
{field: "userId", value: session.fetch(:user).fetch("id")}
|
|
27
|
+
]
|
|
28
|
+
)
|
|
23
29
|
raise APIError.new("NOT_FOUND", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PASSKEY_NOT_FOUND")) unless passkey
|
|
24
|
-
unless passkey.fetch("userId") == session.fetch(:user).fetch("id")
|
|
25
|
-
raise APIError.new("UNAUTHORIZED", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PASSKEY_NOT_FOUND"))
|
|
26
|
-
end
|
|
27
30
|
|
|
28
|
-
ctx.context.adapter.
|
|
31
|
+
deleted = ctx.context.adapter.delete_many(
|
|
32
|
+
model: "passkey",
|
|
33
|
+
where: [
|
|
34
|
+
{field: "id", value: passkey.fetch("id")},
|
|
35
|
+
{field: "userId", value: session.fetch(:user).fetch("id")}
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
raise APIError.new("NOT_FOUND", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PASSKEY_NOT_FOUND")) if deleted.to_i.zero?
|
|
39
|
+
|
|
29
40
|
ctx.json({status: true})
|
|
30
41
|
end
|
|
31
42
|
end
|
|
32
43
|
|
|
33
44
|
def update_passkey_endpoint
|
|
34
|
-
Endpoint.new(path: "/passkey/update-passkey", method: "POST") do |ctx|
|
|
45
|
+
Endpoint.new(path: "/passkey/update-passkey", method: "POST", metadata: Routes.openapi_for(:update_passkey)) do |ctx|
|
|
35
46
|
session = BetterAuth::Routes.current_session(ctx)
|
|
36
47
|
body = Utils.normalize_hash(ctx.body)
|
|
37
48
|
Utils.require_string!(body, :id)
|
|
@@ -39,18 +50,24 @@ module BetterAuth
|
|
|
39
50
|
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("VALIDATION_ERROR"))
|
|
40
51
|
end
|
|
41
52
|
|
|
42
|
-
passkey = ctx.context.adapter.find_one(
|
|
53
|
+
passkey = ctx.context.adapter.find_one(
|
|
54
|
+
model: "passkey",
|
|
55
|
+
where: [
|
|
56
|
+
{field: "id", value: body[:id]},
|
|
57
|
+
{field: "userId", value: session.fetch(:user).fetch("id")}
|
|
58
|
+
]
|
|
59
|
+
)
|
|
43
60
|
raise APIError.new("NOT_FOUND", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PASSKEY_NOT_FOUND")) unless passkey
|
|
44
|
-
if passkey.fetch("userId") != session.fetch(:user).fetch("id")
|
|
45
|
-
raise APIError.new("UNAUTHORIZED", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY"))
|
|
46
|
-
end
|
|
47
61
|
|
|
48
62
|
updated = ctx.context.adapter.update(
|
|
49
63
|
model: "passkey",
|
|
50
|
-
where: [
|
|
64
|
+
where: [
|
|
65
|
+
{field: "id", value: body[:id]},
|
|
66
|
+
{field: "userId", value: session.fetch(:user).fetch("id")}
|
|
67
|
+
],
|
|
51
68
|
update: {name: body[:name].to_s}
|
|
52
69
|
)
|
|
53
|
-
raise APIError.new("
|
|
70
|
+
raise APIError.new("NOT_FOUND", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PASSKEY_NOT_FOUND")) unless updated
|
|
54
71
|
|
|
55
72
|
ctx.json({passkey: Credentials.wire(updated)})
|
|
56
73
|
end
|
|
@@ -10,7 +10,7 @@ module BetterAuth
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def generate_passkey_registration_options_endpoint(config)
|
|
13
|
-
Endpoint.new(path: "/passkey/generate-register-options", method: "GET") do |ctx|
|
|
13
|
+
Endpoint.new(path: "/passkey/generate-register-options", method: "GET", metadata: Routes.openapi_for(:generate_registration_options)) do |ctx|
|
|
14
14
|
query = Utils.normalize_hash(ctx.query)
|
|
15
15
|
Utils.validate_authenticator_attachment!(query[:authenticator_attachment])
|
|
16
16
|
user = Utils.resolve_registration_user(config, ctx, query)
|
|
@@ -40,7 +40,7 @@ module BetterAuth
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def verify_passkey_registration_endpoint(config)
|
|
43
|
-
Endpoint.new(path: "/passkey/verify-registration", method: "POST") do |ctx|
|
|
43
|
+
Endpoint.new(path: "/passkey/verify-registration", method: "POST", metadata: Routes.openapi_for(:verify_registration)) do |ctx|
|
|
44
44
|
body = Utils.normalize_hash(ctx.body)
|
|
45
45
|
Utils.require_key!(body, :response)
|
|
46
46
|
require_session = config.dig(:registration, :require_session) != false
|
|
@@ -62,6 +62,11 @@ module BetterAuth
|
|
|
62
62
|
|
|
63
63
|
begin
|
|
64
64
|
response = Credentials.webauthn_response(body[:response])
|
|
65
|
+
credential_id = Credentials.response_credential_id(response)
|
|
66
|
+
if credential_id && ctx.context.adapter.find_one(model: "passkey", where: [{field: "credentialID", value: credential_id}])
|
|
67
|
+
ctx.context.internal_adapter.delete_verification_by_identifier(verification_token)
|
|
68
|
+
raise APIError.new("BAD_REQUEST", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PREVIOUSLY_REGISTERED"))
|
|
69
|
+
end
|
|
65
70
|
relying_party = Utils.relying_party(config, ctx, origin: origin)
|
|
66
71
|
credential = WebAuthn::Credential.from_create(response, relying_party: relying_party)
|
|
67
72
|
credential.verify(challenge.fetch("expectedChallenge"), user_verification: false)
|
|
@@ -82,21 +87,30 @@ module BetterAuth
|
|
|
82
87
|
raise APIError.new("BAD_REQUEST", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PREVIOUSLY_REGISTERED"))
|
|
83
88
|
end
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
begin
|
|
91
|
+
data = ctx.context.adapter.create(
|
|
92
|
+
model: "passkey",
|
|
93
|
+
data: {
|
|
94
|
+
name: body[:name],
|
|
95
|
+
userId: target_user_id,
|
|
96
|
+
credentialID: credential.id,
|
|
97
|
+
publicKey: Base64.strict_encode64(credential.public_key),
|
|
98
|
+
counter: credential.sign_count.to_i,
|
|
99
|
+
deviceType: authenticator_data&.credential_backup_eligible? ? "multiDevice" : "singleDevice",
|
|
100
|
+
backedUp: authenticator_data&.credential_backed_up? || false,
|
|
101
|
+
transports: Array(Credentials.attestation_response(credential)&.transports).join(","),
|
|
102
|
+
createdAt: Time.now,
|
|
103
|
+
aaguid: Credentials.attestation_response(credential)&.aaguid
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
rescue => error
|
|
107
|
+
ctx.context.internal_adapter.delete_verification_by_identifier(verification_token)
|
|
108
|
+
if Credentials.duplicate_credential_error?(error)
|
|
109
|
+
raise APIError.new("BAD_REQUEST", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("PREVIOUSLY_REGISTERED"))
|
|
110
|
+
end
|
|
111
|
+
ctx.context.logger&.error("Failed to create passkey", error)
|
|
112
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("FAILED_TO_VERIFY_REGISTRATION"))
|
|
113
|
+
end
|
|
100
114
|
ctx.context.internal_adapter.delete_verification_by_identifier(verification_token)
|
|
101
115
|
ctx.json(Credentials.wire(data))
|
|
102
116
|
rescue APIError
|
|
@@ -1,5 +1,181 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
module BetterAuth
|
|
4
|
+
module Passkey
|
|
5
|
+
module Routes
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def openapi_for(route)
|
|
9
|
+
{
|
|
10
|
+
generate_registration_options: generate_registration_options_openapi,
|
|
11
|
+
verify_registration: verify_registration_openapi,
|
|
12
|
+
generate_authentication_options: generate_authentication_options_openapi,
|
|
13
|
+
verify_authentication: verify_authentication_openapi,
|
|
14
|
+
list_passkeys: list_passkeys_openapi,
|
|
15
|
+
delete_passkey: delete_passkey_openapi,
|
|
16
|
+
update_passkey: update_passkey_openapi
|
|
17
|
+
}.fetch(route)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def generate_registration_options_openapi
|
|
21
|
+
{
|
|
22
|
+
openapi: {
|
|
23
|
+
operationId: "generatePasskeyRegistrationOptions",
|
|
24
|
+
description: "Generate registration options for a new passkey",
|
|
25
|
+
parameters: [
|
|
26
|
+
BetterAuth::OpenAPI.query_parameter("authenticatorAttachment", schema: {type: "string", enum: ["platform", "cross-platform"]}, description: "Type of authenticator to use for registration"),
|
|
27
|
+
BetterAuth::OpenAPI.query_parameter("name", description: "Optional custom name for the passkey"),
|
|
28
|
+
BetterAuth::OpenAPI.query_parameter("context", description: "Optional context for passkey-first registration flows")
|
|
29
|
+
],
|
|
30
|
+
responses: {
|
|
31
|
+
"200" => BetterAuth::OpenAPI.json_response("Success", passkey_options_schema)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def verify_registration_openapi
|
|
38
|
+
{
|
|
39
|
+
openapi: {
|
|
40
|
+
operationId: "passkeyVerifyRegistration",
|
|
41
|
+
description: "Verify registration of a new passkey",
|
|
42
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(
|
|
43
|
+
BetterAuth::OpenAPI.object_schema(
|
|
44
|
+
{
|
|
45
|
+
response: {type: "object", additionalProperties: true, description: "WebAuthn registration response"},
|
|
46
|
+
name: {type: "string", description: "Name of the passkey"}
|
|
47
|
+
},
|
|
48
|
+
required: ["response"]
|
|
49
|
+
)
|
|
50
|
+
),
|
|
51
|
+
responses: {
|
|
52
|
+
"200" => BetterAuth::OpenAPI.json_response("Success", BetterAuth::OpenAPI.ref_schema("Passkey")),
|
|
53
|
+
"400" => {description: "Bad request"}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def generate_authentication_options_openapi
|
|
60
|
+
{
|
|
61
|
+
openapi: {
|
|
62
|
+
operationId: "passkeyGenerateAuthenticateOptions",
|
|
63
|
+
description: "Generate authentication options for a passkey",
|
|
64
|
+
responses: {
|
|
65
|
+
"200" => BetterAuth::OpenAPI.json_response("Success", passkey_options_schema)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def verify_authentication_openapi
|
|
72
|
+
{
|
|
73
|
+
openapi: {
|
|
74
|
+
operationId: "passkeyVerifyAuthentication",
|
|
75
|
+
description: "Verify authentication of a passkey",
|
|
76
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(
|
|
77
|
+
BetterAuth::OpenAPI.object_schema(
|
|
78
|
+
{
|
|
79
|
+
response: {type: "object", additionalProperties: true, description: "WebAuthn authentication response"}
|
|
80
|
+
},
|
|
81
|
+
required: ["response"]
|
|
82
|
+
)
|
|
83
|
+
),
|
|
84
|
+
responses: {
|
|
85
|
+
"200" => BetterAuth::OpenAPI.json_response("Success", BetterAuth::OpenAPI.session_response_schema_pair)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def list_passkeys_openapi
|
|
92
|
+
{
|
|
93
|
+
openapi: {
|
|
94
|
+
description: "List all passkeys for the authenticated user",
|
|
95
|
+
responses: {
|
|
96
|
+
"200" => BetterAuth::OpenAPI.json_response(
|
|
97
|
+
"Passkeys retrieved successfully",
|
|
98
|
+
BetterAuth::OpenAPI.array_schema(BetterAuth::OpenAPI.ref_schema("Passkey"))
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def delete_passkey_openapi
|
|
106
|
+
{
|
|
107
|
+
openapi: {
|
|
108
|
+
description: "Delete a specific passkey",
|
|
109
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(passkey_id_body_schema),
|
|
110
|
+
responses: {
|
|
111
|
+
"200" => BetterAuth::OpenAPI.json_response("Passkey deleted successfully", BetterAuth::OpenAPI.status_response_schema)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def update_passkey_openapi
|
|
118
|
+
{
|
|
119
|
+
openapi: {
|
|
120
|
+
description: "Update a specific passkey's name",
|
|
121
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(
|
|
122
|
+
BetterAuth::OpenAPI.object_schema(
|
|
123
|
+
{
|
|
124
|
+
id: {type: "string", description: "The ID of the passkey which will be updated"},
|
|
125
|
+
name: {type: "string", description: "The new passkey name"}
|
|
126
|
+
},
|
|
127
|
+
required: ["id", "name"]
|
|
128
|
+
)
|
|
129
|
+
),
|
|
130
|
+
responses: {
|
|
131
|
+
"200" => BetterAuth::OpenAPI.json_response(
|
|
132
|
+
"Passkey updated successfully",
|
|
133
|
+
BetterAuth::OpenAPI.object_schema(
|
|
134
|
+
{
|
|
135
|
+
passkey: BetterAuth::OpenAPI.ref_schema("Passkey")
|
|
136
|
+
},
|
|
137
|
+
required: ["passkey"]
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def passkey_id_body_schema
|
|
146
|
+
BetterAuth::OpenAPI.object_schema(
|
|
147
|
+
{
|
|
148
|
+
id: {type: "string", description: "The ID of the passkey"}
|
|
149
|
+
},
|
|
150
|
+
required: ["id"]
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def passkey_options_schema
|
|
155
|
+
BetterAuth::OpenAPI.object_schema(
|
|
156
|
+
{
|
|
157
|
+
challenge: {type: "string"},
|
|
158
|
+
rp: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
name: {type: "string"},
|
|
162
|
+
id: {type: "string"}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
user: {type: "object", additionalProperties: true},
|
|
166
|
+
timeout: {type: "number"},
|
|
167
|
+
attestation: {type: "string"},
|
|
168
|
+
excludeCredentials: {type: "array", items: {type: "object", additionalProperties: true}},
|
|
169
|
+
allowCredentials: {type: "array", items: {type: "object", additionalProperties: true}},
|
|
170
|
+
userVerification: {type: "string"},
|
|
171
|
+
extensions: {type: "object", additionalProperties: true}
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
3
179
|
require_relative "routes/registration"
|
|
4
180
|
require_relative "routes/authentication"
|
|
5
181
|
require_relative "routes/management"
|
|
@@ -25,17 +25,22 @@ module BetterAuth
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def allowed_origins(config, ctx, origin: nil)
|
|
28
|
-
|
|
28
|
+
_request_origin = origin
|
|
29
|
+
configured = config.key?(:origin) ? config[:origin] : nil
|
|
30
|
+
origins = configured || context_base_url(ctx)
|
|
31
|
+
Array(origins).compact.map { |value| origin_for(value) }
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def rp_id(config, ctx)
|
|
32
35
|
return config[:rp_id] if config[:rp_id]
|
|
33
36
|
|
|
34
|
-
base_url = ctx.
|
|
37
|
+
base_url = context_base_url(ctx).to_s
|
|
35
38
|
return "localhost" if base_url.empty?
|
|
36
39
|
|
|
37
40
|
URI.parse(base_url).host || "localhost"
|
|
38
41
|
rescue URI::InvalidURIError
|
|
42
|
+
raise APIError.new("BAD_REQUEST", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("FAILED_TO_VERIFY_REGISTRATION")) if strict_base_url?(ctx)
|
|
43
|
+
|
|
39
44
|
"localhost"
|
|
40
45
|
end
|
|
41
46
|
|
|
@@ -169,6 +174,31 @@ module BetterAuth
|
|
|
169
174
|
callback.call(data)
|
|
170
175
|
end
|
|
171
176
|
end
|
|
177
|
+
|
|
178
|
+
def context_base_url(ctx)
|
|
179
|
+
if ctx.context.respond_to?(:base_url)
|
|
180
|
+
ctx.context.base_url
|
|
181
|
+
else
|
|
182
|
+
ctx.context.options.base_url
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def strict_base_url?(ctx)
|
|
187
|
+
return ctx.context.passkey_strict_base_url? if ctx.context.respond_to?(:passkey_strict_base_url?)
|
|
188
|
+
|
|
189
|
+
true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def origin_for(value)
|
|
193
|
+
uri = URI.parse(value.to_s)
|
|
194
|
+
if uri.scheme && uri.host
|
|
195
|
+
BetterAuth::Configuration.origin_for(uri) || value.to_s
|
|
196
|
+
else
|
|
197
|
+
value.to_s
|
|
198
|
+
end
|
|
199
|
+
rescue URI::InvalidURIError
|
|
200
|
+
value.to_s
|
|
201
|
+
end
|
|
172
202
|
end
|
|
173
203
|
end
|
|
174
204
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_auth-passkey
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -91,6 +91,34 @@ dependencies:
|
|
|
91
91
|
- - "~>"
|
|
92
92
|
- !ruby/object:Gem::Version
|
|
93
93
|
version: '5.25'
|
|
94
|
+
- !ruby/object:Gem::Dependency
|
|
95
|
+
name: mysql2
|
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - "~>"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0.5'
|
|
101
|
+
type: :development
|
|
102
|
+
prerelease: false
|
|
103
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - "~>"
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '0.5'
|
|
108
|
+
- !ruby/object:Gem::Dependency
|
|
109
|
+
name: pg
|
|
110
|
+
requirement: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - "~>"
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '1.5'
|
|
115
|
+
type: :development
|
|
116
|
+
prerelease: false
|
|
117
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
118
|
+
requirements:
|
|
119
|
+
- - "~>"
|
|
120
|
+
- !ruby/object:Gem::Version
|
|
121
|
+
version: '1.5'
|
|
94
122
|
- !ruby/object:Gem::Dependency
|
|
95
123
|
name: rake
|
|
96
124
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,6 +133,34 @@ dependencies:
|
|
|
105
133
|
- - "~>"
|
|
106
134
|
- !ruby/object:Gem::Version
|
|
107
135
|
version: '13.2'
|
|
136
|
+
- !ruby/object:Gem::Dependency
|
|
137
|
+
name: sequel
|
|
138
|
+
requirement: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - "~>"
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '5.83'
|
|
143
|
+
type: :development
|
|
144
|
+
prerelease: false
|
|
145
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - "~>"
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: '5.83'
|
|
150
|
+
- !ruby/object:Gem::Dependency
|
|
151
|
+
name: sqlite3
|
|
152
|
+
requirement: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - "~>"
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: '2.0'
|
|
157
|
+
type: :development
|
|
158
|
+
prerelease: false
|
|
159
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
160
|
+
requirements:
|
|
161
|
+
- - "~>"
|
|
162
|
+
- !ruby/object:Gem::Version
|
|
163
|
+
version: '2.0'
|
|
108
164
|
- !ruby/object:Gem::Dependency
|
|
109
165
|
name: standardrb
|
|
110
166
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -119,6 +175,20 @@ dependencies:
|
|
|
119
175
|
- - "~>"
|
|
120
176
|
- !ruby/object:Gem::Version
|
|
121
177
|
version: '1.0'
|
|
178
|
+
- !ruby/object:Gem::Dependency
|
|
179
|
+
name: tiny_tds
|
|
180
|
+
requirement: !ruby/object:Gem::Requirement
|
|
181
|
+
requirements:
|
|
182
|
+
- - "~>"
|
|
183
|
+
- !ruby/object:Gem::Version
|
|
184
|
+
version: '2.1'
|
|
185
|
+
type: :development
|
|
186
|
+
prerelease: false
|
|
187
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
188
|
+
requirements:
|
|
189
|
+
- - "~>"
|
|
190
|
+
- !ruby/object:Gem::Version
|
|
191
|
+
version: '2.1'
|
|
122
192
|
description: Adds passkey/WebAuthn registration, authentication, and credential management
|
|
123
193
|
routes for Better Auth Ruby. Better Auth Ruby is an independent modern authentication
|
|
124
194
|
framework for Ruby inspired by Better Auth.
|
|
@@ -142,14 +212,14 @@ files:
|
|
|
142
212
|
- lib/better_auth/passkey/utils.rb
|
|
143
213
|
- lib/better_auth/passkey/version.rb
|
|
144
214
|
- lib/better_auth/plugins/passkey.rb
|
|
145
|
-
homepage: https://github.com/sebasxsala/better-auth
|
|
215
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
146
216
|
licenses:
|
|
147
217
|
- MIT
|
|
148
218
|
metadata:
|
|
149
|
-
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
150
|
-
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
151
|
-
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-passkey/CHANGELOG.md
|
|
152
|
-
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
219
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
220
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
221
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-passkey/CHANGELOG.md
|
|
222
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
153
223
|
rdoc_options: []
|
|
154
224
|
require_paths:
|
|
155
225
|
- lib
|