better_auth 0.1.1 → 0.3.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 +23 -0
- data/README.md +110 -18
- data/lib/better_auth/adapters/base.rb +49 -0
- data/lib/better_auth/adapters/internal_adapter.rb +589 -0
- data/lib/better_auth/adapters/memory.rb +235 -0
- data/lib/better_auth/adapters/mongodb.rb +9 -0
- data/lib/better_auth/adapters/mssql.rb +42 -0
- data/lib/better_auth/adapters/mysql.rb +33 -0
- data/lib/better_auth/adapters/postgres.rb +17 -0
- data/lib/better_auth/adapters/sql.rb +441 -0
- data/lib/better_auth/adapters/sqlite.rb +20 -0
- data/lib/better_auth/api.rb +226 -0
- data/lib/better_auth/api_error.rb +53 -0
- data/lib/better_auth/auth.rb +42 -0
- data/lib/better_auth/configuration.rb +399 -0
- data/lib/better_auth/context.rb +211 -0
- data/lib/better_auth/cookies.rb +278 -0
- data/lib/better_auth/core.rb +37 -1
- data/lib/better_auth/crypto/jwe.rb +76 -0
- data/lib/better_auth/crypto.rb +191 -0
- data/lib/better_auth/database_hooks.rb +114 -0
- data/lib/better_auth/endpoint.rb +326 -0
- data/lib/better_auth/error.rb +52 -0
- data/lib/better_auth/middleware/origin_check.rb +128 -0
- data/lib/better_auth/password.rb +120 -0
- data/lib/better_auth/plugin.rb +142 -0
- data/lib/better_auth/plugin_context.rb +16 -0
- data/lib/better_auth/plugin_registry.rb +67 -0
- data/lib/better_auth/plugins/access.rb +87 -0
- data/lib/better_auth/plugins/additional_fields.rb +29 -0
- data/lib/better_auth/plugins/admin/schema.rb +28 -0
- data/lib/better_auth/plugins/admin.rb +518 -0
- data/lib/better_auth/plugins/anonymous.rb +198 -0
- data/lib/better_auth/plugins/api_key.rb +16 -0
- data/lib/better_auth/plugins/bearer.rb +128 -0
- data/lib/better_auth/plugins/captcha.rb +159 -0
- data/lib/better_auth/plugins/custom_session.rb +84 -0
- data/lib/better_auth/plugins/device_authorization.rb +302 -0
- data/lib/better_auth/plugins/email_otp.rb +536 -0
- data/lib/better_auth/plugins/expo.rb +88 -0
- data/lib/better_auth/plugins/generic_oauth.rb +780 -0
- data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
- data/lib/better_auth/plugins/jwt.rb +482 -0
- data/lib/better_auth/plugins/last_login_method.rb +92 -0
- data/lib/better_auth/plugins/magic_link.rb +181 -0
- data/lib/better_auth/plugins/mcp.rb +342 -0
- data/lib/better_auth/plugins/multi_session.rb +173 -0
- data/lib/better_auth/plugins/oauth_protocol.rb +694 -0
- data/lib/better_auth/plugins/oauth_provider.rb +16 -0
- data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
- data/lib/better_auth/plugins/oidc_provider.rb +597 -0
- data/lib/better_auth/plugins/one_tap.rb +154 -0
- data/lib/better_auth/plugins/one_time_token.rb +106 -0
- data/lib/better_auth/plugins/open_api.rb +489 -0
- data/lib/better_auth/plugins/organization/schema.rb +106 -0
- data/lib/better_auth/plugins/organization.rb +995 -0
- data/lib/better_auth/plugins/passkey.rb +16 -0
- data/lib/better_auth/plugins/phone_number.rb +321 -0
- data/lib/better_auth/plugins/scim.rb +16 -0
- data/lib/better_auth/plugins/siwe.rb +242 -0
- data/lib/better_auth/plugins/sso.rb +16 -0
- data/lib/better_auth/plugins/stripe.rb +16 -0
- data/lib/better_auth/plugins/two_factor.rb +514 -0
- data/lib/better_auth/plugins/username.rb +278 -0
- data/lib/better_auth/plugins.rb +46 -0
- data/lib/better_auth/rate_limiter.rb +232 -0
- data/lib/better_auth/request_ip.rb +70 -0
- data/lib/better_auth/router.rb +378 -0
- data/lib/better_auth/routes/account.rb +211 -0
- data/lib/better_auth/routes/email_verification.rb +111 -0
- data/lib/better_auth/routes/error.rb +102 -0
- data/lib/better_auth/routes/ok.rb +15 -0
- data/lib/better_auth/routes/password.rb +183 -0
- data/lib/better_auth/routes/session.rb +160 -0
- data/lib/better_auth/routes/sign_in.rb +90 -0
- data/lib/better_auth/routes/sign_out.rb +15 -0
- data/lib/better_auth/routes/sign_up.rb +196 -0
- data/lib/better_auth/routes/social.rb +367 -0
- data/lib/better_auth/routes/user.rb +205 -0
- data/lib/better_auth/schema/sql.rb +202 -0
- data/lib/better_auth/schema.rb +291 -0
- data/lib/better_auth/session.rb +122 -0
- data/lib/better_auth/session_store.rb +91 -0
- data/lib/better_auth/social_providers/apple.rb +91 -0
- data/lib/better_auth/social_providers/atlassian.rb +32 -0
- data/lib/better_auth/social_providers/base.rb +325 -0
- data/lib/better_auth/social_providers/cognito.rb +32 -0
- data/lib/better_auth/social_providers/discord.rb +81 -0
- data/lib/better_auth/social_providers/dropbox.rb +33 -0
- data/lib/better_auth/social_providers/facebook.rb +35 -0
- data/lib/better_auth/social_providers/figma.rb +31 -0
- data/lib/better_auth/social_providers/github.rb +74 -0
- data/lib/better_auth/social_providers/gitlab.rb +67 -0
- data/lib/better_auth/social_providers/google.rb +90 -0
- data/lib/better_auth/social_providers/huggingface.rb +31 -0
- data/lib/better_auth/social_providers/kakao.rb +32 -0
- data/lib/better_auth/social_providers/kick.rb +32 -0
- data/lib/better_auth/social_providers/line.rb +33 -0
- data/lib/better_auth/social_providers/linear.rb +44 -0
- data/lib/better_auth/social_providers/linkedin.rb +30 -0
- data/lib/better_auth/social_providers/microsoft_entra_id.rb +137 -0
- data/lib/better_auth/social_providers/naver.rb +31 -0
- data/lib/better_auth/social_providers/notion.rb +33 -0
- data/lib/better_auth/social_providers/paybin.rb +31 -0
- data/lib/better_auth/social_providers/paypal.rb +36 -0
- data/lib/better_auth/social_providers/polar.rb +31 -0
- data/lib/better_auth/social_providers/railway.rb +49 -0
- data/lib/better_auth/social_providers/reddit.rb +32 -0
- data/lib/better_auth/social_providers/roblox.rb +31 -0
- data/lib/better_auth/social_providers/salesforce.rb +38 -0
- data/lib/better_auth/social_providers/slack.rb +30 -0
- data/lib/better_auth/social_providers/spotify.rb +31 -0
- data/lib/better_auth/social_providers/tiktok.rb +35 -0
- data/lib/better_auth/social_providers/twitch.rb +39 -0
- data/lib/better_auth/social_providers/twitter.rb +32 -0
- data/lib/better_auth/social_providers/vercel.rb +47 -0
- data/lib/better_auth/social_providers/vk.rb +34 -0
- data/lib/better_auth/social_providers/wechat.rb +104 -0
- data/lib/better_auth/social_providers/zoom.rb +31 -0
- data/lib/better_auth/social_providers.rb +38 -0
- data/lib/better_auth/version.rb +1 -1
- data/lib/better_auth.rb +86 -2
- metadata +233 -21
- data/.ruby-version +0 -1
- data/.standard.yml +0 -12
- data/.vscode/settings.json +0 -22
- data/AGENTS.md +0 -50
- data/CLAUDE.md +0 -1
- data/CODE_OF_CONDUCT.md +0 -173
- data/CONTRIBUTING.md +0 -187
- data/Gemfile +0 -12
- data/Makefile +0 -207
- data/Rakefile +0 -25
- data/SECURITY.md +0 -28
- data/docker-compose.yml +0 -63
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module BetterAuth
|
|
7
|
+
module Plugins
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
DEVICE_AUTHORIZATION_ERROR_CODES = {
|
|
11
|
+
"INVALID_DEVICE_CODE" => "Invalid device code",
|
|
12
|
+
"EXPIRED_DEVICE_CODE" => "Device code has expired",
|
|
13
|
+
"EXPIRED_USER_CODE" => "User code has expired",
|
|
14
|
+
"AUTHORIZATION_PENDING" => "Authorization pending",
|
|
15
|
+
"ACCESS_DENIED" => "Access denied",
|
|
16
|
+
"INVALID_USER_CODE" => "Invalid user code",
|
|
17
|
+
"DEVICE_CODE_ALREADY_PROCESSED" => "Device code already processed",
|
|
18
|
+
"POLLING_TOO_FREQUENTLY" => "Polling too frequently",
|
|
19
|
+
"USER_NOT_FOUND" => "User not found",
|
|
20
|
+
"FAILED_TO_CREATE_SESSION" => "Failed to create session",
|
|
21
|
+
"INVALID_DEVICE_CODE_STATUS" => "Invalid device code status",
|
|
22
|
+
"AUTHENTICATION_REQUIRED" => "Authentication required"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
def device_authorization(options = {})
|
|
26
|
+
config = {
|
|
27
|
+
expires_in: "30m",
|
|
28
|
+
interval: "5s",
|
|
29
|
+
device_code_length: 40,
|
|
30
|
+
user_code_length: 8
|
|
31
|
+
}.merge(normalize_hash(options))
|
|
32
|
+
validate_device_authorization_options!(config)
|
|
33
|
+
|
|
34
|
+
Plugin.new(
|
|
35
|
+
id: "device-authorization",
|
|
36
|
+
endpoints: {
|
|
37
|
+
device_code: device_code_endpoint(config),
|
|
38
|
+
device_token: device_token_endpoint(config),
|
|
39
|
+
device_verify: device_verify_endpoint,
|
|
40
|
+
device_approve: device_approve_endpoint,
|
|
41
|
+
device_deny: device_deny_endpoint
|
|
42
|
+
},
|
|
43
|
+
schema: device_authorization_schema(config[:schema]),
|
|
44
|
+
error_codes: DEVICE_AUTHORIZATION_ERROR_CODES,
|
|
45
|
+
options: config
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def device_code_endpoint(config)
|
|
50
|
+
Endpoint.new(path: "/device/code", method: "POST") do |ctx|
|
|
51
|
+
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
52
|
+
client_id = body["client_id"]
|
|
53
|
+
if config[:validate_client] && !config[:validate_client].call(client_id)
|
|
54
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_client", "Invalid client ID")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
config[:on_device_auth_request].call(client_id, body["scope"]) if config[:on_device_auth_request].respond_to?(:call)
|
|
58
|
+
|
|
59
|
+
device_code = generate_device_authorization_device_code(config)
|
|
60
|
+
user_code = generate_device_authorization_user_code(config)
|
|
61
|
+
expires_in = duration_seconds(config[:expires_in])
|
|
62
|
+
interval = duration_seconds(config[:interval])
|
|
63
|
+
ctx.context.adapter.create(
|
|
64
|
+
model: "deviceCode",
|
|
65
|
+
data: {
|
|
66
|
+
"deviceCode" => device_code,
|
|
67
|
+
"userCode" => user_code,
|
|
68
|
+
"expiresAt" => Time.now + expires_in,
|
|
69
|
+
"status" => "pending",
|
|
70
|
+
"pollingInterval" => interval * 1000,
|
|
71
|
+
"clientId" => client_id,
|
|
72
|
+
"scope" => body["scope"]
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
verification_uri = verification_uri(ctx, config)
|
|
76
|
+
complete = OAuthProtocol.redirect_uri_with_params(verification_uri, user_code: user_code)
|
|
77
|
+
ctx.json({
|
|
78
|
+
device_code: device_code,
|
|
79
|
+
user_code: user_code,
|
|
80
|
+
verification_uri: verification_uri,
|
|
81
|
+
verification_uri_complete: complete,
|
|
82
|
+
expires_in: expires_in,
|
|
83
|
+
interval: interval
|
|
84
|
+
}, headers: {"Cache-Control" => "no-store"})
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def device_token_endpoint(config)
|
|
89
|
+
Endpoint.new(path: "/device/token", method: "POST", metadata: {allowed_media_types: ["application/x-www-form-urlencoded", "application/json"]}) do |ctx|
|
|
90
|
+
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
91
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_request", "Unsupported grant type") unless body["grant_type"] == OAuthProtocol::DEVICE_CODE_GRANT
|
|
92
|
+
if config[:validate_client] && !config[:validate_client].call(body["client_id"])
|
|
93
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_grant", "Invalid client ID")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
record = find_device_code(ctx, body["device_code"])
|
|
97
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_grant", DEVICE_AUTHORIZATION_ERROR_CODES["INVALID_DEVICE_CODE"]) unless record
|
|
98
|
+
record = OAuthProtocol.stringify_keys(record)
|
|
99
|
+
if record["clientId"] && record["clientId"] != body["client_id"]
|
|
100
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_grant", "Client ID mismatch")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if record["lastPolledAt"] && record["pollingInterval"].to_i.positive?
|
|
104
|
+
elapsed = ((Time.now - device_authorization_time(record["lastPolledAt"])) * 1000).to_i
|
|
105
|
+
raise device_authorization_error("BAD_REQUEST", "slow_down", DEVICE_AUTHORIZATION_ERROR_CODES["POLLING_TOO_FREQUENTLY"]) if elapsed < record["pollingInterval"].to_i
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
ctx.context.adapter.update(model: "deviceCode", where: [{field: "id", value: record["id"]}], update: {"lastPolledAt" => Time.now})
|
|
109
|
+
|
|
110
|
+
if device_authorization_time(record["expiresAt"]) <= Time.now
|
|
111
|
+
ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
|
|
112
|
+
raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_DEVICE_CODE"])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
case record["status"]
|
|
116
|
+
when "pending"
|
|
117
|
+
raise device_authorization_error("BAD_REQUEST", "authorization_pending", DEVICE_AUTHORIZATION_ERROR_CODES["AUTHORIZATION_PENDING"])
|
|
118
|
+
when "denied"
|
|
119
|
+
ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
|
|
120
|
+
raise device_authorization_error("BAD_REQUEST", "access_denied", DEVICE_AUTHORIZATION_ERROR_CODES["ACCESS_DENIED"])
|
|
121
|
+
when "approved"
|
|
122
|
+
user = ctx.context.internal_adapter.find_user_by_id(record["userId"])
|
|
123
|
+
raise device_authorization_error("INTERNAL_SERVER_ERROR", "server_error", DEVICE_AUTHORIZATION_ERROR_CODES["USER_NOT_FOUND"]) unless user
|
|
124
|
+
|
|
125
|
+
session = ctx.context.internal_adapter.create_session(user["id"])
|
|
126
|
+
raise device_authorization_error("INTERNAL_SERVER_ERROR", "server_error", DEVICE_AUTHORIZATION_ERROR_CODES["FAILED_TO_CREATE_SESSION"]) unless session
|
|
127
|
+
|
|
128
|
+
session_data = {session: session, user: user}
|
|
129
|
+
ctx.context.set_new_session(session_data) if ctx.context.respond_to?(:set_new_session)
|
|
130
|
+
ctx.context.adapter.delete(model: "deviceCode", where: [{field: "id", value: record["id"]}])
|
|
131
|
+
ctx.json({
|
|
132
|
+
access_token: session["token"],
|
|
133
|
+
token_type: "Bearer",
|
|
134
|
+
expires_in: [session["expiresAt"].to_i - Time.now.to_i, 0].max,
|
|
135
|
+
scope: record["scope"].to_s
|
|
136
|
+
}, headers: {"Cache-Control" => "no-store", "Pragma" => "no-cache"})
|
|
137
|
+
else
|
|
138
|
+
raise device_authorization_error("INTERNAL_SERVER_ERROR", "server_error", DEVICE_AUTHORIZATION_ERROR_CODES["INVALID_DEVICE_CODE_STATUS"])
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def device_verify_endpoint
|
|
144
|
+
Endpoint.new(path: "/device", method: "GET") do |ctx|
|
|
145
|
+
code = normalize_user_code(OAuthProtocol.stringify_keys(ctx.query)["user_code"])
|
|
146
|
+
record = find_device_user_code(ctx, code)
|
|
147
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["INVALID_USER_CODE"]) unless record
|
|
148
|
+
record = OAuthProtocol.stringify_keys(record)
|
|
149
|
+
raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_USER_CODE"]) if device_authorization_time(record["expiresAt"]) <= Time.now
|
|
150
|
+
|
|
151
|
+
ctx.json({user_code: code, status: record["status"]})
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def device_approve_endpoint
|
|
156
|
+
Endpoint.new(path: "/device/approve", method: "POST") do |ctx|
|
|
157
|
+
session = Routes.current_session(ctx, allow_nil: true)
|
|
158
|
+
raise device_authorization_error("UNAUTHORIZED", "unauthorized", DEVICE_AUTHORIZATION_ERROR_CODES["AUTHENTICATION_REQUIRED"]) unless session
|
|
159
|
+
|
|
160
|
+
process_device_decision(ctx, session, "approved")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def device_deny_endpoint
|
|
165
|
+
Endpoint.new(path: "/device/deny", method: "POST") do |ctx|
|
|
166
|
+
session = Routes.current_session(ctx, allow_nil: true)
|
|
167
|
+
raise device_authorization_error("UNAUTHORIZED", "unauthorized", DEVICE_AUTHORIZATION_ERROR_CODES["AUTHENTICATION_REQUIRED"]) unless session
|
|
168
|
+
|
|
169
|
+
process_device_decision(ctx, session, "denied")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def process_device_decision(ctx, session, status)
|
|
174
|
+
body = OAuthProtocol.stringify_keys(ctx.body)
|
|
175
|
+
code = normalize_user_code(body["userCode"] || body["user_code"])
|
|
176
|
+
record = find_device_user_code(ctx, code)
|
|
177
|
+
action = (status == "approved") ? "approve" : "deny"
|
|
178
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["INVALID_USER_CODE"]) unless record
|
|
179
|
+
record = OAuthProtocol.stringify_keys(record)
|
|
180
|
+
raise device_authorization_error("BAD_REQUEST", "expired_token", DEVICE_AUTHORIZATION_ERROR_CODES["EXPIRED_USER_CODE"]) if device_authorization_time(record["expiresAt"]) <= Time.now
|
|
181
|
+
raise device_authorization_error("BAD_REQUEST", "invalid_request", DEVICE_AUTHORIZATION_ERROR_CODES["DEVICE_CODE_ALREADY_PROCESSED"]) unless record["status"] == "pending"
|
|
182
|
+
if record["userId"] && record["userId"] != session[:user]["id"]
|
|
183
|
+
raise device_authorization_error("FORBIDDEN", "access_denied", "You are not authorized to #{action} this device authorization")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
ctx.context.adapter.update(
|
|
187
|
+
model: "deviceCode",
|
|
188
|
+
where: [{field: "id", value: record["id"]}],
|
|
189
|
+
update: {"status" => status, "userId" => record["userId"] || session[:user]["id"]}
|
|
190
|
+
)
|
|
191
|
+
ctx.json({success: true})
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def find_device_code(ctx, code)
|
|
195
|
+
ctx.context.adapter.find_one(model: "deviceCode", where: [{field: "deviceCode", value: code.to_s}])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def find_device_user_code(ctx, code)
|
|
199
|
+
device_authorization_user_code_candidates(code).each do |candidate|
|
|
200
|
+
record = ctx.context.adapter.find_one(model: "deviceCode", where: [{field: "userCode", value: candidate}])
|
|
201
|
+
return record if record
|
|
202
|
+
end
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def normalize_user_code(value)
|
|
207
|
+
value.to_s
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def generate_device_authorization_device_code(config)
|
|
211
|
+
return config[:generate_device_code].call.to_s if config[:generate_device_code].respond_to?(:call)
|
|
212
|
+
|
|
213
|
+
SecureRandom.alphanumeric(config[:device_code_length].to_i)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def generate_device_authorization_user_code(config)
|
|
217
|
+
return config[:generate_user_code].call.to_s if config[:generate_user_code].respond_to?(:call)
|
|
218
|
+
|
|
219
|
+
charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
220
|
+
Array.new(config[:user_code_length].to_i) { charset[SecureRandom.random_number(charset.length)] }.join
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def verification_uri(ctx, config)
|
|
224
|
+
uri = config[:verification_uri] || "/device"
|
|
225
|
+
return uri if uri.to_s.start_with?("http://", "https://")
|
|
226
|
+
|
|
227
|
+
"#{OAuthProtocol.endpoint_base(ctx)}#{uri.to_s.start_with?("/") ? uri : "/#{uri}"}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def duration_seconds(value)
|
|
231
|
+
return value if value.is_a?(Integer)
|
|
232
|
+
|
|
233
|
+
match = value.to_s.match(/\A(\d+)(ms|s|m|min|h|d)?\z/)
|
|
234
|
+
raise Error, "Invalid time string" unless match
|
|
235
|
+
|
|
236
|
+
amount = match[1].to_i
|
|
237
|
+
case match[2]
|
|
238
|
+
when "ms" then (amount / 1000.0).ceil
|
|
239
|
+
when "m", "min" then amount * 60
|
|
240
|
+
when "h" then amount * 3600
|
|
241
|
+
when "d" then amount * 86_400
|
|
242
|
+
else amount
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def validate_device_authorization_options!(config)
|
|
247
|
+
duration_seconds(config[:expires_in])
|
|
248
|
+
duration_seconds(config[:interval])
|
|
249
|
+
raise Error, "device_code_length must be a positive integer" unless positive_integer?(config[:device_code_length])
|
|
250
|
+
raise Error, "user_code_length must be a positive integer" unless positive_integer?(config[:user_code_length])
|
|
251
|
+
raise Error, "generate_device_code must be callable" if config.key?(:generate_device_code) && !config[:generate_device_code].respond_to?(:call)
|
|
252
|
+
raise Error, "generate_user_code must be callable" if config.key?(:generate_user_code) && !config[:generate_user_code].respond_to?(:call)
|
|
253
|
+
raise Error, "validate_client must be callable" if config.key?(:validate_client) && !config[:validate_client].respond_to?(:call)
|
|
254
|
+
raise Error, "on_device_auth_request must be callable" if config.key?(:on_device_auth_request) && !config[:on_device_auth_request].respond_to?(:call)
|
|
255
|
+
raise Error, "verification_uri must be a string" if config.key?(:verification_uri) && !config[:verification_uri].is_a?(String)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def positive_integer?(value)
|
|
259
|
+
value.is_a?(Integer) && value.positive?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def device_authorization_user_code_candidates(value)
|
|
263
|
+
original = value.to_s
|
|
264
|
+
upper = original.upcase
|
|
265
|
+
clean = original.delete("-")
|
|
266
|
+
upper_clean = upper.delete("-")
|
|
267
|
+
dashed = (upper_clean.length == 8) ? "#{upper_clean[0, 4]}-#{upper_clean[4, 4]}" : upper_clean
|
|
268
|
+
[original, upper, clean, upper_clean, dashed].uniq
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def device_authorization_error(status, error, description)
|
|
272
|
+
APIError.new(status, code: error, message: description, body: {error: error, error_description: description})
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def device_authorization_time(value)
|
|
276
|
+
return value if value.is_a?(Time)
|
|
277
|
+
|
|
278
|
+
Time.parse(value.to_s)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def device_authorization_schema(custom_schema = nil)
|
|
282
|
+
base = {
|
|
283
|
+
deviceCode: {
|
|
284
|
+
fields: {
|
|
285
|
+
deviceCode: {type: "string", required: true},
|
|
286
|
+
userCode: {type: "string", required: true},
|
|
287
|
+
userId: {type: "string", required: false},
|
|
288
|
+
expiresAt: {type: "date", required: true},
|
|
289
|
+
status: {type: "string", required: true},
|
|
290
|
+
lastPolledAt: {type: "date", required: false},
|
|
291
|
+
pollingInterval: {type: "number", required: false},
|
|
292
|
+
clientId: {type: "string", required: false},
|
|
293
|
+
scope: {type: "string", required: false}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return base unless custom_schema.is_a?(Hash)
|
|
298
|
+
|
|
299
|
+
deep_merge_hashes(base, normalize_hash(custom_schema))
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|