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.
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Passkey
5
- VERSION = "0.1.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -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