padlock_auth 0.1.0
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +275 -0
- data/Rakefile +18 -0
- data/config/locales/padlock_auth.en.yml +14 -0
- data/lib/padlock_auth/abstract_access_token.rb +77 -0
- data/lib/padlock_auth/abstract_strategy.rb +52 -0
- data/lib/padlock_auth/config/option.rb +110 -0
- data/lib/padlock_auth/config/scopes.rb +124 -0
- data/lib/padlock_auth/config.rb +211 -0
- data/lib/padlock_auth/errors.rb +36 -0
- data/lib/padlock_auth/http/error_response.rb +75 -0
- data/lib/padlock_auth/http/forbidden_token_response.rb +67 -0
- data/lib/padlock_auth/http/invalid_token_response.rb +70 -0
- data/lib/padlock_auth/mixins/build_with.rb +49 -0
- data/lib/padlock_auth/mixins/hide_attribute.rb +31 -0
- data/lib/padlock_auth/rails/helpers.rb +142 -0
- data/lib/padlock_auth/rails/token_factory.rb +95 -0
- data/lib/padlock_auth/railtie.rb +22 -0
- data/lib/padlock_auth/rspec_support.rb +46 -0
- data/lib/padlock_auth/token/access_token.rb +55 -0
- data/lib/padlock_auth/token/strategy.rb +80 -0
- data/lib/padlock_auth/utils/abstract_builder.rb +55 -0
- data/lib/padlock_auth/version.rb +4 -0
- data/lib/padlock_auth.rb +76 -0
- data/lib/tasks/padlock_auth_tasks.rake +4 -0
- metadata +143 -0
@@ -0,0 +1,211 @@
|
|
1
|
+
require "padlock_auth/config/option"
|
2
|
+
require "padlock_auth/config/scopes"
|
3
|
+
|
4
|
+
module PadlockAuth
|
5
|
+
# Configuration for PadlockAuth.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# PadlockAuth.configure do |config|
|
9
|
+
# config.secure_with :token do
|
10
|
+
# secret_key "my_secret_key"
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# config.default_scopes :read, :write
|
14
|
+
# config.access_token_methods :from_bearer_authorization, :from_access_token_param
|
15
|
+
# config.raise_on_errors!
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
class Config
|
19
|
+
include PadlockAuth::Mixins::BuildWith
|
20
|
+
|
21
|
+
# The configuration builder for `PadlockAuth::Config`.
|
22
|
+
#
|
23
|
+
# @see PadlockAuth::Utils::AbstractBuilder
|
24
|
+
#
|
25
|
+
class Builder < PadlockAuth::Utils::AbstractBuilder
|
26
|
+
# Configure the strategy to use for authentication.
|
27
|
+
#
|
28
|
+
# Strategies are responsible for building access tokens and authenticating them.
|
29
|
+
# PadlockAuth comes with a default strategy, `PadlockAuth::Token::Strategy`,
|
30
|
+
# which uses a shared secret key to build and authenticate access tokens.
|
31
|
+
#
|
32
|
+
# A strategy can be provided as:
|
33
|
+
# - an instance of `PadlockAuth::AbstractStrategy`, or one matching its interface,
|
34
|
+
# - a class that responds to `.build` and returns an instance of `PadlockAuth::AbstractStrategy`, or
|
35
|
+
# - a string or symbol representing a built-in strategy (e.g. `:token`)
|
36
|
+
#
|
37
|
+
# The string or symbol strategy will be resolved to a class in the `PadlockAuth` namespace
|
38
|
+
# by appending `::Strategy` to the string and looking up the constant in the `PadlockAuth`
|
39
|
+
# namespace. For example, `:token` will resolve to `PadlockAuth::Token::Strategy`.
|
40
|
+
#
|
41
|
+
# You can define your own strategy by subclassing `PadlockAuth::AbstractStrategy`,
|
42
|
+
# and passing an instance of your strategy to `secure_with`, or by using the naming convention
|
43
|
+
# and passing a string or symbol.
|
44
|
+
#
|
45
|
+
# @param strategy [PadlockAuth::AbstractStrategy, Class, String, Symbol] The strategy to use for authentication
|
46
|
+
#
|
47
|
+
# @yield A block to configure the strategy (yielded by the strategy's `build` method)
|
48
|
+
#
|
49
|
+
# @return [PadlockAuth::AbstractStrategy] The strategy instance
|
50
|
+
def secure_with(strategy, &)
|
51
|
+
strategy = case strategy
|
52
|
+
when String, Symbol
|
53
|
+
strategy_klass = "PadlockAuth::#{strategy.to_s.camelize}::Strategy".safe_constantize
|
54
|
+
raise ArgumentError, "unknown strategy: #{strategy}" unless strategy_klass
|
55
|
+
strategy_klass.build(&)
|
56
|
+
when Class
|
57
|
+
strategy.build(&)
|
58
|
+
else
|
59
|
+
strategy
|
60
|
+
end
|
61
|
+
config.instance_variable_set(:@strategy, strategy)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Define default access token scopes for your endpoint.
|
65
|
+
#
|
66
|
+
# Scopes are used to limit access to certain parts of your API. When a token
|
67
|
+
# is created, it is assigned a set of scopes that define what it can access.
|
68
|
+
#
|
69
|
+
# Calls to `padlock_authorize!` will check that the token has the required scopes,
|
70
|
+
# if no scopes are provided, the default scopes will be used.
|
71
|
+
#
|
72
|
+
# @param scopes [Array] Default set of access (PadlockAuth::Config::Scopes.new)
|
73
|
+
# token scopes
|
74
|
+
#
|
75
|
+
def default_scopes(*scopes)
|
76
|
+
config.instance_variable_set(:@default_scopes, PadlockAuth::Config::Scopes.from_array(*scopes))
|
77
|
+
end
|
78
|
+
|
79
|
+
# Change the way access token is authenticated from the request object.
|
80
|
+
#
|
81
|
+
# By default it retrieves a Bearer token from the `HTTP_AUTHORIZATION` header, then
|
82
|
+
# falls back to the `:access_token` or `:bearer_token` params from the
|
83
|
+
# `params` object.
|
84
|
+
#
|
85
|
+
# Available methods:
|
86
|
+
# - `:from_bearer_authorization` - Extracts a Bearer token from the `HTTP_AUTHORIZATION` header
|
87
|
+
# - `:from_access_token_param` - Extracts the token from the `:access_token` param
|
88
|
+
# - `:from_bearer_param` - Extracts the token from the `:bearer_token` param
|
89
|
+
# - `:from_basic_authorization` - Extracts Basic Auth credentials from the `HTTP_AUTHORIZATION` header
|
90
|
+
#
|
91
|
+
# @param methods [Array<Symbol>] Define access token methods, in order of preference
|
92
|
+
#
|
93
|
+
def access_token_methods(*methods)
|
94
|
+
config.instance_variable_set(:@access_token_methods, methods.flatten.compact)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Calls to `padlock_authorize!` will raise an exception when authentication fails.
|
98
|
+
#
|
99
|
+
def raise_on_errors!
|
100
|
+
handle_auth_errors(:raise)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Calls to `padlock_authorize!` will render an error response when authentication fails.
|
104
|
+
#
|
105
|
+
# This is the default behavior.
|
106
|
+
#
|
107
|
+
def render_on_errors!
|
108
|
+
handle_auth_errors(:render)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# @!method build(&)
|
113
|
+
# @!scope class
|
114
|
+
#
|
115
|
+
# Builds the configuration instance using the builder.
|
116
|
+
#
|
117
|
+
# @yield block to configure the configuration
|
118
|
+
#
|
119
|
+
# @return [PadlockAuth::Config] the configuration instance
|
120
|
+
#
|
121
|
+
# @see PadlockAuth::Config::Builder
|
122
|
+
build_with Builder
|
123
|
+
|
124
|
+
extend PadlockAuth::Config::Option
|
125
|
+
|
126
|
+
# @!attribute [r] realm
|
127
|
+
#
|
128
|
+
# The strategy to use for authentication.
|
129
|
+
#
|
130
|
+
# @return [PadlockAuth::AbstractStrategy] The authentication strategy
|
131
|
+
#
|
132
|
+
attr_reader :strategy
|
133
|
+
|
134
|
+
# @!attribute [r] realm
|
135
|
+
#
|
136
|
+
# WWW-Authenticate Realm (default "PadlockAuth").
|
137
|
+
#
|
138
|
+
# @return [String] The Authentication realm
|
139
|
+
#
|
140
|
+
option :realm, default: "PadlockAuth"
|
141
|
+
|
142
|
+
# @!attribute [r] default_scopes
|
143
|
+
#
|
144
|
+
# Default required scopes, used whenever `padlock_authorize!` is called without arguments.
|
145
|
+
#
|
146
|
+
# Empty by default.
|
147
|
+
#
|
148
|
+
# @return [PadlockAuth::Config::Scopes] Default required scopes
|
149
|
+
#
|
150
|
+
def default_scopes
|
151
|
+
@default_scopes ||= PadlockAuth::Config::Scopes.new
|
152
|
+
end
|
153
|
+
|
154
|
+
# @!attribute [r] access_token_methods
|
155
|
+
#
|
156
|
+
# Methods to extract the access token from the request.
|
157
|
+
#
|
158
|
+
# @see PadlockAuth::Rails::TokenExtractor for available methods
|
159
|
+
#
|
160
|
+
# @return [Array<Symbol>] Methods to extract the access token
|
161
|
+
#
|
162
|
+
def access_token_methods
|
163
|
+
@access_token_methods ||= %i[
|
164
|
+
from_bearer_authorization
|
165
|
+
from_access_token_param
|
166
|
+
from_bearer_param
|
167
|
+
]
|
168
|
+
end
|
169
|
+
|
170
|
+
# @!attribute [r] handle_auth_errors
|
171
|
+
#
|
172
|
+
# How to handle authentication errors.
|
173
|
+
#
|
174
|
+
# - `:raise` - Raise an exception when authentication fails
|
175
|
+
# - `:render` - Render an error response when authentication fails
|
176
|
+
#
|
177
|
+
# @return [Symbol] The error handling method
|
178
|
+
#
|
179
|
+
option :handle_auth_errors, default: :render
|
180
|
+
|
181
|
+
# @return [Boolean] Whether to render an error response when authentication fails
|
182
|
+
#
|
183
|
+
def render_on_errors?
|
184
|
+
handle_auth_errors == :render
|
185
|
+
end
|
186
|
+
|
187
|
+
# @return [Boolean] Whether to raise an exception when authentication fails
|
188
|
+
#
|
189
|
+
def raise_on_errors?
|
190
|
+
handle_auth_errors == :raise
|
191
|
+
end
|
192
|
+
|
193
|
+
# @api private
|
194
|
+
#
|
195
|
+
# Called by PadlockAuth::Utils::AbstractBuilder#build to validate the configuration.
|
196
|
+
#
|
197
|
+
# @raise [ArgumentError] If the configuration is invalid
|
198
|
+
#
|
199
|
+
def validate!
|
200
|
+
raise ArgumentError, "strategy has not been configured via secure_with" if strategy.nil?
|
201
|
+
|
202
|
+
raise ArgumentError, "realm is required" if realm.blank?
|
203
|
+
|
204
|
+
unless handle_auth_errors.in? %i[raise render]
|
205
|
+
raise ArgumentError, "handle_auth_errors must be :raise, or :render"
|
206
|
+
end
|
207
|
+
|
208
|
+
true
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PadlockAuth
|
4
|
+
# Errors for PadlockAuth.
|
5
|
+
module Errors
|
6
|
+
# A generic error for PadlockAuth.
|
7
|
+
class PadlockAuthError < StandardError; end
|
8
|
+
|
9
|
+
# An error with a HTTP response.
|
10
|
+
class ResponseError < PadlockAuthError
|
11
|
+
attr_reader :response
|
12
|
+
|
13
|
+
# Initialize a new response error.
|
14
|
+
#
|
15
|
+
# @param response [PadlockAuth::Http::ErrorResponse] The response
|
16
|
+
def initialize(response)
|
17
|
+
@response = response
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# The token is invalid.
|
22
|
+
class InvalidToken < ResponseError; end
|
23
|
+
|
24
|
+
# The token has expired.
|
25
|
+
class TokenExpired < InvalidToken; end
|
26
|
+
|
27
|
+
# The token has been revoked.
|
28
|
+
class TokenRevoked < InvalidToken; end
|
29
|
+
|
30
|
+
# The token is invalid for an unknown reason.
|
31
|
+
class TokenUnknown < InvalidToken; end
|
32
|
+
|
33
|
+
# The token is forbidden for the requested resource.
|
34
|
+
class TokenForbidden < InvalidToken; end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PadlockAuth
|
4
|
+
module Http
|
5
|
+
##
|
6
|
+
# A generic error response for PadlockAuth.
|
7
|
+
#
|
8
|
+
# This class is intended to be extended by specific error responses.
|
9
|
+
#
|
10
|
+
class ErrorResponse
|
11
|
+
# @param attributes [Hash] error attributes
|
12
|
+
#
|
13
|
+
def initialize(attributes = {})
|
14
|
+
@name = attributes[:name]
|
15
|
+
@status = attributes[:status] || :bad_request
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Symbol] The error name
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# @return [Symbol] The HTTP status code
|
22
|
+
attr_reader :status
|
23
|
+
|
24
|
+
# @!attribute [r] description
|
25
|
+
# @return [String] A human-readable description of the error
|
26
|
+
def description
|
27
|
+
I18n.translate(
|
28
|
+
name,
|
29
|
+
scope: %i[padlock_auth errors messages],
|
30
|
+
default: :server_error
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Hash] JSON response body
|
35
|
+
#
|
36
|
+
def body
|
37
|
+
{
|
38
|
+
error: name,
|
39
|
+
error_description: description
|
40
|
+
}.reject { |_, v| v.blank? }
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Hash] HTTP headers
|
44
|
+
#
|
45
|
+
def headers
|
46
|
+
{
|
47
|
+
"Cache-Control" => "no-store, no-cache",
|
48
|
+
"Content-Type" => "application/json; charset=utf-8",
|
49
|
+
"WWW-Authenticate" => authenticate_info
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Raise an exception based on the error response.
|
54
|
+
#
|
55
|
+
# @raise [PadlockAuth::Errors::ResponseError] with the error response
|
56
|
+
def raise_exception!
|
57
|
+
raise exception_class.new(self), description
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
# @return [Class<ResponseError>] Exception class to raise
|
63
|
+
#
|
64
|
+
def exception_class
|
65
|
+
raise NotImplementedError, "error response must define #exception_class"
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def authenticate_info
|
71
|
+
%(Bearer realm="#{PadlockAuth.config.realm}", error="#{name}", error_description="#{description}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PadlockAuth
|
4
|
+
module Http
|
5
|
+
##
|
6
|
+
# A response for a forbidden token.
|
7
|
+
#
|
8
|
+
# A forbidden token response is returned when a token is valid,
|
9
|
+
# but does not have the required scopes.
|
10
|
+
#
|
11
|
+
class ForbiddenTokenResponse < ErrorResponse
|
12
|
+
attr_reader :reason
|
13
|
+
|
14
|
+
##
|
15
|
+
# Create a new forbidden token response from an access token.
|
16
|
+
#
|
17
|
+
# @param access_token [PadlockAuth::AbstractAccessToken] Access token
|
18
|
+
#
|
19
|
+
# @param scopes [Array<String>] Required scopes
|
20
|
+
#
|
21
|
+
# @param attributes [Hash] Additional attributes
|
22
|
+
#
|
23
|
+
def self.from_access_token(access_token, scopes, attributes = {})
|
24
|
+
new(attributes.merge(reason: access_token&.forbidden_token_reason, scopes: scopes))
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create a new forbidden token response.
|
28
|
+
#
|
29
|
+
# @param attributes [Hash] Attributes
|
30
|
+
#
|
31
|
+
def initialize(attributes = {})
|
32
|
+
super(attributes.merge(name: :invalid_scope, status: :forbidden))
|
33
|
+
@reason = attributes[:reason] || :unknown
|
34
|
+
@scopes = attributes[:scopes]
|
35
|
+
end
|
36
|
+
|
37
|
+
# @!attribute [r] description
|
38
|
+
# @return [String] A translated description of the error
|
39
|
+
#
|
40
|
+
def description
|
41
|
+
@description ||=
|
42
|
+
I18n.translate(
|
43
|
+
@reason,
|
44
|
+
scope: %i[padlock_auth errors messages forbidden_token],
|
45
|
+
oauth_scopes: @scopes.map(&:to_s).join(" "),
|
46
|
+
default: :unknown
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Hash] HTTP headers
|
51
|
+
#
|
52
|
+
def headers
|
53
|
+
headers = super
|
54
|
+
headers.delete "WWW-Authenticate" # Authentication was successful, so no need to display auth error info
|
55
|
+
headers
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# @return [Class<PadlockAuth::Errors::TokenForbidden>] Exception class
|
61
|
+
#
|
62
|
+
def exception_class
|
63
|
+
PadlockAuth::Errors::TokenForbidden
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PadlockAuth
|
4
|
+
module Http
|
5
|
+
##
|
6
|
+
# A response for an invalid token.
|
7
|
+
#
|
8
|
+
# An invalid token response is returned when a token is invalid.
|
9
|
+
#
|
10
|
+
class InvalidTokenResponse < ErrorResponse
|
11
|
+
# Reason for the invalid token.
|
12
|
+
#
|
13
|
+
# Possible reasons are:
|
14
|
+
# - :revoked
|
15
|
+
# - :expired
|
16
|
+
# - :unknown
|
17
|
+
#
|
18
|
+
# @return [Symbol] The reason for the invalid token
|
19
|
+
attr_reader :reason
|
20
|
+
|
21
|
+
# Create a new invalid token response from an access token.
|
22
|
+
#
|
23
|
+
# @param access_token [PadlockAuth::AbstractAccessToken] Access token
|
24
|
+
#
|
25
|
+
# @param attributes [Hash] Additional attributes
|
26
|
+
#
|
27
|
+
def self.from_access_token(access_token, attributes = {})
|
28
|
+
new(attributes.merge(reason: access_token&.invalid_token_reason))
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a new invalid token response.
|
32
|
+
#
|
33
|
+
# @param attributes [Hash] Attributes
|
34
|
+
#
|
35
|
+
def initialize(attributes = {})
|
36
|
+
super(attributes.merge(name: :invalid_grant, status: :unauthorized))
|
37
|
+
@reason = attributes[:reason] || :unknown
|
38
|
+
end
|
39
|
+
|
40
|
+
# @!attribute [r] description
|
41
|
+
# @return [String] A translated description of the error
|
42
|
+
#
|
43
|
+
def description
|
44
|
+
@description ||=
|
45
|
+
I18n.translate(
|
46
|
+
reason,
|
47
|
+
scope: %i[padlock_auth errors messages invalid_token],
|
48
|
+
default: :unknown
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
# Return the exception class for the error response.
|
55
|
+
#
|
56
|
+
# Depending on the reason, a different exception class will be raised.
|
57
|
+
#
|
58
|
+
# Defaults to `PadlockAuth::Errors::TokenUnknown`.
|
59
|
+
#
|
60
|
+
# @return [Class<InvalidToken>] Exception class
|
61
|
+
#
|
62
|
+
def exception_class
|
63
|
+
{
|
64
|
+
revoked: PadlockAuth::Errors::TokenRevoked,
|
65
|
+
expired: PadlockAuth::Errors::TokenExpired
|
66
|
+
}.fetch(reason, PadlockAuth::Errors::TokenUnknown)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module PadlockAuth
|
2
|
+
module Mixins
|
3
|
+
# Provides a quick way to build configuration instances.
|
4
|
+
#
|
5
|
+
# Provide `PadlockAuth::Utils::AbstractBuilder` sub-class to the `build_with` class method,
|
6
|
+
# to define a build method that will take a block to configure the instance.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class MyConfig
|
10
|
+
# include PadlockAuth::Mixins::BuildWith
|
11
|
+
#
|
12
|
+
# class Builder < PadlockAuth::Utils::AbstractBuilder
|
13
|
+
# def name(value)
|
14
|
+
# config.instance_variable_set(:@name, value)
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# build_with Builder
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# config = MyConfig.build do
|
22
|
+
# name 'My Name'
|
23
|
+
# end
|
24
|
+
# config.name # => 'My Name'
|
25
|
+
#
|
26
|
+
module BuildWith
|
27
|
+
extend ActiveSupport::Concern
|
28
|
+
|
29
|
+
included do
|
30
|
+
# Prevent direct instantiation of the class
|
31
|
+
private_class_method :new
|
32
|
+
end
|
33
|
+
|
34
|
+
class_methods do
|
35
|
+
# Define a builder class for the configuration.
|
36
|
+
#
|
37
|
+
# The builder class should accept a call to `new` with a new instance of the
|
38
|
+
# configuration class, and a block to configure the instance.
|
39
|
+
#
|
40
|
+
# @yield block to configure the builder class
|
41
|
+
#
|
42
|
+
def build_with(builder_class)
|
43
|
+
mattr_reader(:builder_class) { builder_class }
|
44
|
+
define_singleton_method(:build) { |&block| builder_class.new(new, &block).build }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module PadlockAuth
|
2
|
+
module Mixins
|
3
|
+
# Hide an attribute from inspection and JSON serialization.
|
4
|
+
#
|
5
|
+
# Prevents accidental exposure of sensitive data in logs or responses.
|
6
|
+
#
|
7
|
+
module HideAttribute
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
# Hide an attribute from inspection and JSON serialization.
|
12
|
+
#
|
13
|
+
# @param attribute [Symbol, String] The attribute to hide
|
14
|
+
#
|
15
|
+
def hide_attribute(attribute)
|
16
|
+
mod = Module.new
|
17
|
+
mod.define_method(:inspect) do
|
18
|
+
super().gsub(instance_variable_get(:"@#{attribute}"), "[REDACTED]")
|
19
|
+
end
|
20
|
+
mod.define_method(:as_json) do |options = nil|
|
21
|
+
options ||= {}
|
22
|
+
options[:except] ||= []
|
23
|
+
options[:except] |= [attribute.to_s]
|
24
|
+
super(options)
|
25
|
+
end
|
26
|
+
include mod
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module PadlockAuth
|
2
|
+
module Rails
|
3
|
+
##
|
4
|
+
# Helpers for Rails controllers.
|
5
|
+
#
|
6
|
+
# Provides `padlock_authorize!` method to controllers.
|
7
|
+
module Helpers
|
8
|
+
protected
|
9
|
+
|
10
|
+
# @!visibility public
|
11
|
+
#
|
12
|
+
# Authorize the request with the given scopes.
|
13
|
+
#
|
14
|
+
# If the request is not authorized, an error response will be rendered
|
15
|
+
# or an exception will be raised, depending on the configuration.
|
16
|
+
#
|
17
|
+
# @param scopes [Array<String>] Scopes required for the request, defaults to the default scopes.
|
18
|
+
#
|
19
|
+
def padlock_authorize!(*scopes)
|
20
|
+
@_padlock_auth_scopes = scopes.presence || PadlockAuth.config.default_scopes
|
21
|
+
|
22
|
+
padlock_render_error unless valid_padlock_auth_token?
|
23
|
+
end
|
24
|
+
|
25
|
+
# Default render options for unauthorized requests.
|
26
|
+
#
|
27
|
+
# As the OAuth 2.0 specification does not explicitly define the error response
|
28
|
+
# for an invalid request to a protected resource, this method replicates the
|
29
|
+
# error response for an invalid request access token request.
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# {
|
33
|
+
# error: "invalid_grant",
|
34
|
+
# error_description: "The access token is invalid."
|
35
|
+
# }
|
36
|
+
#
|
37
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-7.2
|
38
|
+
#
|
39
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
40
|
+
#
|
41
|
+
# @param error [PadlockAuth::Http::InvalidTokenResponse] Invalid grant response.
|
42
|
+
#
|
43
|
+
# @return [Hash] Render options, passed to `render`
|
44
|
+
#
|
45
|
+
def padlock_auth_unauthorized_render_options(error:, **)
|
46
|
+
{json: error.body, status: error.status}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Default render options for forbidden requests.
|
50
|
+
#
|
51
|
+
# As the OAuth 2.0 specification does not explicitly define the error response
|
52
|
+
# for an invalid request to a protected resource, this method replicates the
|
53
|
+
# error response for an invalid request access token request.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# {
|
57
|
+
# error: "invalid_scope",
|
58
|
+
# error_description: 'Access to this resource requires scope "foo bar baz".'
|
59
|
+
# }
|
60
|
+
#
|
61
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-7.2
|
62
|
+
#
|
63
|
+
# @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
64
|
+
#
|
65
|
+
# @param error [PadlockAuth::Http::ForbiddenTokenResponse] Invalid scope response
|
66
|
+
#
|
67
|
+
# @return [Hash] Render options, passed to `render`
|
68
|
+
#
|
69
|
+
def padlock_auth_forbidden_render_options(error:, **)
|
70
|
+
{json: error.body, status: error.status}
|
71
|
+
end
|
72
|
+
|
73
|
+
# @!visibility public
|
74
|
+
#
|
75
|
+
# Retrieve the access token from the request.
|
76
|
+
#
|
77
|
+
# Does not check if the token is valid or matches the required scopes.
|
78
|
+
#
|
79
|
+
# @return [PadlockAuth::AbstractToken, nil] Access token
|
80
|
+
#
|
81
|
+
def padlock_auth_token
|
82
|
+
@padlock_auth_token ||= TokenFactory.authenticate(request)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def valid_padlock_auth_token?
|
88
|
+
padlock_auth_token&.acceptable?(@_padlock_auth_scopes)
|
89
|
+
end
|
90
|
+
|
91
|
+
def padlock_render_error
|
92
|
+
error = padlock_auth_error
|
93
|
+
error.raise_exception! if PadlockAuth.config.raise_on_errors?
|
94
|
+
|
95
|
+
headers.merge!(error.headers.reject { |k| k == "Content-Type" })
|
96
|
+
padlock_render_error_with(error)
|
97
|
+
end
|
98
|
+
|
99
|
+
def padlock_render_error_with(error)
|
100
|
+
options = padlock_auth_render_options(error) || {}
|
101
|
+
status = padlock_auth_status_for_error(
|
102
|
+
error, options.delete(:respond_not_found_when_forbidden)
|
103
|
+
)
|
104
|
+
if options.blank? || !respond_to?(:render)
|
105
|
+
head status
|
106
|
+
else
|
107
|
+
options[:status] = status
|
108
|
+
options[:layout] = false if options[:layout].nil?
|
109
|
+
render options
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def padlock_auth_error
|
114
|
+
if padlock_auth_invalid_token_response?
|
115
|
+
PadlockAuth.build_invalid_token_response(padlock_auth_token)
|
116
|
+
else
|
117
|
+
PadlockAuth.build_forbidden_token_response(padlock_auth_token, @_padlock_auth_scopes)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def padlock_auth_render_options(error)
|
122
|
+
if padlock_auth_invalid_token_response?
|
123
|
+
padlock_auth_unauthorized_render_options(error: error)
|
124
|
+
else
|
125
|
+
padlock_auth_forbidden_render_options(error: error)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def padlock_auth_status_for_error(error, respond_not_found_when_forbidden)
|
130
|
+
if respond_not_found_when_forbidden && error.status == :forbidden
|
131
|
+
:not_found
|
132
|
+
else
|
133
|
+
error.status
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def padlock_auth_invalid_token_response?
|
138
|
+
!padlock_auth_token || !padlock_auth_token.accessible?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|