standard_id 0.1.0 → 0.1.2

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +347 -7
  3. data/app/controllers/standard_id/api/providers_controller.rb +15 -16
  4. data/app/controllers/standard_id/web/verify_email/base_controller.rb +9 -0
  5. data/app/controllers/standard_id/web/verify_email/confirm_controller.rb +41 -0
  6. data/app/controllers/standard_id/web/verify_email/start_controller.rb +39 -0
  7. data/app/controllers/standard_id/web/verify_phone/base_controller.rb +9 -0
  8. data/app/controllers/standard_id/web/verify_phone/confirm_controller.rb +41 -0
  9. data/app/controllers/standard_id/web/verify_phone/start_controller.rb +39 -0
  10. data/app/forms/standard_id/web/signup_form.rb +3 -14
  11. data/app/models/standard_id/client_application.rb +2 -1
  12. data/app/models/standard_id/{passwordless_challenge.rb → code_challenge.rb} +9 -5
  13. data/app/models/standard_id/email_identifier.rb +2 -0
  14. data/app/models/standard_id/identifier.rb +17 -0
  15. data/app/models/standard_id/phone_number_identifier.rb +2 -0
  16. data/app/models/standard_id/username_identifier.rb +2 -0
  17. data/config/routes/web.rb +12 -0
  18. data/db/migrate/20250830000000_create_standard_id_client_applications.rb +3 -3
  19. data/db/migrate/20250830232800_create_standard_id_identifiers.rb +1 -1
  20. data/db/migrate/20250831075703_create_standard_id_credentials.rb +2 -2
  21. data/db/migrate/20250831154635_create_standard_id_sessions.rb +2 -2
  22. data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +2 -2
  23. data/db/migrate/20250907090000_create_standard_id_code_challenges.rb +29 -0
  24. data/lib/generators/standard_id/install/templates/standard_id.rb +36 -3
  25. data/lib/{standard_id → standard_config}/config.rb +1 -1
  26. data/lib/standard_config/config_provider.rb +82 -0
  27. data/lib/standard_config/manager.rb +86 -0
  28. data/lib/standard_config/schema.rb +137 -0
  29. data/lib/standard_config.rb +38 -0
  30. data/lib/standard_id/api/session_manager.rb +1 -1
  31. data/lib/standard_id/config/schema.rb +49 -0
  32. data/lib/standard_id/oauth/client_credentials_flow.rb +1 -5
  33. data/lib/standard_id/oauth/implicit_authorization_flow.rb +1 -1
  34. data/lib/standard_id/oauth/password_flow.rb +0 -4
  35. data/lib/standard_id/oauth/passwordless_otp_flow.rb +7 -10
  36. data/lib/standard_id/oauth/token_grant_flow.rb +6 -2
  37. data/lib/standard_id/oauth/token_lifetime_resolver.rb +50 -0
  38. data/lib/standard_id/passwordless/base_strategy.rb +8 -4
  39. data/lib/standard_id/version.rb +1 -1
  40. data/lib/standard_id.rb +13 -4
  41. metadata +16 -4
  42. data/db/migrate/20250903135906_create_standard_id_passwordless_challenges.rb +0 -22
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f9b8808e874cdc6e89dca4b7ed1bb9636ae510d3947d48eb6f82933d40a6d9d1
4
- data.tar.gz: 9fbd9e342e280da87695e011d5d6f031530eee53ced1c9ef93dbba4af072c141
3
+ metadata.gz: d8da5350b060ff2f0494489d5cc6718cc08b2dbca835a9c28927e228b712e39d
4
+ data.tar.gz: 96ac14f364fd07b8d6750d31bd015b7bc2717d692206a203b3fe8161a6a1008f
5
5
  SHA512:
