keycard 0.3.2 → 0.3.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/keycard.gemspec +1 -1
- data/lib/keycard/authentication/auth_token.rb +28 -0
- data/lib/keycard/authentication/method.rb +99 -0
- data/lib/keycard/authentication/result.rb +72 -0
- data/lib/keycard/authentication/session_user_id.rb +29 -0
- data/lib/keycard/authentication/user_eid.rb +28 -0
- data/lib/keycard/controller_methods.rb +111 -0
- data/lib/keycard/notary.rb +99 -0
- data/lib/keycard/reloadable_proxy.rb +40 -0
- data/lib/keycard/version.rb +1 -1
- data/lib/keycard.rb +15 -0
- metadata +14 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a73c4a3d99e36a4126bde8248bc3050037dc333a9a264b3169dff4a715d9a67
|
4
|
+
data.tar.gz: 320cec317aa2b0fdf3d85bf02506db09267954484898e1b351d4bd69de2d7253
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa1c61ee6513b56dd213229ee7df793076b095a54b52865f31b07d293a4bc235e72a563900f1fee5aff693de0136ad30b8be7b8345e47606d760ca5474d8ad5a
|
7
|
+
data.tar.gz: 86f727073283be6c122c3d4a0aaf7c5e738223306b08331779528087aecbe6bec1759b9fbaaa4819f033d1a3dc1fd0a4fd370d1107c4d5e068c9ec678fea9334
|
data/keycard.gemspec
CHANGED
@@ -33,6 +33,6 @@ Gem::Specification.new do |spec|
|
|
33
33
|
spec.add_development_dependency "rspec"
|
34
34
|
spec.add_development_dependency "rubocop"
|
35
35
|
spec.add_development_dependency "rubocop-rspec"
|
36
|
-
spec.add_development_dependency "sqlite3"
|
36
|
+
spec.add_development_dependency "sqlite3", "~> 1.3.13"
|
37
37
|
spec.add_development_dependency "yard"
|
38
38
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
module Authentication
|
5
|
+
# Identity verification based on an authorization token.
|
6
|
+
#
|
7
|
+
# The bound finder method is expected to take one parameter, the token as
|
8
|
+
# presented by the user. This will typically need to be digested for
|
9
|
+
# comparison with a stored version.
|
10
|
+
class AuthToken < Method
|
11
|
+
def apply
|
12
|
+
if token.nil?
|
13
|
+
skipped("No auth_token found in request attributes")
|
14
|
+
elsif (account = finder.call(token))
|
15
|
+
succeeded(account, "Account found for supplied Authorization Token", csrf_safe: true)
|
16
|
+
else
|
17
|
+
failed("Account not found for supplied Authorization Token")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def token
|
24
|
+
attributes.auth_token
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
module Authentication
|
5
|
+
# An abstract identity authentication method. Subclasses will inspect the
|
6
|
+
# attributes and session for a request, attempting to match an account, and
|
7
|
+
# recording the results on a {Result}.
|
8
|
+
#
|
9
|
+
# The general operation is that each authentication method will have its
|
10
|
+
# {#apply} method called. It should examine the attributes, session, or
|
11
|
+
# credentials, and decide whether the required information is present. Then:
|
12
|
+
#
|
13
|
+
# 1. If the method is not applicable, call {#skipped} with a message naming
|
14
|
+
# the authentication method and why it was not applicable.
|
15
|
+
# 2. If the method is applicable, call the finder to attempt to locate the
|
16
|
+
# user/account and verify the method-specific information. For example,
|
17
|
+
# some methods will trust a username attribute that arrived by way of
|
18
|
+
# a reverse proxy, and the finder will only need to verify that a user
|
19
|
+
# exists with the given username. Other methods will need to verify that
|
20
|
+
# a token or password supplied hashes to the correct value.
|
21
|
+
# 3. Depending on whether a user/account is identified and authenticated,
|
22
|
+
# call {#succeeded} with the account and a message, or {#failed} with
|
23
|
+
# a message.
|
24
|
+
#
|
25
|
+
# Each of the status methods appends to a result for diagnostic or audit
|
26
|
+
# purposes and affects whether the chain of authentication should continue or
|
27
|
+
# be terminated. If a authentication method is skipped, the next one will be
|
28
|
+
# attempted. If it succeeds, or fails, the chain will be terminated. If it
|
29
|
+
# succeeds, the identity attributes will be assigned to the account, and it
|
30
|
+
# will be set as the account on the result.
|
31
|
+
#
|
32
|
+
# For integration with larger-scale configuration (like how request
|
33
|
+
# attributes should be extracted and which authentication methods should be
|
34
|
+
# used, in what order), see {Keycard::Notary}.
|
35
|
+
#
|
36
|
+
# For stateful integration with controllers (like the notions of a "current
|
37
|
+
# user" and logging in and out), see {Keycard::ControllerMethods}.
|
38
|
+
class Method
|
39
|
+
def initialize(attributes:, session:, result:, finder:, **credentials)
|
40
|
+
@attributes = attributes
|
41
|
+
@session = session
|
42
|
+
@result = result
|
43
|
+
@finder = finder
|
44
|
+
@credentials = credentials
|
45
|
+
end
|
46
|
+
|
47
|
+
# Bind a finder callable and yield a factory lambda to create a
|
48
|
+
# Verification with all of the other parameters. This allows for
|
49
|
+
# configuring a prototype at the system level and applying items that vary
|
50
|
+
# per request more conveniently.
|
51
|
+
def self.bind(finder)
|
52
|
+
lambda do |attributes, session, result, **credentials|
|
53
|
+
new(
|
54
|
+
attributes: attributes,
|
55
|
+
session: session,
|
56
|
+
result: result,
|
57
|
+
finder: finder,
|
58
|
+
credentials: credentials
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Bind a class method as a finder. This is more convenient form than
|
64
|
+
# {::bind} because it uses a {Keycard::ReloadableProxy}, making it easier
|
65
|
+
# to work with finder methods on ActiveRecord models, which are reloaded in
|
66
|
+
# development on each change, without restarting the server.
|
67
|
+
def self.bind_class_method(finder_class, method)
|
68
|
+
bind(ReloadableProxy.new(finder_class, method))
|
69
|
+
end
|
70
|
+
|
71
|
+
# Attempt to apply this authentication method and record the status on the
|
72
|
+
# result.
|
73
|
+
def apply
|
74
|
+
skipped("Base Verification is always skipped; it should not be used directly.")
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def skipped(message)
|
80
|
+
result.skipped(message)
|
81
|
+
end
|
82
|
+
|
83
|
+
def succeeded(account, message, csrf_safe: false)
|
84
|
+
account.identity = attributes.identity
|
85
|
+
result.succeeded(account, message, csrf_safe: csrf_safe)
|
86
|
+
end
|
87
|
+
|
88
|
+
def failed(message)
|
89
|
+
result.failed(message)
|
90
|
+
end
|
91
|
+
|
92
|
+
attr_reader :attributes
|
93
|
+
attr_reader :session
|
94
|
+
attr_reader :result
|
95
|
+
attr_reader :finder
|
96
|
+
attr_reader :credentials
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
module Authentication
|
5
|
+
# A Result is the central point of information about an authentication
|
6
|
+
# attempt. It logs the authentication methods attempted with their statuses
|
7
|
+
# and reports the overall status. When authentication is successful, it holds
|
8
|
+
# the user/account that was verified.
|
9
|
+
class Result
|
10
|
+
attr_reader :account
|
11
|
+
attr_reader :log
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@account = nil
|
15
|
+
@log = []
|
16
|
+
@failed = false
|
17
|
+
@csrf_safe = false
|
18
|
+
end
|
19
|
+
|
20
|
+
# Has this authentication completed successfully?
|
21
|
+
def authenticated?
|
22
|
+
!account.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Was there a failure for an attempted authentication method?
|
26
|
+
def failed?
|
27
|
+
@failed
|
28
|
+
end
|
29
|
+
|
30
|
+
# Does a completed verification protect from Cross-Site Request Forgery?
|
31
|
+
#
|
32
|
+
# This should be true in cases where the client presents authentication
|
33
|
+
# that is not automatic, like an authentication token, rather than
|
34
|
+
# automatic credentials like cookies or proxy-applied headers.
|
35
|
+
def csrf_safe?
|
36
|
+
@csrf_safe
|
37
|
+
end
|
38
|
+
|
39
|
+
# Log that the authentication method was not applicable; continue the chain.
|
40
|
+
#
|
41
|
+
# @param message [String] a message about why the authentication method was skipped
|
42
|
+
# @return [Boolean] false, indicating that the authentication method was inconclusive
|
43
|
+
def skipped(message)
|
44
|
+
log << "[SKIPPED] #{message}"
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
# Log that the authentication method failed; terminate the chain.
|
49
|
+
#
|
50
|
+
# @param message [String] a message about how the authentication method failed
|
51
|
+
# @return [Boolean] true, indicating that further authentication should not occur
|
52
|
+
def failed(message)
|
53
|
+
log << "[FAILURE] #{message}"
|
54
|
+
@failed = true
|
55
|
+
end
|
56
|
+
|
57
|
+
# Log that the authentication method succeeded; terminate the chain.
|
58
|
+
#
|
59
|
+
# @param account [User|Account] Object/model representing the authenticated account
|
60
|
+
# @param message [String] a message about how the authentication method succeeded
|
61
|
+
# @param csrf_safe [Boolean] set to true if this authentication method precludes
|
62
|
+
# Cross-Site Request Forgery, as with a non-cookie token sent with the request
|
63
|
+
# @return [Boolean] true, indicating that further authentication should not occur
|
64
|
+
def succeeded(account, message, csrf_safe: false)
|
65
|
+
@account = account
|
66
|
+
@csrf_safe ||= csrf_safe
|
67
|
+
log << "[SUCCESS] #{message}"
|
68
|
+
true
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
module Authentication
|
5
|
+
# Identity verification based on a user_id present in the session.
|
6
|
+
#
|
7
|
+
# A user_id in the session would typically be placed there after some other
|
8
|
+
# login process, after which it is sufficient to authenticate the session.
|
9
|
+
# The finder, then, takes only one parameter, the ID as on the account's #id
|
10
|
+
# property.
|
11
|
+
class SessionUserId < Method
|
12
|
+
def apply
|
13
|
+
if user_id.nil?
|
14
|
+
skipped("No user_id found in session")
|
15
|
+
elsif (account = finder.call(user_id))
|
16
|
+
succeeded(account, "Account found for user_id '#{user_id}' in session")
|
17
|
+
else
|
18
|
+
failed("Account not found for user_id '#{user_id}' in session")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def user_id
|
25
|
+
session[:user_id]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
module Authentication
|
5
|
+
# Identity verification based on the user EID request attribute.
|
6
|
+
#
|
7
|
+
# The EID will typically be present in single sign-on scenarios, where
|
8
|
+
# there is a proxy in place to set secure headers. The finder is expected
|
9
|
+
# to take one paramter, the user_eid itself.
|
10
|
+
class UserEid < Method
|
11
|
+
def apply
|
12
|
+
if user_eid.nil?
|
13
|
+
skipped("No user_eid found in request attributes")
|
14
|
+
elsif (account = finder.call(user_eid))
|
15
|
+
succeeded(account, "Account found for user_eid '#{user_eid}'")
|
16
|
+
else
|
17
|
+
failed("Account not found for user_eid '#{user_eid}'")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def user_eid
|
24
|
+
attributes.user_eid
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
# Mixin for conveniences in controllers.
|
5
|
+
#
|
6
|
+
# These methods depend on a `notary` method in your controller that returns a
|
7
|
+
# configured {Keycard::Notary} instance.
|
8
|
+
module ControllerMethods
|
9
|
+
# The default session timeout is 24 hours, in seconds.
|
10
|
+
DEFAULT_SESSION_TIMEOUT = 60 * 60 * 24
|
11
|
+
|
12
|
+
# Check whether the current request is authenticated as coming from a known
|
13
|
+
# person or account.
|
14
|
+
#
|
15
|
+
# @return [Boolean] true if any of the {Notary}'s configured authentication
|
16
|
+
# methods succeeds
|
17
|
+
def logged_in?
|
18
|
+
authentication.authenticated?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Retrieve the user/account to which the current request is attributed.
|
22
|
+
#
|
23
|
+
# @return [User/Account] the user/account that has been authenticated; nil
|
24
|
+
# if no one is logged in
|
25
|
+
def current_user
|
26
|
+
authentication.account
|
27
|
+
end
|
28
|
+
|
29
|
+
# Validate the session, resetting it if expired.
|
30
|
+
#
|
31
|
+
# This should be called as a before_action before {#authenticate!} when
|
32
|
+
# working with session-based logins. It preserves a CSRF token, if present,
|
33
|
+
# so login forms and the like will pass forgery protection.
|
34
|
+
def validate_session
|
35
|
+
csrf_token = session[:_csrf_token]
|
36
|
+
elapsed = begin
|
37
|
+
Time.now - (session[:timestamp] || Time.at(0))
|
38
|
+
rescue StandardError
|
39
|
+
session_timeout
|
40
|
+
end
|
41
|
+
reset_session if elapsed >= session_timeout
|
42
|
+
session[:_csrf_token] = csrf_token
|
43
|
+
session[:timestamp] = Time.now if session.key?(:timestamp)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Require that some authentication method successfully identifies a user/account,
|
47
|
+
# raising an exception if there is a failure for active credentials or no
|
48
|
+
# applicable credentials are presented.
|
49
|
+
#
|
50
|
+
# @raise [AuthenticationFailed] if credentials for an attempted
|
51
|
+
# authentication method are incorrect
|
52
|
+
# @raise [AuthenticationRequired] if all authentication methods are skipped
|
53
|
+
# and authentication could not be attempted
|
54
|
+
# @return nil
|
55
|
+
def authenticate!
|
56
|
+
raise AuthenticationFailed if authentication.failed?
|
57
|
+
raise AuthenticationRequired unless authentication.authenticated?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Attempt to authenticate, optionally with user-supplied credentials, and
|
61
|
+
# establish a session.
|
62
|
+
#
|
63
|
+
# @param credentials [Hash|kwargs] user-supplied credentials that will be
|
64
|
+
# passed to each authentication method
|
65
|
+
# @return [Boolean] whether the login attempt was successful
|
66
|
+
def login(**credentials)
|
67
|
+
authentication(credentials).authenticated?.tap do |success|
|
68
|
+
setup_session if success
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Log an account in without checking any credentials, starting a session.
|
73
|
+
#
|
74
|
+
# @param account [User|Account] the user/account object to consider current;
|
75
|
+
# must have an #id property.
|
76
|
+
def auto_login(account)
|
77
|
+
request.env["keycard.authentication"] = notary.waive(account)
|
78
|
+
setup_session
|
79
|
+
end
|
80
|
+
|
81
|
+
# Clear authentication status and terminate any open session.
|
82
|
+
def logout
|
83
|
+
request.env["keycard.authentication"] = notary.reject
|
84
|
+
reset_session
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def authentication(**credentials)
|
90
|
+
request.env["keycard.authentication"] ||=
|
91
|
+
notary.authenticate(request, session, credentials)
|
92
|
+
end
|
93
|
+
|
94
|
+
# The session timeout, in seconds. Sessions will be cleared before any
|
95
|
+
# further authentication unless there is a timestamp younger than this many
|
96
|
+
# seconds old. The default is 24 hours.
|
97
|
+
#
|
98
|
+
# @return [Integer] session timeout, in seconds
|
99
|
+
def session_timeout
|
100
|
+
DEFAULT_SESSION_TIMEOUT
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_session
|
104
|
+
return_url = session[:return_to]
|
105
|
+
reset_session
|
106
|
+
session[:return_to] = return_url
|
107
|
+
session[:user_id] = current_user.id
|
108
|
+
session[:timestamp] = Time.now
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
# A Notary is the primary entry point for authentication needs. It will
|
5
|
+
# examine the request, session, and user-supplied credentials and provide
|
6
|
+
# a {Result} with the results of identity verification.
|
7
|
+
#
|
8
|
+
# It relies on configuration to extract the correct attributes from each
|
9
|
+
# request and to use the appropriate identity {AuthenticationMethod}. Each
|
10
|
+
# authentication method will attempt to locate a matching account and, for
|
11
|
+
# those methods that involve user-supplied credentials, verify that are
|
12
|
+
# correct for that account.
|
13
|
+
class Notary
|
14
|
+
# Create a Notary, which authenticates requests, verifying the identity of
|
15
|
+
# the requester and issuing an {Authentication::Result}.
|
16
|
+
#
|
17
|
+
# @param attributes_factory [Request::AttributesFactory] the factory to create
|
18
|
+
# {Request::Attributes} from the current request.
|
19
|
+
# @param methods [Array<callable>] the list of {AuthenticationMethod}s to
|
20
|
+
# use, in order, each wrapped as a callable initializer. Each factory
|
21
|
+
# should take (attributes, session, result, **credentials) and
|
22
|
+
# instantiate an AuthenticationMethod with a bound account/user finder.
|
23
|
+
def initialize(attributes_factory:, methods:)
|
24
|
+
@attributes_factory = attributes_factory
|
25
|
+
@methods = methods
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a default Notary instance, using common authenticaiton methods and
|
29
|
+
# the default AttributesFactory that creates request attributes based on
|
30
|
+
# the Keycard.config.access value.
|
31
|
+
#
|
32
|
+
# This instance assumes that there is a `User` model with class methods
|
33
|
+
# called `authenticate_by_auth_token`, `authenticate_by_id`, and
|
34
|
+
# `authenticate_by_user_eid`. These should find the user with the given
|
35
|
+
# id, authorization token, and EID/username. This is the order of
|
36
|
+
# precedence, as well, corresponding to the following {AuthenticationMethod}s:
|
37
|
+
#
|
38
|
+
# 1. {Keycard::Authentication::AuthToken}
|
39
|
+
# 2. {Keycard::Authentication::SessionUserId}
|
40
|
+
# 3. {Keycard::Authentication::UserEid}
|
41
|
+
#
|
42
|
+
# @return [Keycard::Notary] a default Notary instance, bound to conventional
|
43
|
+
# authentication methods on a User class.
|
44
|
+
def self.default
|
45
|
+
new(
|
46
|
+
attributes_factory: Keycard::Request::AttributesFactory.new,
|
47
|
+
methods: [
|
48
|
+
Keycard::Authentication::AuthToken.bind_class_method(:User, :authenticate_by_auth_token),
|
49
|
+
Keycard::Authentication::SessionUserId.bind_class_method(:User, :authenticate_by_id),
|
50
|
+
Keycard::Authentication::UserEid.bind_class_method(:User, :authenticate_by_user_eid)
|
51
|
+
]
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Authenticate a request, giving a Result of the result.
|
56
|
+
#
|
57
|
+
# @param request [Rack::Request] the active request, used to extract attributes
|
58
|
+
# @param session [Session] the active session, to be inspected with #[]
|
59
|
+
# @return [Authentication::Result] the result of this authentication
|
60
|
+
def authenticate(request, session, **credentials)
|
61
|
+
attributes = attributes_factory.for(request)
|
62
|
+
Authentication::Result.new.tap do |result|
|
63
|
+
methods.find do |factory|
|
64
|
+
factory.call(attributes, session, result, credentials).apply
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Bypass normal authentication and create a Result for the given
|
70
|
+
# user/account. This would typically only be used in development or other
|
71
|
+
# administrative scenarios where it is appropriate to allow impersonation.
|
72
|
+
def waive(account)
|
73
|
+
Authentication::Result.new.tap do |result|
|
74
|
+
result.succeeded(account, "Administrative waiver for #{account}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Issue an unconditional rejection Result. This is useful for a logout
|
79
|
+
# workflow, where authenticating again yield a passing result. The
|
80
|
+
# notion here is that the rejection would be cached just like any other
|
81
|
+
# result, rather than simply clearing it for the request.
|
82
|
+
#
|
83
|
+
# A logout would typically be followed by an immediate redirect, but this
|
84
|
+
# is a provision to ensure that the current request stays unauthenticated.
|
85
|
+
#
|
86
|
+
# @see {Keycard::ControllerMethods#logout} for how this is used in the
|
87
|
+
# context of the state of the current request.
|
88
|
+
def reject
|
89
|
+
Authentication::Result.new.tap do |result|
|
90
|
+
result.failed("Authentication rejected; session terminated")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
attr_reader :attributes_factory
|
97
|
+
attr_reader :methods
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Keycard
|
4
|
+
# A proxy for class methods, as for authentication methods on User/Account
|
5
|
+
# models. This is useful primarily during development mode, where binding
|
6
|
+
# a finder into a Verfication factory can break because of code reloading.
|
7
|
+
#
|
8
|
+
# For example, something like this would fail across requests where the User
|
9
|
+
# model is saved (in development):
|
10
|
+
# `AuthToken.bind(User.public_method(:authenticate_by_auth_token))`.
|
11
|
+
#
|
12
|
+
# Instead, you can bind to a class method with a proxy that will catch the
|
13
|
+
# error from Rails thrown when using a stale reference, replace the target
|
14
|
+
# method, and retry the call transparently.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# callable = ReloadableProxy.new(:User, :authenticate_by_auth_token)
|
18
|
+
class ReloadableProxy
|
19
|
+
attr_reader :classname, :methodname, :target
|
20
|
+
|
21
|
+
def initialize(classname, methodname)
|
22
|
+
@classname = classname
|
23
|
+
@methodname = methodname
|
24
|
+
lookup
|
25
|
+
end
|
26
|
+
|
27
|
+
# Call the proxied class method, looking it up again if the class has been
|
28
|
+
# reloaded (as signified byt he ArgumentError Rails raises).
|
29
|
+
def call(*args)
|
30
|
+
target.call(*args)
|
31
|
+
rescue ArgumentError
|
32
|
+
lookup
|
33
|
+
target.call(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def lookup
|
37
|
+
@target = Object.const_get(classname).method(methodname)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/keycard/version.rb
CHANGED
data/lib/keycard.rb
CHANGED
@@ -6,6 +6,9 @@ require "ostruct"
|
|
6
6
|
|
7
7
|
# All of the Keycard components are contained within this top-level module.
|
8
8
|
module Keycard
|
9
|
+
class AuthenticationRequired < StandardError; end
|
10
|
+
class AuthenticationFailed < StandardError; end
|
11
|
+
|
9
12
|
def self.config
|
10
13
|
@config ||= OpenStruct.new(
|
11
14
|
access: :direct
|
@@ -19,3 +22,15 @@ require "keycard/railtie" if defined?(Rails)
|
|
19
22
|
require "keycard/institution_finder"
|
20
23
|
require "keycard/request"
|
21
24
|
require "keycard/token"
|
25
|
+
|
26
|
+
require "keycard/notary"
|
27
|
+
|
28
|
+
require "keycard/authentication/method"
|
29
|
+
require "keycard/authentication/result"
|
30
|
+
|
31
|
+
require "keycard/authentication/auth_token"
|
32
|
+
require "keycard/authentication/session_user_id"
|
33
|
+
require "keycard/authentication/user_eid"
|
34
|
+
|
35
|
+
require "keycard/reloadable_proxy"
|
36
|
+
require "keycard/controller_methods"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keycard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Botimer
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2019-07-
|
12
|
+
date: 2019-07-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sequel
|
@@ -127,16 +127,16 @@ dependencies:
|
|
127
127
|
name: sqlite3
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
129
129
|
requirements:
|
130
|
-
- - "
|
130
|
+
- - "~>"
|
131
131
|
- !ruby/object:Gem::Version
|
132
|
-
version:
|
132
|
+
version: 1.3.13
|
133
133
|
type: :development
|
134
134
|
prerelease: false
|
135
135
|
version_requirements: !ruby/object:Gem::Requirement
|
136
136
|
requirements:
|
137
|
-
- - "
|
137
|
+
- - "~>"
|
138
138
|
- !ruby/object:Gem::Version
|
139
|
-
version:
|
139
|
+
version: 1.3.13
|
140
140
|
- !ruby/object:Gem::Dependency
|
141
141
|
name: yard
|
142
142
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,10 +182,18 @@ files:
|
|
182
182
|
- docs/runtime_context.rst
|
183
183
|
- keycard.gemspec
|
184
184
|
- lib/keycard.rb
|
185
|
+
- lib/keycard/authentication/auth_token.rb
|
186
|
+
- lib/keycard/authentication/method.rb
|
187
|
+
- lib/keycard/authentication/result.rb
|
188
|
+
- lib/keycard/authentication/session_user_id.rb
|
189
|
+
- lib/keycard/authentication/user_eid.rb
|
190
|
+
- lib/keycard/controller_methods.rb
|
185
191
|
- lib/keycard/db.rb
|
186
192
|
- lib/keycard/digest_key.rb
|
187
193
|
- lib/keycard/institution_finder.rb
|
194
|
+
- lib/keycard/notary.rb
|
188
195
|
- lib/keycard/railtie.rb
|
196
|
+
- lib/keycard/reloadable_proxy.rb
|
189
197
|
- lib/keycard/request.rb
|
190
198
|
- lib/keycard/request/attributes.rb
|
191
199
|
- lib/keycard/request/attributes_factory.rb
|