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.
- checksums.yaml +4 -4
- data/README.md +340 -7
- data/app/controllers/standard_id/api/providers_controller.rb +15 -16
- data/app/controllers/standard_id/web/verify_email/base_controller.rb +9 -0
- data/app/controllers/standard_id/web/verify_email/confirm_controller.rb +41 -0
- data/app/controllers/standard_id/web/verify_email/start_controller.rb +39 -0
- data/app/controllers/standard_id/web/verify_phone/base_controller.rb +9 -0
- data/app/controllers/standard_id/web/verify_phone/confirm_controller.rb +41 -0
- data/app/controllers/standard_id/web/verify_phone/start_controller.rb +39 -0
- data/app/forms/standard_id/web/signup_form.rb +3 -14
- data/app/models/standard_id/{passwordless_challenge.rb → code_challenge.rb} +9 -5
- data/app/models/standard_id/email_identifier.rb +2 -0
- data/app/models/standard_id/identifier.rb +17 -0
- data/app/models/standard_id/phone_number_identifier.rb +2 -0
- data/app/models/standard_id/username_identifier.rb +2 -0
- data/config/routes/web.rb +12 -0
- data/db/migrate/20250830000000_create_standard_id_client_applications.rb +3 -3
- data/db/migrate/20250830232800_create_standard_id_identifiers.rb +1 -1
- data/db/migrate/20250831075703_create_standard_id_credentials.rb +2 -2
- data/db/migrate/20250831154635_create_standard_id_sessions.rb +2 -2
- data/db/migrate/20250901134520_create_standard_id_client_secret_credentials.rb +2 -2
- data/db/migrate/20250907090000_create_standard_id_code_challenges.rb +29 -0
- data/lib/generators/standard_id/install/templates/standard_id.rb +31 -3
- data/lib/{standard_id → standard_config}/config.rb +1 -1
- data/lib/standard_config/config_provider.rb +82 -0
- data/lib/standard_config/manager.rb +86 -0
- data/lib/standard_config/schema.rb +137 -0
- data/lib/standard_config.rb +38 -0
- data/lib/standard_id/api/session_manager.rb +1 -1
- data/lib/standard_id/config/schema.rb +48 -0
- data/lib/standard_id/oauth/passwordless_otp_flow.rb +7 -6
- data/lib/standard_id/passwordless/base_strategy.rb +8 -4
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +12 -4
- metadata +15 -4
- data/db/migrate/20250903135906_create_standard_id_passwordless_challenges.rb +0 -22
data/config/routes/web.rb
CHANGED
|
@@ -19,6 +19,18 @@ StandardId::WebEngine.routes.draw do
|
|
|
19
19
|
resource :confirm, only: [:show, :update], controller: :confirm
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Identifier verification (email)
|
|
23
|
+
namespace :verify_email do
|
|
24
|
+
resource :start, only: [:show, :create], controller: :start
|
|
25
|
+
resource :confirm, only: [:show, :update], controller: :confirm
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Identifier verification (phone)
|
|
29
|
+
namespace :verify_phone do
|
|
30
|
+
resource :start, only: [:show, :create], controller: :start
|
|
31
|
+
resource :confirm, only: [:show, :update], controller: :confirm
|
|
32
|
+
end
|
|
33
|
+
|
|
22
34
|
# Account management
|
|
23
35
|
resource :account, only: [:show, :edit, :update], controller: :account
|
|
24
36
|
resources :sessions, only: [:index, :destroy], controller: :sessions
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class CreateStandardIdClientApplications < ActiveRecord::Migration[7.1]
|
|
2
2
|
def change
|
|
3
|
-
create_table :standard_id_client_applications do |t|
|
|
3
|
+
create_table :standard_id_client_applications, id: primary_key_type do |t|
|
|
4
4
|
# Polymorphic owner association (Account, Organization, etc.)
|
|
5
|
-
t.references :owner, null: false, polymorphic: true, index: true
|
|
5
|
+
t.references :owner, type: primary_key_type, null: false, polymorphic: true, index: true
|
|
6
6
|
|
|
7
7
|
# Basic client information
|
|
8
8
|
t.string :name, null: false
|
|
@@ -50,7 +50,7 @@ class CreateStandardIdClientApplications < ActiveRecord::Migration[7.1]
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
if connection.adapter_name.downcase.include?("postgres")
|
|
53
|
-
add_index :
|
|
53
|
+
add_index :standard_id_client_applications, :metadata, if_not_exists: true, using: :gin
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class CreateStandardIdIdentifiers < ActiveRecord::Migration[8.0]
|
|
2
2
|
def change
|
|
3
3
|
create_table :standard_id_identifiers, id: primary_key_type do |t|
|
|
4
|
-
t.references :account, null: false, foreign_key: { to_table: StandardId.
|
|
4
|
+
t.references :account, type: primary_key_type, null: false, foreign_key: { to_table: StandardId.account_class.table_name }, index: true
|
|
5
5
|
|
|
6
6
|
t.string :type, null: false
|
|
7
7
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
class CreateStandardIdCredentials < ActiveRecord::Migration[8.0]
|
|
2
2
|
def change
|
|
3
3
|
create_table :standard_id_credentials, id: primary_key_type do |t|
|
|
4
|
-
t.references :identifier, null: false, foreign_key: { to_table: :standard_id_identifiers }, index: true
|
|
5
|
-
t.references :credentialable, null: false, polymorphic: true, index: true
|
|
4
|
+
t.references :identifier, type: primary_key_type, null: false, foreign_key: { to_table: :standard_id_identifiers }, index: true
|
|
5
|
+
t.references :credentialable, type: primary_key_type, null: false, polymorphic: true, index: true
|
|
6
6
|
|
|
7
7
|
t.timestamps
|
|
8
8
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
class CreateStandardIdSessions < ActiveRecord::Migration[8.0]
|
|
2
2
|
def change
|
|
3
3
|
create_table :standard_id_sessions, id: primary_key_type do |t|
|
|
4
|
-
t.references :account, null: false, foreign_key: true, index: true
|
|
4
|
+
t.references :account, type: primary_key_type, null: false, foreign_key: true, index: true
|
|
5
5
|
|
|
6
6
|
# STI type column
|
|
7
7
|
t.string :type, null: false, index: true
|
|
@@ -29,7 +29,7 @@ class CreateStandardIdSessions < ActiveRecord::Migration[8.0]
|
|
|
29
29
|
t.datetime :last_refreshed_at
|
|
30
30
|
|
|
31
31
|
# ServiceSession columns
|
|
32
|
-
t.references :owner, polymorphic: true, null: true, index: true
|
|
32
|
+
t.references :owner, type: primary_key_type, polymorphic: true, null: true, index: true
|
|
33
33
|
t.string :service_name
|
|
34
34
|
t.string :service_version
|
|
35
35
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
class CreateStandardIdClientSecretCredentials < ActiveRecord::Migration[7.1]
|
|
2
2
|
def change
|
|
3
|
-
create_table :standard_id_client_secret_credentials do |t|
|
|
3
|
+
create_table :standard_id_client_secret_credentials, id: primary_key_type do |t|
|
|
4
4
|
t.string :name, null: false
|
|
5
5
|
|
|
6
|
-
t.references :client_application, null: false, foreign_key: { to_table: :standard_id_client_applications }, index: true
|
|
6
|
+
t.references :client_application, type: primary_key_type, null: false, foreign_key: { to_table: :standard_id_client_applications }, index: true
|
|
7
7
|
|
|
8
8
|
t.string :client_id, null: false, index: true # Denormalized for performance
|
|
9
9
|
t.string :client_secret_digest, null: false
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class CreateStandardIdCodeChallenges < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :standard_id_code_challenges, id: primary_key_type do |t|
|
|
4
|
+
t.string :realm, null: false # e.g., authentication, verification
|
|
5
|
+
t.string :channel, null: false # e.g., email, sms
|
|
6
|
+
t.string :target, null: false # recipient address (email/phone), normalized by caller
|
|
7
|
+
t.string :code, null: false
|
|
8
|
+
|
|
9
|
+
t.datetime :expires_at, null: false
|
|
10
|
+
t.datetime :used_at
|
|
11
|
+
|
|
12
|
+
t.string :ip_address
|
|
13
|
+
t.text :user_agent
|
|
14
|
+
|
|
15
|
+
if connection.adapter_name.downcase.include?("postgres")
|
|
16
|
+
t.jsonb :metadata, default: {}, null: false
|
|
17
|
+
t.index :metadata, using: :gin
|
|
18
|
+
else
|
|
19
|
+
t.json :metadata, default: {}, null: false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
t.timestamps
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
add_index :standard_id_code_challenges, [:realm, :channel, :target, :code], name: "index_code_challenges_on_lookup"
|
|
26
|
+
add_index :standard_id_code_challenges, :expires_at
|
|
27
|
+
add_index :standard_id_code_challenges, :used_at
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -2,10 +2,38 @@
|
|
|
2
2
|
# Generated by: rails g standard_id:install
|
|
3
3
|
|
|
4
4
|
StandardId.configure do |c|
|
|
5
|
-
#
|
|
5
|
+
# Base configuration (account model, infrastructure settings)
|
|
6
6
|
c.account_class_name = "User"
|
|
7
|
-
|
|
8
|
-
# Optional: customize cache store and logger used internally by StandardId
|
|
9
7
|
# c.cache_store = Rails.cache
|
|
10
8
|
# c.logger = Rails.logger
|
|
9
|
+
# c.web_layout = "application"
|
|
10
|
+
# c.passwordless_email_sender = ->(email, code) { PasswordlessMailer.with(code: code, to: email).deliver_later }
|
|
11
|
+
# c.passwordless_sms_sender = ->(phone, code) { SmsProvider.send_code(phone: phone, code: code) }
|
|
12
|
+
|
|
13
|
+
# Configuration subsets (creates static OpenStruct objects)
|
|
14
|
+
# c.passwordless.code_ttl = 600 # 10 minutes
|
|
15
|
+
# c.passwordless.max_attempts = 3
|
|
16
|
+
# c.password.minimum_length = 8
|
|
17
|
+
# c.password.require_special_chars = true
|
|
18
|
+
# c.oauth.default_token_lifetime = 3600 # 1 hour
|
|
19
|
+
|
|
20
|
+
# Social login credentials (if enabled in your app)
|
|
21
|
+
# c.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
|
|
22
|
+
# c.social.google_client_secret = ENV["GOOGLE_CLIENT_SECRET"]
|
|
23
|
+
# c.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
|
|
24
|
+
# c.social.apple_private_key = ENV["APPLE_PRIVATE_KEY"]
|
|
25
|
+
# c.social.apple_key_id = ENV["APPLE_KEY_ID"]
|
|
26
|
+
# c.social.apple_team_id = ENV["APPLE_TEAM_ID"]
|
|
27
|
+
|
|
28
|
+
# OIDC Logout allow list
|
|
29
|
+
# c.allowed_post_logout_redirect_uris = [
|
|
30
|
+
# "https://app.example.com/logged_out",
|
|
31
|
+
# "https://admin.example.com/signout"
|
|
32
|
+
# ]
|
|
11
33
|
end
|
|
34
|
+
|
|
35
|
+
# Notes:
|
|
36
|
+
# - All configuration scopes and fields are defined via the schema DSL
|
|
37
|
+
# in: lib/standard_id/config/schema.rb
|
|
38
|
+
# - Values are automatically cast to the correct type (string, integer, boolean, etc.)
|
|
39
|
+
# - Invalid scopes or fields will raise ArgumentError with helpful messages
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "ostruct"
|
|
2
|
+
|
|
3
|
+
module StandardConfig
|
|
4
|
+
class ConfigProvider
|
|
5
|
+
def initialize(scope_name, resolver_proc, schema = nil)
|
|
6
|
+
@scope_name = scope_name
|
|
7
|
+
@resolver_proc = resolver_proc
|
|
8
|
+
@schema = schema
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def method_missing(method_name, *args)
|
|
12
|
+
if method_name.to_s.end_with?('=')
|
|
13
|
+
# Setter - only works for static configs (OpenStruct objects)
|
|
14
|
+
field_name = method_name.to_s.chomp('=').to_sym
|
|
15
|
+
validate_field!(field_name)
|
|
16
|
+
|
|
17
|
+
config_object = @resolver_proc.call
|
|
18
|
+
if config_object.respond_to?(method_name)
|
|
19
|
+
config_object.send(method_name, args.first)
|
|
20
|
+
elsif config_object.respond_to?(:[]=)
|
|
21
|
+
# Support hash-like providers
|
|
22
|
+
value = args.first
|
|
23
|
+
config_object[field_name] = value
|
|
24
|
+
# Also set string key for convenience if symbol not used by provider
|
|
25
|
+
begin
|
|
26
|
+
config_object[field_name.to_s] = value
|
|
27
|
+
rescue StandardError
|
|
28
|
+
# ignore if provider doesn't accept string keys
|
|
29
|
+
end
|
|
30
|
+
else
|
|
31
|
+
raise NoMethodError, "Configuration object doesn't support setting #{field_name}"
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
# Getter
|
|
35
|
+
get_field(method_name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def get_field(field_name)
|
|
40
|
+
validate_field!(field_name)
|
|
41
|
+
|
|
42
|
+
config_object = @resolver_proc.call
|
|
43
|
+
raw_value = if config_object.respond_to?(field_name)
|
|
44
|
+
config_object.send(field_name)
|
|
45
|
+
elsif config_object.respond_to?(:[])
|
|
46
|
+
config_object[field_name] || config_object[field_name.to_s]
|
|
47
|
+
else
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Cast the value according to schema
|
|
52
|
+
field_def = @schema&.field_definition(@scope_name, field_name)
|
|
53
|
+
return raw_value unless field_def
|
|
54
|
+
|
|
55
|
+
casted = @schema&.cast_value(raw_value, field_def.type) || raw_value
|
|
56
|
+
# Return dup for mutable structures to prevent accidental mutation of shared defaults
|
|
57
|
+
if casted.is_a?(Array)
|
|
58
|
+
casted.dup
|
|
59
|
+
elsif casted.is_a?(Hash)
|
|
60
|
+
casted.dup
|
|
61
|
+
else
|
|
62
|
+
casted
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
67
|
+
field_name = method_name.to_s.end_with?('=') ? method_name.to_s.chomp('=').to_sym : method_name.to_sym
|
|
68
|
+
@schema&.valid_field?(@scope_name, field_name) || super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def validate_field!(field_name)
|
|
74
|
+
return unless @schema # Skip validation if no schema provided
|
|
75
|
+
|
|
76
|
+
unless @schema.valid_field?(@scope_name, field_name)
|
|
77
|
+
valid_fields = @schema.scopes[@scope_name]&.fields&.keys || []
|
|
78
|
+
raise ArgumentError, "Unknown field '#{field_name}' for scope '#{@scope_name}'. Valid fields: #{valid_fields}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
require "ostruct"
|
|
2
|
+
require "standard_config/config_provider"
|
|
3
|
+
|
|
4
|
+
module StandardConfig
|
|
5
|
+
class Manager
|
|
6
|
+
def initialize(schema)
|
|
7
|
+
@schema = schema
|
|
8
|
+
@providers = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Register a configuration provider for a scope
|
|
12
|
+
def register(scope_name, resolver_proc)
|
|
13
|
+
scope_name = scope_name.to_sym
|
|
14
|
+
|
|
15
|
+
# Validate scope exists in schema
|
|
16
|
+
unless @schema.valid_scope?(scope_name)
|
|
17
|
+
raise ArgumentError, "Unknown configuration scope: #{scope_name}. Valid scopes: #{@schema.scopes.keys}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
@providers[scope_name] = ConfigProvider.new(scope_name, resolver_proc, @schema)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def registered?(scope_name)
|
|
25
|
+
@providers.key?(scope_name.to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Access configuration scopes via method calls
|
|
29
|
+
def method_missing(method_name, *args)
|
|
30
|
+
method_str = method_name.to_s
|
|
31
|
+
scope_name = method_str.end_with?("=") ? method_str.chomp("=").to_sym : method_name.to_sym
|
|
32
|
+
|
|
33
|
+
# Handle field setter via unique scope resolution
|
|
34
|
+
if method_str.end_with?("=")
|
|
35
|
+
field = scope_name
|
|
36
|
+
scopes = @schema.scopes_with_field(field)
|
|
37
|
+
if scopes.size == 1
|
|
38
|
+
s = scopes.first
|
|
39
|
+
register(s, -> { create_static_config_for_scope(s) }) unless @providers.key?(s)
|
|
40
|
+
@providers[s].public_send(method_name, *args)
|
|
41
|
+
return args.first
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Handle field getter via unique scope resolution
|
|
46
|
+
scopes = @schema.scopes_with_field(scope_name)
|
|
47
|
+
if scopes.size == 1
|
|
48
|
+
s = scopes.first
|
|
49
|
+
register(s, -> { create_static_config_for_scope(s) }) unless @providers.key?(s)
|
|
50
|
+
return @providers[s].get_field(scope_name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Handle scope access
|
|
54
|
+
if @providers.key?(scope_name)
|
|
55
|
+
return @providers[scope_name]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Create static provider for valid scopes on first access
|
|
59
|
+
if @schema.valid_scope?(scope_name)
|
|
60
|
+
register(scope_name, -> { create_static_config_for_scope(scope_name) })
|
|
61
|
+
return @providers[scope_name]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
68
|
+
method_str = method_name.to_s
|
|
69
|
+
scope_name = method_str.end_with?("=") ? method_str.chomp("=").to_sym : method_name.to_sym
|
|
70
|
+
@schema.valid_scope?(scope_name) ||
|
|
71
|
+
@schema.scopes_with_field(scope_name).any? ||
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def create_static_config_for_scope(scope_name)
|
|
78
|
+
@static_configs ||= {}
|
|
79
|
+
@static_configs[scope_name] ||= OpenStruct.new.tap do |config|
|
|
80
|
+
@schema.scopes[scope_name].fields.each do |field_name, field_def|
|
|
81
|
+
config.send("#{field_name}=", field_def.default_value)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
module StandardConfig
|
|
2
|
+
class Schema
|
|
3
|
+
def initialize
|
|
4
|
+
@scopes = {}
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# DSL entry
|
|
8
|
+
def draw(&block)
|
|
9
|
+
Drawer.new(self).instance_eval(&block) if block_given?
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def scopes
|
|
14
|
+
@scopes
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def scope(name, &block)
|
|
18
|
+
name_sym = name.to_sym
|
|
19
|
+
builder = scopes[name_sym] ||= ScopeBuilder.new(name_sym)
|
|
20
|
+
builder.instance_eval(&block) if block_given?
|
|
21
|
+
builder
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def valid_scope?(name)
|
|
25
|
+
scopes.key?(name.to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def valid_field?(scope_name, field_name)
|
|
29
|
+
return false unless valid_scope?(scope_name)
|
|
30
|
+
scopes[scope_name.to_sym].fields.key?(field_name.to_sym)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def field_definition(scope_name, field_name)
|
|
34
|
+
return nil unless valid_scope?(scope_name)
|
|
35
|
+
scopes[scope_name.to_sym].fields[field_name.to_sym]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return an array of scope names that define the given field
|
|
39
|
+
def scopes_with_field(field_name)
|
|
40
|
+
scopes.keys.select { |s| scopes[s].fields.key?(field_name.to_sym) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def cast_value(value, type)
|
|
44
|
+
return value if value.nil?
|
|
45
|
+
|
|
46
|
+
case type
|
|
47
|
+
when :any
|
|
48
|
+
value
|
|
49
|
+
when :string
|
|
50
|
+
value.to_s
|
|
51
|
+
when :integer
|
|
52
|
+
value.to_i
|
|
53
|
+
when :float
|
|
54
|
+
value.to_f
|
|
55
|
+
when :boolean
|
|
56
|
+
case value
|
|
57
|
+
when true, false then value
|
|
58
|
+
when 'true', '1', 1 then true
|
|
59
|
+
when 'false', '0', 0 then false
|
|
60
|
+
else !!value
|
|
61
|
+
end
|
|
62
|
+
when :array
|
|
63
|
+
Array(value)
|
|
64
|
+
when :hash
|
|
65
|
+
value.is_a?(Hash) ? value : {}
|
|
66
|
+
else
|
|
67
|
+
value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class ScopeBuilder
|
|
72
|
+
attr_reader :name, :fields
|
|
73
|
+
|
|
74
|
+
def initialize(name)
|
|
75
|
+
@name = name.to_sym
|
|
76
|
+
@fields = {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def field(name, type: :string, default: nil, readonly: false)
|
|
80
|
+
key = name.to_sym
|
|
81
|
+
if @fields.key?(key)
|
|
82
|
+
Kernel.warn("[StandardId::Configuration] Redefining field '#{key}' in scope '#{@name}'. Last definition wins.")
|
|
83
|
+
end
|
|
84
|
+
@fields[key] = FieldDefinition.new(name, type: type, default: default, readonly: readonly)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class FieldDefinition
|
|
89
|
+
attr_reader :name, :type, :default, :readonly
|
|
90
|
+
|
|
91
|
+
def initialize(name, type: :string, default: nil, readonly: false)
|
|
92
|
+
@name = name.to_sym
|
|
93
|
+
@type = type
|
|
94
|
+
@default = default
|
|
95
|
+
@readonly = readonly
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def default_value
|
|
99
|
+
if @default.respond_to?(:call)
|
|
100
|
+
@default.call
|
|
101
|
+
elsif @default.is_a?(Array)
|
|
102
|
+
@default.dup
|
|
103
|
+
else
|
|
104
|
+
@default
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Internal DSL driver
|
|
110
|
+
class Drawer
|
|
111
|
+
def initialize(schema)
|
|
112
|
+
@schema = schema
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# scope :base do ... end OR scope :passwordless do ... end
|
|
116
|
+
def scope(name, &block)
|
|
117
|
+
name_sym = name.to_sym
|
|
118
|
+
# Ensure scope exists, then evaluate the block in a scoped context
|
|
119
|
+
@schema.scope(name_sym)
|
|
120
|
+
ScopedScope.new(@schema, name_sym).instance_eval(&block) if block_given?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class ScopedScope
|
|
125
|
+
def initialize(schema, scope_name)
|
|
126
|
+
@schema = schema
|
|
127
|
+
@scope_name = scope_name
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def field(name, type: :string, default: nil, readonly: false)
|
|
131
|
+
# Add field to the last declared scope by using ScopeBuilder within @schema.scope
|
|
132
|
+
# This method will be called inside Schema.scope block via Drawer
|
|
133
|
+
@schema.scopes[@scope_name].field(name, type: type, default: default, readonly: readonly)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "standard_config/config"
|
|
2
|
+
require "standard_config/config_provider"
|
|
3
|
+
require "standard_config/manager"
|
|
4
|
+
require "standard_config/schema"
|
|
5
|
+
|
|
6
|
+
module StandardConfig
|
|
7
|
+
class << self
|
|
8
|
+
def schema
|
|
9
|
+
@schema ||= Schema.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def configure(&block)
|
|
13
|
+
config.register(:base, block) unless config.registered?(:base) if block_given? && block.arity == 0
|
|
14
|
+
|
|
15
|
+
yield config if block_given?
|
|
16
|
+
|
|
17
|
+
config
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def config
|
|
21
|
+
@manager ||= Manager.new(schema)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def create_default_config
|
|
27
|
+
require "ostruct"
|
|
28
|
+
static_config = OpenStruct.new
|
|
29
|
+
base_scope = schema.scopes[:base]
|
|
30
|
+
if base_scope
|
|
31
|
+
base_scope.fields.each do |field_name, field_def|
|
|
32
|
+
static_config.send("#{field_name}=", field_def.default_value)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
static_config
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -12,7 +12,7 @@ module StandardId
|
|
|
12
12
|
|
|
13
13
|
def current_account
|
|
14
14
|
return unless current_session
|
|
15
|
-
@current_account ||= StandardId.
|
|
15
|
+
@current_account ||= StandardId.account_class.find_by(id: current_session.account_id)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def revoke_current_session!
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Schema definitions for StandardId
|
|
2
|
+
# This file defines the configuration schema structure
|
|
3
|
+
|
|
4
|
+
require "standard_config"
|
|
5
|
+
|
|
6
|
+
StandardConfig.schema.draw do
|
|
7
|
+
scope :base do
|
|
8
|
+
field :account_class_name, type: :string, default: "User"
|
|
9
|
+
field :cache_store, type: :any, default: nil
|
|
10
|
+
field :logger, type: :any, default: nil
|
|
11
|
+
field :web_layout, type: :string, default: nil
|
|
12
|
+
field :passwordless_email_sender, type: :any, default: nil
|
|
13
|
+
field :passwordless_sms_sender, type: :any, default: nil
|
|
14
|
+
field :issuer, type: :string, default: nil
|
|
15
|
+
field :login_url, type: :string, default: nil
|
|
16
|
+
field :allowed_post_logout_redirect_uris, type: :array, default: []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
scope :passwordless do
|
|
20
|
+
field :code_ttl, type: :integer, default: 600 # 10 minutes in seconds
|
|
21
|
+
field :max_attempts, type: :integer, default: 3
|
|
22
|
+
field :retry_delay, type: :integer, default: 30 # 30 seconds
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
scope :password do
|
|
26
|
+
field :minimum_length, type: :integer, default: 8
|
|
27
|
+
field :require_special_chars, type: :boolean, default: false
|
|
28
|
+
field :require_uppercase, type: :boolean, default: false
|
|
29
|
+
field :require_numbers, type: :boolean, default: false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
scope :oauth do
|
|
33
|
+
field :default_token_lifetime, type: :integer, default: 3600 # 1 hour in seconds
|
|
34
|
+
field :refresh_token_lifetime, type: :integer, default: 2592000 # 30 days in seconds
|
|
35
|
+
field :client_id, type: :string, default: nil
|
|
36
|
+
field :client_secret, type: :string, default: nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
scope :social do
|
|
40
|
+
field :google_client_id, type: :string, default: nil
|
|
41
|
+
field :google_client_secret, type: :string, default: nil
|
|
42
|
+
field :apple_client_id, type: :string, default: nil
|
|
43
|
+
field :apple_client_secret, type: :string, default: nil
|
|
44
|
+
field :apple_private_key, type: :string, default: nil
|
|
45
|
+
field :apple_key_id, type: :string, default: nil
|
|
46
|
+
field :apple_team_id, type: :string, default: nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -7,12 +7,12 @@ module StandardId
|
|
|
7
7
|
def authenticate!
|
|
8
8
|
validate_client_secret!(params[:client_id], params[:client_secret]) if params[:client_secret].present?
|
|
9
9
|
|
|
10
|
-
raise StandardId::InvalidGrantError, "Invalid or expired verification code" if
|
|
10
|
+
raise StandardId::InvalidGrantError, "Invalid or expired verification code" if code_challenge.blank?
|
|
11
11
|
raise StandardId::InvalidGrantError, "Unable to authenticate user" if account.blank?
|
|
12
12
|
|
|
13
13
|
validate_requested_scope!
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
code_challenge.use!
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
private
|
|
@@ -45,10 +45,11 @@ module StandardId
|
|
|
45
45
|
1.hour
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def
|
|
49
|
-
@
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
def code_challenge
|
|
49
|
+
@code_challenge ||= StandardId::CodeChallenge.active.find_by(
|
|
50
|
+
realm: "authentication",
|
|
51
|
+
channel: params[:connection],
|
|
52
|
+
target: params[:username],
|
|
52
53
|
code: params[:otp]
|
|
53
54
|
)
|
|
54
55
|
end
|
|
@@ -24,14 +24,18 @@ module StandardId
|
|
|
24
24
|
protected
|
|
25
25
|
|
|
26
26
|
def create_challenge!(username)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
code = generate_otp_code
|
|
28
|
+
|
|
29
|
+
cc = StandardId::CodeChallenge.create!(
|
|
30
|
+
realm: "authentication",
|
|
31
|
+
channel: connection_type,
|
|
32
|
+
target: username,
|
|
33
|
+
code: code,
|
|
31
34
|
expires_at: 10.minutes.from_now,
|
|
32
35
|
ip_address: request.remote_ip,
|
|
33
36
|
user_agent: request.user_agent
|
|
34
37
|
)
|
|
38
|
+
cc
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
def generate_otp_code
|
data/lib/standard_id/version.rb
CHANGED