standard_id 0.1.1 → 0.1.3
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 +31 -3
- data/app/controllers/concerns/standard_id/api_authentication.rb +4 -0
- data/app/models/standard_id/client_application.rb +2 -1
- data/lib/generators/standard_id/install/templates/standard_id.rb +14 -0
- data/lib/standard_id/api/authentication_guard.rb +36 -0
- data/lib/standard_id/config/schema.rb +3 -0
- data/lib/standard_id/jwt_service.rb +19 -5
- data/lib/standard_id/oauth/authorization_code_flow.rb +8 -0
- data/lib/standard_id/oauth/client_credentials_flow.rb +7 -3
- data/lib/standard_id/oauth/implicit_authorization_flow.rb +1 -1
- data/lib/standard_id/oauth/password_flow.rb +4 -4
- data/lib/standard_id/oauth/passwordless_otp_flow.rb +4 -4
- data/lib/standard_id/oauth/token_grant_flow.rb +73 -3
- data/lib/standard_id/oauth/token_lifetime_resolver.rb +50 -0
- data/lib/standard_id/version.rb +1 -1
- data/lib/standard_id.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70f439b35a454065b4a2e45dfedc85b7316b26b0140d6f005930fc909ac78efd
|
|
4
|
+
data.tar.gz: bbf731529fdb2c0cb979e6fecfa711e83daaa1fddfd111263d44618b38bd8fa4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1d1c0de85827c32f47680adad39da9457528b24376079c154aced6bd46fe40fbe72918df27a9290f54ef6df58e862b251dad7568d16d1abe5dd45c2b477bd45
|
|
7
|
+
data.tar.gz: 8d3a5b5c14e42b1850969b6268728ff47c0b78eb5e482f94b0ad4e2b752e98addc52267588adac384d82d01a207047b1e15b25bc9640c56282d5f67c0f297bd0
|
data/README.md
CHANGED
|
@@ -87,12 +87,12 @@ end
|
|
|
87
87
|
```ruby
|
|
88
88
|
# For web controllers
|
|
89
89
|
class ApplicationController < ActionController::Base
|
|
90
|
-
include StandardId::
|
|
90
|
+
include StandardId::WebAuthentication
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# For API controllers
|
|
94
94
|
class ApiController < ActionController::API
|
|
95
|
-
include StandardId::
|
|
95
|
+
include StandardId::ApiAuthentication
|
|
96
96
|
end
|
|
97
97
|
```
|
|
98
98
|
|
|
@@ -123,9 +123,37 @@ StandardId.configure do |config|
|
|
|
123
123
|
# config.password.require_special_chars = true
|
|
124
124
|
# config.passwordless.code_ttl = 600
|
|
125
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
|
+
# }
|
|
126
131
|
end
|
|
127
132
|
```
|
|
128
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
|
+
### Custom Token Claims
|
|
137
|
+
|
|
138
|
+
You can add additional JWT claims for any token issued through the OAuth token endpoint by mapping scopes to claim names and providing callbacks to resolve each claim. Scopes listed in `oauth.scope_claims` are evaluated against the requested token scopes; when a scope matches, every claim listed for that scope is resolved via the callable defined in `oauth.claim_resolvers`.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
StandardId.configure do |config|
|
|
142
|
+
config.oauth.scope_claims = {
|
|
143
|
+
profile: %i[email display_name]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
config.oauth.claim_resolvers = {
|
|
147
|
+
email: ->(account:) { account.email },
|
|
148
|
+
display_name: ->(account:, client:) {
|
|
149
|
+
"#{account.name} for #{client.client_id}"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Resolvers receive keyword arguments with the context containing `client`, `account`, and `request`, so you can reference only what you need. This lets you, for example, pull organization info off the client application or decorate claims with account attributes.
|
|
156
|
+
|
|
129
157
|
### Social Login Setup
|
|
130
158
|
|
|
131
159
|
```ruby
|
|
@@ -274,7 +302,7 @@ StandardId creates the following tables:
|
|
|
274
302
|
|
|
275
303
|
```ruby
|
|
276
304
|
# Create OAuth client
|
|
277
|
-
client = StandardId::
|
|
305
|
+
client = StandardId::ClientApplication.create!(
|
|
278
306
|
owner: current_account,
|
|
279
307
|
name: "My Application",
|
|
280
308
|
redirect_uris: "https://app.com/callback",
|
|
@@ -14,6 +14,10 @@ module StandardId
|
|
|
14
14
|
authentication_guard.require_session!(session_manager)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def require_scopes!(*required_scopes)
|
|
18
|
+
authentication_guard.require_scopes!(session_manager, *required_scopes)
|
|
19
|
+
end
|
|
20
|
+
|
|
17
21
|
def session_manager
|
|
18
22
|
@session_manager ||= StandardId::Api::SessionManager.new(token_manager, request:)
|
|
19
23
|
end
|
|
@@ -16,6 +16,20 @@ StandardId.configure do |c|
|
|
|
16
16
|
# c.password.minimum_length = 8
|
|
17
17
|
# c.password.require_special_chars = true
|
|
18
18
|
# c.oauth.default_token_lifetime = 3600 # 1 hour
|
|
19
|
+
# c.oauth.refresh_token_lifetime = 2_592_000 # 30 days
|
|
20
|
+
# c.oauth.token_lifetimes = {
|
|
21
|
+
# password: 8.hours,
|
|
22
|
+
# client_credentials: 24.hours
|
|
23
|
+
# }
|
|
24
|
+
# c.oauth.scope_claims = {
|
|
25
|
+
# profile: %i[email display_name]
|
|
26
|
+
# }
|
|
27
|
+
# c.oauth.claim_resolvers = {
|
|
28
|
+
# email: ->(account:) { account.email },
|
|
29
|
+
# display_name: ->(account:, client:) {
|
|
30
|
+
# "#{account.name} for #{client.client_id}"
|
|
31
|
+
# }
|
|
32
|
+
# }
|
|
19
33
|
|
|
20
34
|
# Social login credentials (if enabled in your app)
|
|
21
35
|
# c.social.google_client_id = ENV["GOOGLE_CLIENT_ID"]
|
|
@@ -15,6 +15,42 @@ module StandardId
|
|
|
15
15
|
|
|
16
16
|
api_session
|
|
17
17
|
end
|
|
18
|
+
|
|
19
|
+
def require_scopes!(session_manager, *required_scopes)
|
|
20
|
+
api_session = require_session!(session_manager)
|
|
21
|
+
|
|
22
|
+
expected_scopes = normalize_scopes(required_scopes)
|
|
23
|
+
return api_session if expected_scopes.empty?
|
|
24
|
+
|
|
25
|
+
token_scopes = extract_session_scopes(api_session)
|
|
26
|
+
unless (token_scopes & expected_scopes).any?
|
|
27
|
+
raise StandardId::InvalidScopeError,
|
|
28
|
+
"Access token missing required scope. Requires one of: #{expected_scopes.join(', ')}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
api_session
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def extract_session_scopes(api_session)
|
|
37
|
+
api_session&.scopes || []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def normalize_scopes(required_scopes)
|
|
41
|
+
return [] if required_scopes.nil?
|
|
42
|
+
|
|
43
|
+
case required_scopes
|
|
44
|
+
when String
|
|
45
|
+
[required_scopes]
|
|
46
|
+
when Symbol
|
|
47
|
+
[required_scopes.to_s]
|
|
48
|
+
when Array
|
|
49
|
+
required_scopes.flat_map { |value| normalize_scopes(value) }.uniq
|
|
50
|
+
else
|
|
51
|
+
raise ArgumentError, "Scopes must be provided as a String, Symbol, or Array"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
18
54
|
end
|
|
19
55
|
end
|
|
20
56
|
end
|
|
@@ -32,8 +32,11 @@ StandardConfig.schema.draw do
|
|
|
32
32
|
scope :oauth do
|
|
33
33
|
field :default_token_lifetime, type: :integer, default: 3600 # 1 hour in seconds
|
|
34
34
|
field :refresh_token_lifetime, type: :integer, default: 2592000 # 30 days in seconds
|
|
35
|
+
field :token_lifetimes, type: :hash, default: -> { {} }
|
|
35
36
|
field :client_id, type: :string, default: nil
|
|
36
37
|
field :client_secret, type: :string, default: nil
|
|
38
|
+
field :scope_claims, type: :hash, default: -> { {} }
|
|
39
|
+
field :claim_resolvers, type: :hash, default: -> { {} }
|
|
37
40
|
end
|
|
38
41
|
|
|
39
42
|
scope :social do
|
|
@@ -3,9 +3,14 @@ require "jwt"
|
|
|
3
3
|
module StandardId
|
|
4
4
|
class JwtService
|
|
5
5
|
ALGORITHM = "HS256"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
RESERVED_JWT_KEYS = %i[sub client_id scope grant_type exp iat aud iss nbf jti]
|
|
7
|
+
BASE_SESSION_FIELDS = %i[account_id client_id scopes grant_type]
|
|
8
|
+
|
|
9
|
+
def self.session_class
|
|
10
|
+
Struct.new(*(BASE_SESSION_FIELDS + claim_resolver_keys), keyword_init: true) do
|
|
11
|
+
def active?
|
|
12
|
+
true
|
|
13
|
+
end
|
|
9
14
|
end
|
|
10
15
|
end
|
|
11
16
|
|
|
@@ -33,11 +38,12 @@ module StandardId
|
|
|
33
38
|
Array(payload[:scope]).compact
|
|
34
39
|
end
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
session_class.new(
|
|
42
|
+
**payload.slice(*claim_resolver_keys),
|
|
37
43
|
account_id: payload[:sub],
|
|
38
44
|
client_id: payload[:client_id],
|
|
39
45
|
scopes: scopes,
|
|
40
|
-
grant_type: payload[:grant_type]
|
|
46
|
+
grant_type: payload[:grant_type],
|
|
41
47
|
)
|
|
42
48
|
end
|
|
43
49
|
|
|
@@ -46,5 +52,13 @@ module StandardId
|
|
|
46
52
|
def self.secret_key
|
|
47
53
|
Rails.application.secret_key_base
|
|
48
54
|
end
|
|
55
|
+
|
|
56
|
+
def self.claim_resolver_keys
|
|
57
|
+
resolvers = StandardId.config.oauth.claim_resolvers
|
|
58
|
+
keys = Hash.try_convert(resolvers)&.keys
|
|
59
|
+
keys.compact.map(&:to_sym).uniq.excluding(*RESERVED_JWT_KEYS)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
49
63
|
end
|
|
50
64
|
end
|
|
@@ -48,6 +48,14 @@ module StandardId
|
|
|
48
48
|
def find_authorization_code(code)
|
|
49
49
|
StandardId::AuthorizationCode.lookup(code)
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
def token_client
|
|
53
|
+
@credential&.client_application
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def token_account
|
|
57
|
+
@authorization_code&.account
|
|
58
|
+
end
|
|
51
59
|
end
|
|
52
60
|
end
|
|
53
61
|
end
|
|
@@ -11,7 +11,7 @@ module StandardId
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def subject_id
|
|
14
|
-
@credential.
|
|
14
|
+
@credential.client_id
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def client_id
|
|
@@ -30,8 +30,12 @@ module StandardId
|
|
|
30
30
|
params[:audience]
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
|
|
33
|
+
def token_client
|
|
34
|
+
@credential&.client_application
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def token_account
|
|
38
|
+
nil
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
end
|
|
@@ -39,10 +39,6 @@ module StandardId
|
|
|
39
39
|
true
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
def token_expiry
|
|
43
|
-
8.hours # Longer expiry for user sessions
|
|
44
|
-
end
|
|
45
|
-
|
|
46
42
|
def authenticate_account(username, password)
|
|
47
43
|
StandardId::PasswordCredential
|
|
48
44
|
.includes(credential: :account)
|
|
@@ -65,6 +61,10 @@ module StandardId
|
|
|
65
61
|
def default_scope
|
|
66
62
|
"read"
|
|
67
63
|
end
|
|
64
|
+
|
|
65
|
+
def token_account
|
|
66
|
+
@account
|
|
67
|
+
end
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -41,10 +41,6 @@ module StandardId
|
|
|
41
41
|
true
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
-
def token_expiry
|
|
45
|
-
1.hour
|
|
46
|
-
end
|
|
47
|
-
|
|
48
44
|
def code_challenge
|
|
49
45
|
@code_challenge ||= StandardId::CodeChallenge.active.find_by(
|
|
50
46
|
realm: "authentication",
|
|
@@ -83,6 +79,10 @@ module StandardId
|
|
|
83
79
|
def default_scope
|
|
84
80
|
"read"
|
|
85
81
|
end
|
|
82
|
+
|
|
83
|
+
def token_account
|
|
84
|
+
account
|
|
85
|
+
end
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
end
|
|
@@ -52,17 +52,19 @@ module StandardId
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def build_jwt_payload(expires_in)
|
|
55
|
-
{
|
|
55
|
+
base_payload = {
|
|
56
56
|
sub: subject_id,
|
|
57
57
|
client_id: client_id,
|
|
58
58
|
scope: token_scope,
|
|
59
59
|
grant_type: grant_type,
|
|
60
60
|
aud: audience
|
|
61
61
|
}.compact
|
|
62
|
+
|
|
63
|
+
base_payload.merge(claims_from_scope_mapping)
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
def token_expiry
|
|
65
|
-
|
|
67
|
+
TokenLifetimeResolver.access_token_for(token_lifetime_key)
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
def supports_refresh_token?
|
|
@@ -80,7 +82,11 @@ module StandardId
|
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
def refresh_token_expiry
|
|
83
|
-
|
|
85
|
+
TokenLifetimeResolver.refresh_token_lifetime
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def token_lifetime_key
|
|
89
|
+
grant_type&.to_sym
|
|
84
90
|
end
|
|
85
91
|
|
|
86
92
|
def subject_id
|
|
@@ -102,6 +108,70 @@ module StandardId
|
|
|
102
108
|
def audience
|
|
103
109
|
params[:audience]
|
|
104
110
|
end
|
|
111
|
+
|
|
112
|
+
def claims_from_scope_mapping
|
|
113
|
+
scope_claims = StandardId.config.oauth.scope_claims.with_indifferent_access
|
|
114
|
+
resolvers = StandardId.config.oauth.claim_resolvers.with_indifferent_access
|
|
115
|
+
return {} if scope_claims.empty? || resolvers.empty?
|
|
116
|
+
|
|
117
|
+
claims = {}
|
|
118
|
+
current_scopes.each do |scope|
|
|
119
|
+
Array(scope_claims[scope]).each do |claim_key|
|
|
120
|
+
next if claims.key?(claim_key)
|
|
121
|
+
|
|
122
|
+
value = resolve_claim_value(resolvers[claim_key])
|
|
123
|
+
claims[claim_key] = value unless value.nil?
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
claims.compact.symbolize_keys
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def current_scopes
|
|
131
|
+
Array.wrap(token_scope)
|
|
132
|
+
.flat_map { |value| value.to_s.split(/\s+/) }
|
|
133
|
+
.reject(&:blank?)
|
|
134
|
+
.uniq
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def token_account
|
|
138
|
+
return nil if subject_id.blank?
|
|
139
|
+
|
|
140
|
+
account_class = StandardId.account_class
|
|
141
|
+
return nil unless account_class.respond_to?(:find_by)
|
|
142
|
+
|
|
143
|
+
account_class.find_by(id: subject_id)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def token_client
|
|
147
|
+
StandardId::ClientApplication.find_by(client_id: client_id)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def claim_resolvers_context
|
|
151
|
+
@claim_resolvers_context ||= {
|
|
152
|
+
client: token_client,
|
|
153
|
+
account: token_account,
|
|
154
|
+
request: request
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def callable_parameters(resolver)
|
|
159
|
+
parameters = if resolver.respond_to?(:parameters)
|
|
160
|
+
resolver.parameters
|
|
161
|
+
elsif resolver.respond_to?(:method) && resolver.respond_to?(:call)
|
|
162
|
+
resolver.method(:call).parameters
|
|
163
|
+
else
|
|
164
|
+
[]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
accepts_all = parameters.any? { |type, _| type == :keyrest }
|
|
168
|
+
|
|
169
|
+
accepts_all ? claim_resolvers_context.keys : parameters.map { |_, name| name.to_sym }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def resolve_claim_value(resolver)
|
|
173
|
+
resolver&.call(**claim_resolvers_context.slice(*callable_parameters(resolver)))
|
|
174
|
+
end
|
|
105
175
|
end
|
|
106
176
|
end
|
|
107
177
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module StandardId
|
|
2
|
+
module Oauth
|
|
3
|
+
class TokenLifetimeResolver
|
|
4
|
+
class << self
|
|
5
|
+
DEFAULT_ACCESS_TOKEN_LIFETIME = 1.hour.to_i
|
|
6
|
+
DEFAULT_REFRESH_TOKEN_LIFETIME = 30.days.to_i
|
|
7
|
+
|
|
8
|
+
def access_token_for(flow_key)
|
|
9
|
+
configured = lookup_token_lifetime(flow_key)
|
|
10
|
+
positive_seconds(configured, default_access_token_lifetime)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def refresh_token_lifetime
|
|
14
|
+
positive_seconds(oauth_config.refresh_token_lifetime, DEFAULT_REFRESH_TOKEN_LIFETIME)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def default_access_token_lifetime
|
|
20
|
+
positive_seconds(oauth_config.default_token_lifetime, DEFAULT_ACCESS_TOKEN_LIFETIME)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def lookup_token_lifetime(flow_key)
|
|
24
|
+
config = oauth_config
|
|
25
|
+
return nil unless config.respond_to?(:token_lifetimes)
|
|
26
|
+
|
|
27
|
+
lifetimes = config.token_lifetimes || {}
|
|
28
|
+
lifetimes[flow_key.to_sym] || lifetimes[flow_key.to_s] if flow_key
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def positive_seconds(value, fallback_value)
|
|
32
|
+
normalized_value = case value
|
|
33
|
+
when ActiveSupport::Duration
|
|
34
|
+
value.to_i
|
|
35
|
+
when Numeric, String
|
|
36
|
+
value.to_i
|
|
37
|
+
else
|
|
38
|
+
0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
(normalized_value.positive? ? normalized_value : fallback_value).seconds
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def oauth_config
|
|
45
|
+
StandardId.config.oauth
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/standard_id/version.rb
CHANGED
data/lib/standard_id.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "standard_id/api/session_manager"
|
|
|
12
12
|
require "standard_id/api/token_manager"
|
|
13
13
|
require "standard_id/api/authentication_guard"
|
|
14
14
|
require "standard_id/oauth/base_request_flow"
|
|
15
|
+
require "standard_id/oauth/token_lifetime_resolver"
|
|
15
16
|
require "standard_id/oauth/token_grant_flow"
|
|
16
17
|
require "standard_id/oauth/client_credentials_flow"
|
|
17
18
|
require "standard_id/oauth/authorization_code_flow"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- lib/standard_id/oauth/subflows/social_login_grant.rb
|
|
161
161
|
- lib/standard_id/oauth/subflows/traditional_code_grant.rb
|
|
162
162
|
- lib/standard_id/oauth/token_grant_flow.rb
|
|
163
|
+
- lib/standard_id/oauth/token_lifetime_resolver.rb
|
|
163
164
|
- lib/standard_id/passwordless/base_strategy.rb
|
|
164
165
|
- lib/standard_id/passwordless/email_strategy.rb
|
|
165
166
|
- lib/standard_id/passwordless/sms_strategy.rb
|