padlock_auth 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|