standard_id 0.1.0 → 0.1.1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +340 -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/{passwordless_challenge.rb → code_challenge.rb} +9 -5
  12. data/app/models/standard_id/email_identifier.rb +2 -0
  13. data/app/models/standard_id/identifier.rb +17 -0
  14. data/app/models/standard_id/phone_number_identifier.rb +2 -0
  15. data/app/models/standard_id/username_identifier.rb +2 -0
  16. data/config/routes/web.rb +12 -0
  17. data/db/migrate/20250830000000_create_standard_id_client_applications.rb +3 -3
  18. data/db/migrate/20250830232800_create_standard_id_identifiers.rb +1 -1
  19. data/db/migrate/20250831075703_create_standard_id_credentials.rb +2 -2
  20. data/db/migrate/20250831154635_create_standard_id_sessions.rb +2 -2
  21. data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +2 -2
  22. data/db/migrate/20250907090000_create_standard_id_code_challenges.rb +29 -0
  23. data/lib/generators/standard_id/install/templates/standard_id.rb +31 -3
  24. data/lib/{standard_id → standard_config}/config.rb +1 -1
  25. data/lib/standard_config/config_provider.rb +82 -0
  26. data/lib/standard_config/manager.rb +86 -0
  27. data/lib/standard_config/schema.rb +137 -0
  28. data/lib/standard_config.rb +38 -0
  29. data/lib/standard_id/api/session_manager.rb +1 -1
  30. data/lib/standard_id/config/schema.rb +48 -0
  31. data/lib/standard_id/oauth/passwordless_otp_flow.rb +7 -6
  32. data/lib/standard_id/passwordless/base_strategy.rb +8 -4
  33. data/lib/standard_id/version.rb +1 -1
  34. data/lib/standard_id.rb +12 -4
  35. metadata +15 -4
  36. 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: c7150dd77b059e80767d262bc8a8b507399491977208404f3bd2d04209f9f45d
4
+ data.tar.gz: e5157110e538a9e6cf625cee184fb423747235eac74c65821fb1b62ba0201452
5
5
  SHA512:
6
- metadata.gz: 1a6264b3b7f1dcf8e0f3f00861eb265d535bc8254352e6dd6c95c64dd59756db7ddf96cace31200ac0a9bb557710316a2306cdf1c52e75b5d13644530c0749e4
7
- data.tar.gz: 5c6f5126077d8e8ca4598cf9668001b5517004ad8b444cb1fb04301979790cb2da2ed44b7f8ebeb7d75bbbd50f0ddc0fb758c40a4bbeeb9a453d975b1a66825a
6
+ metadata.gz: 6c06a909ce1cca77763f8a9c23a01e2703a84c4b4b99435c5f63dc0a9f3ccf54c3f55c98ea9147b51a9b8ce538be6be57003a4f7c744f608c2a4fe40bf04df46
7
+ data.tar.gz: b6dc47dc00160c2487c8611b73c21330cd6cbec0865036e235628fb744151c4f946afd0db17f02fa0ca31f3df7261cf94adad09d8c9b2963ed32806dcaf19e63
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,315 @@ gem "standard_id"
13
47
 
14
48
  And then execute:
15
49
  ```bash
16
- $ bundle
50
+ $ bundle install
17
51
  ```
18
52
 
19
- Or install it yourself as:
53
+ ## Quick Start
54
+
55
+ ### 1. Generate Configuration
56
+
20
57
  ```bash
21
- $ gem install standard_id
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::Web::WebAuthentication
91
+ end
92
+
93
+ # For API controllers
94
+ class ApiController < ActionController::API
95
+ include StandardId::Api::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
+ end
127
+ ```
128
+
129
+ ### Social Login Setup
130
+
131
+ ```ruby
132
+ StandardId.configure do |config|
133
+ # Google OAuth
134
+ config.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
135
+ config.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
136
+
137
+ # Apple Sign In
138
+ config.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
139
+ config.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
140
+ config.social.apple_key_id = ENV["APPLE_KEY_ID"]
141
+ config.social.apple_team_id = ENV["APPLE_TEAM_ID"]
142
+ end
22
143
  ```