6
- metadata.gz: 1a6264b3b7f1dcf8e0f3f00861eb265d535bc8254352e6dd6c95c64dd59756db7ddf96cace31200ac0a9bb557710316a2306cdf1c52e75b5d13644530c0749e4
7
- data.tar.gz: 5c6f5126077d8e8ca4598cf9668001b5517004ad8b444cb1fb04301979790cb2da2ed44b7f8ebeb7d75bbbd50f0ddc0fb758c40a4bbeeb9a453d975b1a66825a
6
+ metadata.gz: 195c91598a768df91279b0c6bda4d8b312f73a94d4c52c68b2864fd75453f2c474059c4f7d740e0d8c0b4086471ea193c9ded4bb7b492f3c098c950f941f6140
7
+ data.tar.gz: 80d64202a7b69456e241952b79f0a65ea9a88ed8b242ff31015e11854d141bd32ccbeaccc2f60ccddf78d188dbbec74e48f839c9181a2b4cdce01917001e4125
data/README.md CHANGED
@@ -1,10 +1,44 @@
1
1
  # StandardId
2
- Short description and motivation.
3
2
 
4
- ## Usage
5
- How to use my plugin.
3
+ A comprehensive authentication engine for Rails applications, built on the security primitives introduced in Rails 7/8. StandardId provides a complete, secure-by-default solution for identity management, reducing boilerplate and eliminating common security pitfalls.
4
+
5
+ ## Features
6
+
7
+ ### 🔐 Complete Authentication System
8
+ - **Web Authentication**: Cookie-based sessions with CSRF protection
9
+ - **API Authentication**: JWT-based tokens for API access
10
+ - **Dual Engine Architecture**: Separate web (`/`) and API (`/api`) endpoints
11
+ - **Session Management**: Browser sessions, device sessions, and service sessions with STI
12
+
13
+ ### 🚀 OAuth 2.0 & OpenID Connect
14
+ - **Authorization Code Flow**: Standard OAuth flow with PKCE support
15
+ - **Implicit Flow**: For single-page applications
16
+ - **Client Credentials Flow**: For service-to-service authentication
17
+ - **Password Flow**: Direct username/password authentication
18
+ - **Refresh Token Flow**: Automatic token renewal
19
+ - **Social Login**: Google OAuth and Apple Sign In integration
20
+
21
+ ### 📱 Passwordless Authentication
22
+ - **Email OTP**: Send one-time passwords via email
23
+ - **SMS OTP**: Send one-time passwords via SMS
24
+ - **Configurable Delivery**: Host app controls message delivery
25
+ - **10-minute Expiry**: Secure time-limited codes
26
+
27
+ ### 🏢 Multi-Tenant Support
28
+ - **Client Management**: OAuth clients with secret rotation
29
+ - **Polymorphic Ownership**: Clients can belong to accounts, organizations, etc.
30
+ - **Scope Management**: Fine-grained permission control
31
+ - **Redirect URI Validation**: Secure callback handling
32
+
33
+ ### 🔑 Advanced Security
34
+ - **PKCE Support**: Proof Key for Code Exchange
35
+ - **JWT Tokens**: Stateless authentication with configurable expiry
36
+ - **Secret Rotation**: Client secret management with audit trail
37
+ - **Remember Me**: Extended session support
38
+ - **Account Lockout**: Protection against brute force attacks
6
39
 
7
40
  ## Installation
41
+
8
42
  Add this line to your application's Gemfile:
9
43
 
10
44
  ```ruby
@@ -13,16 +47,322 @@ gem "standard_id"
13
47
 
14
48
  And then execute:
15
49
  ```bash
