keycard 0.3.2 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|