23
144
 
145
+ ### Passwordless Authentication
146
+
147
+ ```ruby
148
+ StandardId.configure do |config|
149
+ # Email delivery
150
+ config.passwordless_email_sender = ->(email, code) {
151
+ UserMailer.send_code(email, code).deliver_now
152
+ }
153
+
154
+ # SMS delivery
155
+ config.passwordless_sms_sender = ->(phone, code) {
156
+ SmsService.send_code(phone, code)
157
+ }
158
+ end
159
+ ```
160
+
161
+ ## Usage Examples
162
+
163
+ ### Web Authentication
164
+
165
+ ```erb
166
+ <!-- Login form -->
167
+ <%= form_with url: login_path, local: true do |f| %>
168
+ <%= f.email_field :email, placeholder: "Email" %>
169
+ <%= f.password_field :password, placeholder: "Password" %>
170
+ <%= f.check_box :remember_me %>
171
+ <%= f.label :remember_me, "Remember me" %>
172
+ <%= f.submit "Sign In" %>
173
+ <% end %>
174
+ ```
175
+
176
+ ### OAuth Authorization
177
+
178
+ ```ruby
179
+ # Redirect to authorization endpoint
180
+ redirect_to "/api/authorize?" + {
181
+ response_type: "code",
182
+ client_id: "your_client_id",
183
+ redirect_uri: "https://your-app.com/callback",
184
+ scope: "openid profile email",
185
+ state: "random_state_value"
186
+ }.to_query
187
+ ```
188
+
189
+ ### Social Login
190
+
191
+ ```ruby
192
+ # Google login
193
+ redirect_to "/api/authorize?" + {
194
+ response_type: "code",
195
+ client_id: "your_client_id",
196
+ redirect_uri: "https://your-app.com/callback",
197
+ connection: "google-oauth2"
198
+ }.to_query
199
+
200
+ # Apple login
201
+ redirect_to "/api/authorize?" + {
202
+ response_type: "code",
203
+ client_id: "your_client_id",
204
+ redirect_uri: "https://your-app.com/callback",
205
+ connection: "apple"
206
+ }.to_query
207
+ ```
208
+
209
+ ### Passwordless Authentication
210
+
211
+ ```ruby
212
+ # Start passwordless flow
213
+ POST /api/passwordless/start
214
+ {
215
+ "connection": "email",
216
+ "username": "user@example.com"
217
+ }
218
+
219
+ # Verify code
220
+ POST /api/passwordless/verify
221
+ {
222
+ "connection": "email",
223
+ "username": "user@example.com",
224
+ "otp": "123456"
225
+ }
226
+ ```
227
+
228
+ ### API Authentication
229
+
230
+ ```ruby
231
+ # In your API controllers
232
+ class Api::UsersController < ApiController
233
+ before_action :authenticate_account!
234
+
235
+ def show
236
+ render json: current_account
237
+ end
238
+ end
239
+ ```
240
+
241
+ ## Database Schema
242
+
243
+ StandardId creates the following tables:
244
+
245
+ - `standard_id_accounts` - User accounts
246
+ - `standard_id_identifiers` - Email/phone identifiers (STI)
247
+ - `standard_id_sessions` - Authentication sessions (STI)
248
+ - `standard_id_clients` - OAuth clients
249
+ - `standard_id_client_secret_credentials` - Client secrets
250
+ - `standard_id_password_credentials` - Password storage
251
+ - `standard_id_code_challenges` - OTP codes for authentication and verification
252
+
253
+ ## API Endpoints
254
+
255
+ ### Web Routes (mounted at `/`)
256
+ - `GET /login` - Login form
257
+ - `POST /login` - Process login
258
+ - `POST /logout` - Logout
259
+ - `GET /signup` - Signup form
260
+ - `POST /signup` - Process signup
261
+ - `GET /account` - Account management
262
+ - `GET /sessions` - Active sessions
263
+
264
+ ### API Routes (mounted at `/api`)
265
+ - `GET /authorize` - OAuth authorization endpoint
266
+ - `POST /oauth/token` - Token exchange endpoint
267
+ - `GET /userinfo` - OpenID Connect userinfo
268
+ - `POST /passwordless/start` - Start passwordless flow
269
+ - `POST /passwordless/verify` - Verify OTP code
270
+ - `GET /oauth/callback/google` - Google OAuth callback
271
+ - `POST /oauth/callback/apple` - Apple Sign In callback
272
+
273
+ ## Client Management
274
+
275
+ ```ruby
276
+ # Create OAuth client
277
+ client = StandardId::Client.create!(
278
+ owner: current_account,
279
+ name: "My Application",
280
+ redirect_uris: "https://app.com/callback",
281
+ grant_types: ["authorization_code", "refresh_token"],
282
+ response_types: ["code"],
283
+ scopes: ["openid", "profile", "email"]
284
+ )
285
+
286
+ # Generate client secret
287
+ secret = client.create_client_secret!(name: "Production Secret")
288
+
289
+ # Rotate client secret
290
+ new_secret = client.rotate_client_secret!
291
+ ```
292
+
293
+ ## Schema DSL
294
+
295
+ Schema is declared using a routes-like DSL and can be extended by provider gems:
296
+
297
+ ```ruby
298
+ # core gem (already provided)
299
+ require "standard_id/config/schema"
300
+
301
+ StandardConfig.schema.draw do
302
+ scope :base do
303
+ field :account_class_name, type: :string, default: "User"
304
+ end
305
+
306
+ scope :social do
307
+ field :google_client_id, type: :string, default: nil
308
+ end
309
+ end
310
+
311
+ # provider gem
312
+ require "standard_id/config/schema"
313
+
314
+ StandardConfig.schema.draw do
315
+ scope :social do
316
+ field :my_provider_client_id, type: :string, default: nil
317
+ end
318
+ end
319
+ ```
320
+
321
+ Notes:
322
+
323
+ - Multiple `schema.draw` calls are additive; the same scope can be extended in multiple files/gems.
324
+ - Redefining an existing field will emit a warning; last definition wins.
325
+
326
+ ## Testing
327
+
328
+ StandardId includes comprehensive test coverage:
329
+
330
+ ```bash
331
+ # Run all tests
332
+ bundle exec rspec
333
+
334
+ # Run specific test suites
335
+ bundle exec rspec spec/models/
336
+ bundle exec rspec spec/controllers/
337
+ ```
338
+
339
+ ## Security Considerations
340
+
341
+ - All passwords are hashed using bcrypt
342
+ - JWT tokens are signed and verified
343
+ - CSRF protection enabled for web requests
344
+ - Secure session management with proper expiry
345
+ - Client secrets are rotatable with audit trail
346
+ - PKCE support for public clients
347
+ - Rate limiting on authentication endpoints
348
+
24
349
  ## Contributing
