better_auth-passkey 0.1.0 → 0.5.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 +7 -0
- data/README.md +99 -1
- data/lib/better_auth/passkey/challenges.rb +46 -0
- data/lib/better_auth/passkey/credentials.rb +59 -0
- data/lib/better_auth/passkey/error_codes.rb +24 -0
- data/lib/better_auth/passkey/routes/authentication.rb +94 -0
- data/lib/better_auth/passkey/routes/management.rb +61 -0
- data/lib/better_auth/passkey/routes/registration.rb +93 -0
- data/lib/better_auth/passkey/routes.rb +5 -0
- data/lib/better_auth/passkey/schema.rb +57 -0
- data/lib/better_auth/passkey/utils.rb +174 -0
- data/lib/better_auth/passkey/version.rb +1 -1
- data/lib/better_auth/passkey.rb +6 -0
- data/lib/better_auth/plugins/passkey.rb +51 -369
- metadata +10 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "webauthn"
|
|
5
|
+
|
|
6
|
+
module BetterAuth
|
|
7
|
+
module Passkey
|
|
8
|
+
module Utils
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def normalize_hash(value)
|
|
12
|
+
BetterAuth::Passkey::Schema.normalize_hash(value)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def relying_party(config, ctx, origin: nil)
|
|
16
|
+
WebAuthn::RelyingParty.new(
|
|
17
|
+
id: rp_id(config, ctx),
|
|
18
|
+
name: config[:rp_name] || ctx.context.app_name,
|
|
19
|
+
allowed_origins: allowed_origins(config, ctx, origin: origin)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def origin(config, ctx)
|
|
24
|
+
config[:origin] || ctx.headers["origin"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def allowed_origins(config, ctx, origin: nil)
|
|
28
|
+
Array(origin || config[:origin] || ctx.context.options.base_url).compact
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def rp_id(config, ctx)
|
|
32
|
+
return config[:rp_id] if config[:rp_id]
|
|
33
|
+
|
|
34
|
+
base_url = ctx.context.options.base_url.to_s
|
|
35
|
+
return "localhost" if base_url.empty?
|
|
36
|
+
|
|
37
|
+
URI.parse(base_url).host || "localhost"
|
|
38
|
+
rescue URI::InvalidURIError
|
|
39
|
+
"localhost"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def authenticator_selection(config, query)
|
|
43
|
+
selection = normalize_hash(config[:authenticator_selection] || {})
|
|
44
|
+
attachment = query[:authenticator_attachment]
|
|
45
|
+
selection[:authenticator_attachment] = attachment if attachment
|
|
46
|
+
{
|
|
47
|
+
resident_key: selection[:resident_key] || "preferred",
|
|
48
|
+
user_verification: selection[:user_verification] || "preferred",
|
|
49
|
+
authenticator_attachment: selection[:authenticator_attachment]
|
|
50
|
+
}.compact
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_authenticator_attachment!(value)
|
|
54
|
+
return if value.nil? || ["platform", "cross-platform"].include?(value)
|
|
55
|
+
|
|
56
|
+
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("VALIDATION_ERROR"))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def require_key!(body, key)
|
|
60
|
+
return if body.key?(key)
|
|
61
|
+
|
|
62
|
+
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("VALIDATION_ERROR"))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def require_string!(body, key)
|
|
66
|
+
require_key!(body, key)
|
|
67
|
+
return if body[key].is_a?(String)
|
|
68
|
+
|
|
69
|
+
raise APIError.new("BAD_REQUEST", message: BASE_ERROR_CODES.fetch("VALIDATION_ERROR"))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def resolve_registration_user(config, ctx, query)
|
|
73
|
+
require_session = config.dig(:registration, :require_session) != false
|
|
74
|
+
if require_session
|
|
75
|
+
session = BetterAuth::Routes.current_session(ctx, allow_nil: true, sensitive: true, fresh: true)
|
|
76
|
+
unless session&.dig(:user, "id")
|
|
77
|
+
raise APIError.new("UNAUTHORIZED", message: BetterAuth::Passkey::ErrorCodes::PASSKEY_ERROR_CODES.fetch("SESSION_REQUIRED"))
|
|
78
|
+
end
|
|
79
|
+
user = session.fetch(:user)
|
|
80
|
+
return registration_user_data(
|
|
81
|
+
id: user.fetch("id"),
|
|
82
|
+
name: user["email"] || user["id"],
|
|
83
|
+
display_name: user["email"] || user["id"],
|
|
84
|
+
email: user["email"]
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
session = BetterAuth::Routes.current_session(ctx, allow_nil: true)
|
|
89
|
+
if session
|
|
90
|
+
user = session.fetch(:user)
|
|
91
|
+
return registration_user_data(
|
|
92
|
+
id: user.fetch("id"),
|
|
93
|
+
name: user["email"] || user["id"],
|
|
94
|
+
display_name: user["email"] || user["id"],
|
|
95
|
+
email: user["email"]
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
resolver = config.dig(:registration, :resolve_user)
|
|
100
|
+
unless resolver.respond_to?(:call)
|
|
101
|
+
raise APIError.new("BAD_REQUEST", message: BetterAuth::Passkey::ErrorCodes::PASSKEY_ERROR_CODES.fetch("RESOLVE_USER_REQUIRED"))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
resolved = normalize_hash(call_callback(resolver, {ctx: ctx, context: query[:context]}) || {})
|
|
105
|
+
unless resolved[:id].to_s != "" && resolved[:name].to_s != ""
|
|
106
|
+
raise APIError.new("BAD_REQUEST", message: BetterAuth::Passkey::ErrorCodes::PASSKEY_ERROR_CODES.fetch("RESOLVED_USER_INVALID"))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
registration_user_data(
|
|
110
|
+
id: resolved[:id],
|
|
111
|
+
name: resolved[:name],
|
|
112
|
+
display_name: resolved[:display_name],
|
|
113
|
+
email: resolved[:email]
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def registration_user_data(id:, name:, display_name: nil, email: nil)
|
|
118
|
+
{
|
|
119
|
+
"id" => id,
|
|
120
|
+
"name" => name,
|
|
121
|
+
"displayName" => display_name,
|
|
122
|
+
"email" => email
|
|
123
|
+
}.compact
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_extensions(extensions, ctx)
|
|
127
|
+
return nil unless extensions
|
|
128
|
+
|
|
129
|
+
normalize_hash(extensions.respond_to?(:call) ? call_callback(extensions, {ctx: ctx}) : extensions)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def after_registration_verification_user_id(config, ctx, credential, challenge, response, session)
|
|
133
|
+
user_data = challenge.fetch("userData")
|
|
134
|
+
target_user_id = user_data.fetch("id")
|
|
135
|
+
callback = config.dig(:registration, :after_verification)
|
|
136
|
+
return target_user_id unless callback.respond_to?(:call)
|
|
137
|
+
|
|
138
|
+
result = normalize_hash(call_callback(callback, {
|
|
139
|
+
ctx: ctx,
|
|
140
|
+
verification: credential,
|
|
141
|
+
user: {
|
|
142
|
+
id: user_data.fetch("id"),
|
|
143
|
+
name: user_data["name"] || user_data.fetch("id"),
|
|
144
|
+
display_name: user_data["displayName"] || user_data["display_name"]
|
|
145
|
+
},
|
|
146
|
+
client_data: response,
|
|
147
|
+
context: challenge["context"]
|
|
148
|
+
}) || {})
|
|
149
|
+
returned_user_id = result[:user_id]
|
|
150
|
+
return target_user_id if returned_user_id.nil? || returned_user_id == ""
|
|
151
|
+
|
|
152
|
+
unless returned_user_id.is_a?(String) && returned_user_id.length.positive?
|
|
153
|
+
raise APIError.new("BAD_REQUEST", message: BetterAuth::Passkey::ErrorCodes::PASSKEY_ERROR_CODES.fetch("RESOLVED_USER_INVALID"))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if session && returned_user_id != session.fetch(:user).fetch("id")
|
|
157
|
+
raise APIError.new("UNAUTHORIZED", message: BetterAuth::Passkey::ErrorCodes::PASSKEY_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_REGISTER_THIS_PASSKEY"))
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
returned_user_id
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def call_callback(callback, data)
|
|
164
|
+
return nil unless callback.respond_to?(:call)
|
|
165
|
+
|
|
166
|
+
if callback.parameters.any? { |kind, _name| [:key, :keyreq, :keyrest].include?(kind) }
|
|
167
|
+
callback.call(**data)
|
|
168
|
+
else
|
|
169
|
+
callback.call(data)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
data/lib/better_auth/passkey.rb
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require "better_auth"
|
|
4
4
|
require_relative "passkey/version"
|
|
5
|
+
require_relative "passkey/error_codes"
|
|
6
|
+
require_relative "passkey/schema"
|
|
7
|
+
require_relative "passkey/utils"
|
|
8
|
+
require_relative "passkey/challenges"
|
|
9
|
+
require_relative "passkey/credentials"
|
|
10
|
+
require_relative "passkey/routes"
|
|
5
11
|
require_relative "plugins/passkey"
|
|
6
12
|
|
|
7
13
|
module BetterAuth
|