16
- $ bundle
50
+ $ bundle install
51
+ ```
52
+
53
+ ## Quick Start
54
+
55
+ ### 1. Generate Configuration
56
+
57
+ ```bash
58
+ rails generate standard_id:install
59
+ ```
60
+
61
+ ### 2. Configure Your Account Model
62
+
63
+ ```ruby
64
+ # config/initializers/standard_id.rb
65
+ StandardId.configure do |config|
66
+ config.account_class_name = "User" # or "Account"
67
+ config.issuer = "https://your-app.com"
68
+ config.login_url = "/login"
69
+ end
70
+ ```
71
+
72
+ ### 3. Mount the Engines
73
+
74
+ ```ruby
75
+ # config/routes.rb
76
+ Rails.application.routes.draw do
77
+ mount StandardId::WebEngine, at: "/", as: :standard_id_web
78
+
79
+ namespace :api do
80
+ mount StandardId::ApiEngine, at: "/", as: :standard_id_api
81
+ end
82
+ end
83
+ ```
84
+
85
+ ### 4. Include Authentication in Controllers
86
+
87
+ ```ruby
88
+ # For web controllers
89
+ class ApplicationController < ActionController::Base
90
+ include StandardId::WebAuthentication
91
+ end
92
+
93
+ # For API controllers
94
+ class ApiController < ActionController::API
95
+ include StandardId::ApiAuthentication
96
+ end
97
+ ```
98
+
99
+ ## Configuration
100
+
101
+ ### Basic Configuration
102
+
103
+ ```ruby
104
+ StandardId.configure do |config|
105
+ # Required: Your account model
106
+ config.account_class_name = "User"
107
+
108
+ # OAuth issuer for ID tokens
109
+ config.issuer = "https://your-app.com"
110
+
111
+ # Login URL for redirects
112
+ config.login_url = "/login"
113
+
114
+ # Custom layout for web views
115
+ config.web_layout = "application"
116
+
117
+ # Passwordless delivery callbacks
118
+ # config.passwordless_email_sender = ->(email, code) { UserMailer.send_code(email, code).deliver_now }
119
+ # config.passwordless_sms_sender = ->(phone, code) { SmsService.send_code(phone, code) }
120
+
121
+ # Subset configuration
122
+ # config.password.minimum_length = 12
123
+ # config.password.require_special_chars = true
124
+ # config.passwordless.code_ttl = 600
125
+ # config.oauth.default_token_lifetime = 3600
126
+ # config.oauth.refresh_token_lifetime = 2_592_000
127
+ # config.oauth.token_lifetimes = {
128
+ # password: 8.hours.to_i,
129
+ # implicit: 15.minutes.to_i
130
+ # }
131
+ end
132
+ ```
133
+
134
+ `default_token_lifetime` is applied to every OAuth grant unless you override it in `oauth.token_lifetimes`. Keys map to OAuth grant types (for example `:password`, `:client_credentials`, `:refresh_token`) and should return durations in seconds. Non-token endpoint flows such as the implicit flow can be customized with their symbol key (e.g. `:implicit`). Refresh tokens can be tuned separately through `oauth.refresh_token_lifetime`.
135
+
136
+ ### Social Login Setup
137
+
138
+ ```ruby
139
+ StandardId.configure do |config|
140
+ # Google OAuth
141
+ config.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
142
+ config.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
143
+
144
+ # Apple Sign In
145
+ config.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
146
+ config.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
147
+ config.social.apple_key_id = ENV["APPLE_KEY_ID"]
148
+ config.social.apple_team_id = ENV["APPLE_TEAM_ID"]
149
+ end
150
+ ```
151
+
152
+ ### Passwordless Authentication
153
+
154
+ ```ruby
155
+ StandardId.configure do |config|
156
+ # Email delivery
157
+ config.passwordless_email_sender = ->(email, code) {
158
+ UserMailer.send_code(email, code).deliver_now
159
+ }
160
+
161
+ # SMS delivery
162
+ config.passwordless_sms_sender = ->(phone, code) {
163
+ SmsService.send_code(phone, code)
164
+ }
165
+ end
166
+ ```
167
+
168
+ ## Usage Examples
169
+
170
+ ### Web Authentication
171
+
172
+ ```erb
173
+ <!-- Login form -->
174
+ <%= form_with url: login_path, local: true do |f| %>
175
+ <%= f.email_field :email, placeholder: "Email" %>
176
+ <%= f.password_field :password, placeholder: "Password" %>
177
+ <%= f.check_box :remember_me %>
178
+ <%= f.label :remember_me, "Remember me" %>
179
+ <%= f.submit "Sign In" %>
180
+ <% end %>
181
+ ```
182
+
183
+ ### OAuth Authorization
184
+
185
+ ```ruby
186
+ # Redirect to authorization endpoint
187
+ redirect_to "/api/authorize?" + {
188
+ response_type: "code",
189
+ client_id: "your_client_id",
190
+ redirect_uri: "https://your-app.com/callback",
191
+ scope: "openid profile email",
192
+ state: "random_state_value"
193
+ }.to_query
194
+ ```
195
+
196
+ ### Social Login
197
+
198
+ ```ruby
199
+ # Google login
200
+ redirect_to "/api/authorize?" + {
201
+ response_type: "code",
202
+ client_id: "your_client_id",
203
+ redirect_uri: "https://your-app.com/callback",
204
+ connection: "google-oauth2"
205
+ }.to_query
206
+
207
+ # Apple login
208
+ redirect_to "/api/authorize?" + {
209
+ response_type: "code",
210
+ client_id: "your_client_id",
211
+ redirect_uri: "https://your-app.com/callback",
212
+ connection: "apple"
213
+ }.to_query
214
+ ```
215
+
216
+ ### Passwordless Authentication
217
+
218
+ ```ruby
219
+ # Start passwordless flow
220
+ POST /api/passwordless/start
221
+ {
222
+ "connection": "email",
223
+ "username": "user@example.com"
224
+ }
225
+
226
+ # Verify code
227
+ POST /api/passwordless/verify
228
+ {
229
+ "connection": "email",
230
+ "username": "user@example.com",
231
+ "otp": "123456"
232
+ }
233
+ ```
234
+
235
+ ### API Authentication
236
+
237
+ ```ruby
238
+ # In your API controllers
239
+ class Api::UsersController < ApiController
240
+ before_action :authenticate_account!
241
+
242
+ def show
243
+ render json: current_account
244
+ end
245
+ end
246
+ ```
247
+
248
+ ## Database Schema
249
+
250
+ StandardId creates the following tables:
251
+
252
+ - `standard_id_accounts` - User accounts
253
+ - `standard_id_identifiers` - Email/phone identifiers (STI)
254
+ - `standard_id_sessions` - Authentication sessions (STI)
255
+ - `standard_id_clients` - OAuth clients
256
+ - `standard_id_client_secret_credentials` - Client secrets
257
+ - `standard_id_password_credentials` - Password storage
258
+ - `standard_id_code_challenges` - OTP codes for authentication and verification
259
+
260
+ ## API Endpoints
261
+
262
+ ### Web Routes (mounted at `/`)
263
+ - `GET /login` - Login form
264
+ - `POST /login` - Process login
265
+ - `POST /logout` - Logout
266
+ - `GET /signup` - Signup form
267
+ - `POST /signup` - Process signup
268
+ - `GET /account` - Account management
269
+ - `GET /sessions` - Active sessions
270
+
271
+ ### API Routes (mounted at `/api`)
272
+ - `GET /authorize` - OAuth authorization endpoint
273
+ - `POST /oauth/token` - Token exchange endpoint
274
+ - `GET /userinfo` - OpenID Connect userinfo
275
+ - `POST /passwordless/start` - Start passwordless flow
276
+ - `POST /passwordless/verify` - Verify OTP code
277
+ - `GET /oauth/callback/google` - Google OAuth callback
278
+ - `POST /oauth/callback/apple` - Apple Sign In callback
279
+
280
+ ## Client Management
281
+
282
+ ```ruby
283
+ # Create OAuth client
284
+ client = StandardId::ClientApplication.create!(
285
+ owner: current_account,
286
+ name: "My Application",
287
+ redirect_uris: "https://app.com/callback",
288
+ grant_types: ["authorization_code", "refresh_token"],
289
+ response_types: ["code"],
290
+ scopes: ["openid", "profile", "email"]
291
+ )
292
+
293
+ # Generate client secret
294
+ secret = client.create_client_secret!(name: "Production Secret")
295
+
296
+ # Rotate client secret
297
+ new_secret = client.rotate_client_secret!
17
298
  ```
18
299
 
19
- Or install it yourself as:
300
+ ## Schema DSL
301
+
302
+ Schema is declared using a routes-like DSL and can be extended by provider gems:
303
+
304
+ ```ruby
305
+ # core gem (already provided)
306
+ require "standard_id/config/schema"
307
+
308
+ StandardConfig.schema.draw do
309
+ scope :base do
310
+ field :account_class_name, type: :string, default: "User"
311
+ end
312
+
313
+ scope :social do
314
+ field :google_client_id, type: :string, default: nil
315
+ end
316
+ end
317
+
318
+ # provider gem
319
+ require "standard_id/config/schema"
320
+
321
+ StandardConfig.schema.draw do
322
+ scope :social do
323
+ field :my_provider_client_id, type: :string, default: nil
324
+ end
325
+ end
326
+ ```
327
+
328
+ Notes:
329
+
330
+ - Multiple `schema.draw` calls are additive; the same scope can be extended in multiple files/gems.
331
+ - Redefining an existing field will emit a warning; last definition wins.
332
+
333
+ ## Testing
334
+
335
+ StandardId includes comprehensive test coverage:
336
+
20
337
  ```bash
