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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1b6ca556a3e04611cfc956e3d8861c05ce0933063f8a1a73c561caa5abcbb99
4
- data.tar.gz: 83ea6e86f56b59c7a6bcc0c4ea1504078f9900017ccd807a2a05da1cf73ca383
3
+ metadata.gz: 0bc0ba7fd7e24d51fa66e69af69a9765451d92dd8cb87d5a3a5c23e6734dc258
4
+ data.tar.gz: 32d5ac99eb08d923dda471be6cb66b08b5c6c660ff80998e3f3e9cb2132f367c
5
5
  SHA512:
6
- metadata.gz: 5383e6056c527e1d6ddf00a84b4acc5b00895ff478a823c5830fff053c4cf6281424f5bad3ea932de43f7b741474f54871913d07609f69995c46cb7f141b64b3
7
- data.tar.gz: 75d60921c7de4cd1d3a8b414e4339dc692a91a1c6c866b6e879a50b2f2ae1e135bace181bb43e86a3601b86709e041d46afb526430d50a600705d7741306a001
6
+ metadata.gz: ec4ec014d9a35ed4ee62ed3e65748d8f87e3fdc2f286cf34285ad6a9fd7a5d9ad5ed5ad48f40b95ddff9ca1805a044090d71c75033bdcef7d6692b286898e29d
7
+ data.tar.gz: 0f90a06468a408976910c81fc490124695be43e6f7c9e2d4e5b81efd6a553a026efef296e059d3ab02365ba74b42fdbc05457ad92774db0e51f9708dd0360c9b
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.10.0] - 2026-05-21
6
+
7
+ - Improved passkey challenge handling, management routes, rate limits, and adapter coverage.
8
+
5
9
  ## [0.7.0] - 2026-05-05
6
10
 
7
11
  - Require a fresh session for session-required passkey registration verification.
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` raises `UNAUTHORIZED` with the `PASSKEY_NOT_FOUND` message, mirroring upstream's `requireResourceOwnership` middleware behavior when only `notFoundError` is configured.
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 = ctx.context.adapter.find_one(model: "passkey", where: [{field: "credentialID", value: credential_id}])
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
- session = ctx.context.internal_adapter.create_session(passkey.fetch("userId"))
79
- raise APIError.new("INTERNAL_SERVER_ERROR", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("UNABLE_TO_CREATE_SESSION")) unless session
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(model: "passkey", where: [{field: "id", value: body[:id]}])
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.delete(model: "passkey", where: [{field: "id", value: passkey.fetch("id")}])
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(model: "passkey", where: [{field: "id", value: body[:id]}])
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: [{field: "id", value: body[:id]}],
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("INTERNAL_SERVER_ERROR", message: ErrorCodes::PASSKEY_ERROR_CODES.fetch("FAILED_TO_UPDATE_PASSKEY")) unless updated
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
- data = ctx.context.adapter.create(
86
- model: "passkey",
87
- data: {
88
- name: body[:name],
89
- userId: target_user_id,
90
- credentialID: credential.id,
91
- publicKey: Base64.strict_encode64(credential.public_key),
92
- counter: credential.sign_count,
93
- deviceType: authenticator_data&.credential_backup_eligible? ? "multiDevice" : "singleDevice",
94
- backedUp: authenticator_data&.credential_backed_up? || false,
95
- transports: Array(Credentials.attestation_response(credential)&.transports).join(","),
96
- createdAt: Time.now,
97
- aaguid: Credentials.attestation_response(credential)&.aaguid
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
- Array(origin || config[:origin] || ctx.context.options.base_url).compact
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.context.options.base_url.to_s
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Passkey
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  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.8.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