25
- Contribution directions go here.
350
+
351
+ 1. Fork the repository
352
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
353
+ 3. Write tests for your changes
354
+ 4. Ensure all tests pass (`bin/rspec`)
355
+ 5. Commit your changes (`git commit -am 'Add amazing feature'`)
356
+ 6. Push to the branch (`git push origin feature/amazing-feature`)
357
+ 7. Open a Pull Request
26
358
 
27
359
  ## License
360
+
28
361
  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
@@ -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
@@ -10,6 +10,8 @@ module StandardId
10
10
  # Shared validations
11
11
  validates :value, presence: true, uniqueness: { scope: [:account_id, :type] }
12
12
 
13
+ after_commit :mark_account_verified!, on: :update, if: :just_verified?
14
+
13
15
  def verified?
14
16
  verified_at.present?
15
17
  end
@@ -21,5 +23,20 @@ module StandardId
21
23
  def unverify!
22
24
  update!(verified_at: nil)
23
25
  end
26
+
27
+ private
28
+
29
+ def just_verified?
30
+ saved_change_to_verified_at? && verified_at.present?
31
+ end
32
+
33
+ def mark_account_verified!
34
+ return if account.nil?
35
+
36
+ return unless account.has_attribute?(:verified)
37
+ return unless account.has_attribute?(:verified_at)
38
+
39
+ account.update!(verified: true, verified_at: Time.current)
40
+ end
24
41
  end
25
42
  end