21
- $ gem install standard_id
338
+ # Run all tests
339
+ bundle exec rspec
340
+
341
+ # Run specific test suites
342
+ bundle exec rspec spec/models/
343
+ bundle exec rspec spec/controllers/
22
344
  ```
23
345
 
346
+ ## Security Considerations
347
+
348
+ - All passwords are hashed using bcrypt
349
+ - JWT tokens are signed and verified
350
+ - CSRF protection enabled for web requests
351
+ - Secure session management with proper expiry
352
+ - Client secrets are rotatable with audit trail
353
+ - PKCE support for public clients
354
+ - Rate limiting on authentication endpoints
355
+
24
356
  ## Contributing
25
- Contribution directions go here.
357
+
358
+ 1. Fork the repository
359
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
360
+ 3. Write tests for your changes
361
+ 4. Ensure all tests pass (`bin/rspec`)
362
+ 5. Commit your changes (`git commit -am 'Add amazing feature'`)
363
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
364
+ 7. Open a Pull Request
26
365
 
27
366
  ## License
367
+
28
368
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -123,22 +123,21 @@ module StandardId
123
123
 
124
124
  identifier = StandardId::EmailIdentifier.find_by(value: email)
125
125
 
126
- if identifier
127
- identifier.account
128
- else
129
- account = Account.create!(
130
- name: (user_info["name"] || user_info["given_name"] || email),
131
- email: email
132
- )
133
-
134
- StandardId::EmailIdentifier.create!(
135
- account: account,
136
- value: email,
137
- verified_at: Time.current
138
- )
139
-
140
- account
141
- end
126
+ return identifier.account if identifier.present?
127
+
128
+ account = Account.create!(
129
+ name: (user_info["name"] || user_info["given_name"] || email),
130
+ email:
131
+ )
132
+
133
+ identifier = StandardId::EmailIdentifier.create!(
134
+ account:,
135
+ value: email
136
+ )
137
+
138
+ identifier.verify!
139
+
140
+ account
142
141
  end
143
142
 
144
143
  def generate_authorization_code
@@ -0,0 +1,9 @@
1
+ module StandardId
2
+ module Web
3
+ module VerifyEmail
4
+ class BaseController < StandardId::Web::BaseController
5
+ skip_before_action :require_browser_session!
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ module StandardId
2
+ module Web
3
+ module VerifyEmail
4
+ class ConfirmController < BaseController
5
+ before_action :prepare_code_challenge
6
+
7
+ def show
8
+ return redirect_to(standard_id_web.login_path, alert: "Invalid or expired verification code") if @challenge.nil?
9
+ render plain: "verify email confirm", status: :ok
10
+ end
11
+
12
+ def update
13
+ return redirect_to(standard_id_web.login_path, alert: "Invalid or expired verification code") if @challenge.nil?
14
+
15
+ identifier = StandardId::EmailIdentifier.find_by(value: @challenge.target)
16
+ if identifier.present?
17
+ identifier.verify!
18
+ end
19
+ @challenge.use!
20
+
21
+ redirect_to standard_id_web.login_path, notice: "Your email has been verified. Please sign in.", status: :see_other
22
+ end
23
+
24
+ private
25
+
26
+ def prepare_code_challenge
27
+ email = params[:email].to_s.strip.downcase
28
+ code = params[:code].to_s
29
+ return @challenge = nil if email.blank? || code.blank?
30
+
31
+ @challenge = StandardId::CodeChallenge.active.find_by(
32
+ realm: "verification",
33
+ channel: "email",
34
+ target: email,
35
+ code: code
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ module StandardId
2
+ module Web
3
+ module VerifyEmail
4
+ class StartController < BaseController
5
+ def show
6
+ render plain: "verify email start", status: :ok
7
+ end
8
+
9
+ def create
10
+ email = params[:email].to_s.strip.downcase
11
+ if email.blank?
12
+ flash[:alert] = "Please enter your email address"
13
+ render plain: "missing email", status: :unprocessable_content and return
14
+ end
15
+
16
+ challenge = StandardId::CodeChallenge.create!(
17
+ realm: "verification",
18
+ channel: "email",
19
+ target: email,
20
+ code: generate_otp_code,
21
+ expires_at: 10.minutes.from_now,
22
+ ip_address: request.remote_ip,
23
+ user_agent: request.user_agent
24
+ )
25
+
26
+ StandardId.config.passwordless_email_sender&.call(email, challenge.code)
27
+
28
+ redirect_to standard_id_web.login_path, notice: "Verification code sent to your email", status: :see_other
29
+ end
30
+
31
+ private
32
+
33
+ def generate_otp_code
34
+ (SecureRandom.random_number(900_000) + 100_000).to_s
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,9 @@
1
+ module StandardId
2
+ module Web
3
+ module VerifyPhone
4
+ class BaseController < StandardId::Web::BaseController
5
+ skip_before_action :require_browser_session!
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,41 @@
1
+ module StandardId
2
+ module Web
3
+ module VerifyPhone
4
+ class ConfirmController < BaseController
5
+ before_action :prepare_code_challenge
6
+
7
+ def show
8
+ return redirect_to(standard_id_web.login_path, alert: "Invalid or expired verification code") if @challenge.nil?
9
+ render plain: "verify phone confirm", status: :ok
10
+ end
11
+
12
+ def update
13
+ return redirect_to(standard_id_web.login_path, alert: "Invalid or expired verification code") if @challenge.nil?
14
+
15
+ identifier = StandardId::PhoneNumberIdentifier.find_by(value: @challenge.target)
16
+ if identifier.present?
17
+ identifier.verify!
18
+ end
19
+ @challenge.use!
20
+
21
+ redirect_to standard_id_web.login_path, notice: "Your phone number has been verified. Please sign in.", status: :see_other
22
+ end
23
+
24
+ private
25
+
26
+ def prepare_code_challenge
27
+ phone = params[:phone_number].to_s.strip
28
+ code = params[:code].to_s
29
+ return @challenge = nil if phone.blank? || code.blank?
30
+
31
+ @challenge = StandardId::CodeChallenge.active.find_by(
32
+ realm: "verification",
33
+ channel: "sms",
34
+ target: phone,
35
+ code: code
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ module StandardId
2
+ module Web
3
+ module VerifyPhone
4
+ class StartController < BaseController
5
+ def show
6
+ render plain: "verify phone start", status: :ok
7
+ end
8
+
9
+ def create
10
+ phone = params[:phone_number].to_s.strip
11
+ if phone.blank? || !(phone.match?(/\A\+?[1-9]\d{1,14}\z/))
12
+ flash[:alert] = "Please enter a valid phone number"
13
+ render plain: "invalid phone", status: :unprocessable_content and return
14
+ end
15
+
16
+ challenge = StandardId::CodeChallenge.create!(
17
+ realm: "verification",
18
+ channel: "sms",
19
+ target: phone,
20
+ code: generate_otp_code,
21
+ expires_at: 10.minutes.from_now,
22
+ ip_address: request.remote_ip,
23
+ user_agent: request.user_agent
24
+ )
25
+
26
+ StandardId.config.passwordless_sms_sender&.call(phone, challenge.code)
27
+
28
+ redirect_to standard_id_web.login_path, notice: "Verification code sent via SMS", status: :see_other
29
+ end
30
+
31
+ private
32
+
33
+ def generate_otp_code
34
+ (SecureRandom.random_number(900_000) + 100_000).to_s
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -39,26 +39,15 @@ module StandardId
39
39
  private
40
40
 
41
41
  def account_params
42
- # Placeholder for account fields. Add/permit additional attributes as needed.
43
- {
44
- name: (email.to_s.split("@").first.presence || "User"),
45
- email: email
46
- }
42
+ { name: (email.to_s.split("@").first.presence || "User"), email: }
47
43
  end
48
44
 
49
45
  def email_identifier_params
50
- {
51
- value: email,
52
- verified_at: Time.current,
53
- type: "StandardId::EmailIdentifier"
54
- }
46
+ { type: "StandardId::EmailIdentifier", value: email }
55
47
  end
56
48
 
57
49
  def password_credential_params
58
- {
59
- login: email,
60
- password: password
61
- }
50
+ { login: email, password: }
62
51
  end
63
52
  end
64
53
  end
@@ -94,7 +94,8 @@ module StandardId
94
94
  def create_client_secret!(name: "Default Secret", **options)
95
95
  client_secret_credentials.create!({
96
96
  name: name,
97
- client_id: client_id
97
+ client_id: client_id,
98
+ scopes: scopes
98
99
  }.merge(options))
99
100
  end
100
101
 
@@ -1,10 +1,14 @@
1
1
  module StandardId
2
- class PasswordlessChallenge < ApplicationRecord
3
- self.table_name = "standard_id_passwordless_challenges"
2
+ class CodeChallenge < ApplicationRecord
3
+ self.table_name = "standard_id_code_challenges"
4
4
 
5
- validates :connection_type, presence: true, inclusion: { in: %w[email sms] }
6
- validates :username, presence: true
7
- validates :code, presence: true, uniqueness: { scope: [:connection_type, :username, :expires_at] }
5
+ REALMS = %w[authentication verification].freeze
6
+ CHANNELS = %w[email sms].freeze
7
+
8
+ validates :realm, presence: true, inclusion: { in: REALMS }
9
+ validates :channel, presence: true, inclusion: { in: CHANNELS }
10
+ validates :target, presence: true
11
+ validates :code, presence: true
8
12
  validates :expires_at, presence: true
9
13
 
10
14
  scope :active, -> { where(used_at: nil).where("expires_at > ?", Time.current) }
@@ -1,5 +1,7 @@
1
1
  module StandardId
2
2
  class EmailIdentifier < Identifier
3
+ normalizes :value, with: ->(e) { e.strip.downcase }
4
+
3
5
  validates :value, format: { with: URI::MailTo::EMAIL_REGEXP }
4
6
  end
5
7
  end