doorkeeper 5.9.5 → 6.0.0.beta2

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +70 -3
  3. data/README.md +204 -0
  4. data/app/controllers/doorkeeper/applications_controller.rb +38 -9
  5. data/app/controllers/doorkeeper/authorizations_controller.rb +17 -3
  6. data/app/controllers/doorkeeper/metadata_controller.rb +20 -0
  7. data/app/controllers/doorkeeper/tokens_controller.rb +15 -2
  8. data/app/views/doorkeeper/authorizations/new.html.erb +24 -0
  9. data/config/locales/en.yml +2 -0
  10. data/lib/doorkeeper/client_authentication/credentials.rb +11 -0
  11. data/lib/doorkeeper/client_authentication/fallback_method.rb +19 -0
  12. data/lib/doorkeeper/client_authentication/legacy_callable.rb +52 -0
  13. data/lib/doorkeeper/client_authentication/method.rb +40 -0
  14. data/lib/doorkeeper/client_authentication/registry.rb +47 -0
  15. data/lib/doorkeeper/client_authentication/verified_credentials.rb +19 -0
  16. data/lib/doorkeeper/client_authentication.rb +102 -0
  17. data/lib/doorkeeper/config/option.rb +1 -1
  18. data/lib/doorkeeper/config/validations.rb +162 -1
  19. data/lib/doorkeeper/config.rb +161 -9
  20. data/lib/doorkeeper/document_cache.rb +81 -0
  21. data/lib/doorkeeper/errors.rb +49 -0
  22. data/lib/doorkeeper/http_fetcher.rb +232 -0
  23. data/lib/doorkeeper/models/access_grant_mixin.rb +16 -0
  24. data/lib/doorkeeper/models/access_token_mixin.rb +81 -5
  25. data/lib/doorkeeper/models/application_mixin.rb +14 -6
  26. data/lib/doorkeeper/models/concerns/secret_storable.rb +10 -1
  27. data/lib/doorkeeper/oauth/authorization/code.rb +10 -0
  28. data/lib/doorkeeper/oauth/authorization/token.rb +13 -2
  29. data/lib/doorkeeper/oauth/authorization/uri_builder.rb +11 -0
  30. data/lib/doorkeeper/oauth/authorization_code_request.rb +127 -3
  31. data/lib/doorkeeper/oauth/base_request.rb +1 -2
  32. data/lib/doorkeeper/oauth/client.rb +24 -0
  33. data/lib/doorkeeper/oauth/client_authentication/client_secret_basic.rb +57 -0
  34. data/lib/doorkeeper/oauth/client_authentication/client_secret_post.rb +33 -0
  35. data/lib/doorkeeper/oauth/client_authentication/none.rb +71 -0
  36. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/key_resolver.rb +104 -0
  37. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt/replay_guard.rb +78 -0
  38. data/lib/doorkeeper/oauth/client_authentication/private_key_jwt.rb +247 -0
  39. data/lib/doorkeeper/oauth/client_credentials/creator.rb +25 -9
  40. data/lib/doorkeeper/oauth/client_credentials_request.rb +36 -5
  41. data/lib/doorkeeper/oauth/code_response.rb +16 -2
  42. data/lib/doorkeeper/oauth/error_response.rb +11 -2
  43. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +35 -23
  44. data/lib/doorkeeper/oauth/metadata_response.rb +165 -0
  45. data/lib/doorkeeper/oauth/password_access_token_request.rb +27 -1
  46. data/lib/doorkeeper/oauth/pre_authorization.rb +63 -8
  47. data/lib/doorkeeper/oauth/refresh_token_request.rb +51 -1
  48. data/lib/doorkeeper/oauth/resource_indicator_validator.rb +70 -0
  49. data/lib/doorkeeper/oauth/scopes.rb +25 -0
  50. data/lib/doorkeeper/oauth/token_introspection.rb +33 -7
  51. data/lib/doorkeeper/orm/active_record/mixins/application.rb +2 -2
  52. data/lib/doorkeeper/rails/routes/mapping.rb +1 -0
  53. data/lib/doorkeeper/rails/routes.rb +6 -0
  54. data/lib/doorkeeper/request.rb +59 -0
  55. data/lib/doorkeeper/server.rb +5 -2
  56. data/lib/doorkeeper/version.rb +4 -4
  57. data/lib/doorkeeper.rb +12 -4
  58. data/lib/generators/doorkeeper/grant_reuse_revocation_generator.rb +43 -0
  59. data/lib/generators/doorkeeper/resource_indicators_generator.rb +33 -0
  60. data/lib/generators/doorkeeper/templates/add_access_token_to_access_grants.rb.erb +10 -0
  61. data/lib/generators/doorkeeper/templates/enable_resource_indicators_migration.rb.erb +8 -0
  62. data/lib/generators/doorkeeper/templates/initializer.rb +131 -6
  63. data/lib/generators/doorkeeper/templates/migration.rb.erb +13 -0
  64. metadata +52 -4
  65. data/lib/doorkeeper/oauth/client/credentials.rb +0 -71
  66. data/lib/doorkeeper/oauth/stateless_token.rb +0 -139
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module ClientAuthentication
5
+ # Adapter that lets a legacy callable +client_credentials+ extractor
6
+ # (e.g. +->(request) { [client_id, client_secret] }+) keep working through
7
+ # the client authentication registry during the deprecation window.
8
+ #
9
+ # The callable is expected to return a +[uid, secret]+ pair (or +nil+ when
10
+ # it does not apply), matching the historical +Credentials.from_request+
11
+ # contract. It is considered to match a request whenever it yields present
12
+ # credentials, mirroring the previous "first extractor that returns a uid
13
+ # wins" behaviour.
14
+ class LegacyCallable
15
+ def initialize(callable)
16
+ @callable = callable
17
+ end
18
+
19
+ # Legacy extractors pull a uid/secret pair out of the request, so they
20
+ # are treated as shared-secret methods.
21
+ def uses_shared_secret?
22
+ true
23
+ end
24
+
25
+ def matches_request?(request)
26
+ credentials_for(request).present?
27
+ end
28
+
29
+ def authenticate(request)
30
+ credentials_for(request)
31
+ end
32
+
33
+ private
34
+
35
+ # Invoke the wrapped extractor at most once per request. +matches_request?+
36
+ # and +authenticate+ both need the extracted credentials, and the legacy
37
+ # +Credentials.from_request+ contract called each extractor exactly once,
38
+ # so cache the result on the request itself to preserve that. The adapter
39
+ # is stored in the config/registry and therefore shared across requests
40
+ # and threads, so the cache must live on the per-request +env+ (keyed per
41
+ # adapter instance) rather than on the adapter, which would otherwise race.
42
+ def credentials_for(request)
43
+ request.env[cache_key] ||=
44
+ Doorkeeper::ClientAuthentication::Credentials.new(*@callable.call(request))
45
+ end
46
+
47
+ def cache_key
48
+ @cache_key ||= "doorkeeper.client_authentication.legacy_callable.#{@callable.object_id}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module ClientAuthentication
5
+ # Wraps a registered client authentication method, pairing its
6
+ # registration +name+ with the +strategy+ object that knows how to match
7
+ # and authenticate a request.
8
+ #
9
+ # NOTE: the wrapped object is exposed as +strategy+ rather than +method+ on
10
+ # purpose — an +attr_reader :method+ would shadow Ruby's core
11
+ # +Object#method+ reflection API and break +wrapper.method(:authenticate)+.
12
+ class Method
13
+ attr_reader :name, :strategy
14
+
15
+ delegate :matches_request?, :authenticate, to: :strategy
16
+
17
+ def initialize(name, strategy)
18
+ @name = name
19
+ @strategy = strategy
20
+ end
21
+
22
+ # Whether this method authenticates clients with a shared symmetric
23
+ # secret. Callers that must refuse such methods — because no secret
24
+ # can have been established with the client — can ask the registry
25
+ # instead of keeping a hard-coded list of method names.
26
+ #
27
+ # Strategies may declare this themselves by defining
28
+ # +uses_shared_secret?+; for strategies that don't, the registration
29
+ # name is checked for "client_secret" as a conservative fallback, so an
30
+ # undeclared method errs on the side of being treated as secret-based.
31
+ def uses_shared_secret?
32
+ if strategy.respond_to?(:uses_shared_secret?)
33
+ strategy.uses_shared_secret?
34
+ else
35
+ name.to_s.include?("client_secret")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module ClientAuthentication
5
+ # Holds the registered client authentication methods and provides the DSL
6
+ # to register and look them up by name.
7
+ module Registry
8
+ mattr_accessor :registered_methods
9
+ self.registered_methods = {}
10
+
11
+ # Allows to register a custom OAuth client authentication method so that
12
+ # Doorkeeper could recognize and process it.
13
+ #
14
+ def register(name, method)
15
+ unless name.respond_to?(:to_sym)
16
+ raise ArgumentError,
17
+ "client authentication method name must be a Symbol or String, got #{name.inspect}"
18
+ end
19
+
20
+ unless method.respond_to?(:matches_request?) && method.respond_to?(:authenticate)
21
+ raise ArgumentError,
22
+ "client authentication method '#{name}' must respond to " \
23
+ "#matches_request? and #authenticate, got #{method.inspect}"
24
+ end
25
+
26
+ name_key = name.to_sym
27
+
28
+ if registered_methods.key?(name_key)
29
+ ::Kernel.warn <<~WARNING
30
+ [DOORKEEPER] '#{name_key}' client authentication strategy is already registered and will be overridden
31
+ in #{caller(1..1).first}
32
+ WARNING
33
+ end
34
+
35
+ registered_methods[name_key] = Doorkeeper::ClientAuthentication::Method.new(name_key, method)
36
+ end
37
+
38
+ # [NOTE]: switch to #fetch once the deprecated client_credentials
39
+ # fallbacks are removed.
40
+ def get(name)
41
+ return unless name.respond_to?(:to_sym)
42
+
43
+ registered_methods[name.to_sym]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doorkeeper
4
+ module ClientAuthentication
5
+ # Credentials for a client that was already fully authenticated by its
6
+ # authentication method (e.g. a verified private_key_jwt assertion), so
7
+ # the client lookup must not run a secret comparison — there is no
8
+ # secret, and the proof of identity has already been checked.
9
+ class VerifiedCredentials < Credentials
10
+ def initialize(uid)
11
+ super(uid, nil)
12
+ end
13
+
14
+ def pre_authenticated?
15
+ true
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "doorkeeper/client_authentication/credentials"
4
+ require "doorkeeper/client_authentication/verified_credentials"
5
+ require "doorkeeper/client_authentication/fallback_method"
6
+ require "doorkeeper/client_authentication/legacy_callable"
7
+ require "doorkeeper/client_authentication/method"
8
+ require "doorkeeper/client_authentication/registry"
9
+
10
+ module Doorkeeper
11
+ # Registry of the OAuth client authentication methods (RFC 6749 §2.3)
12
+ # Doorkeeper knows how to process. Each registered method is able to tell
13
+ # whether it +matches_request?+ and how to +authenticate+ it into a
14
+ # Credentials object.
15
+ module ClientAuthentication
16
+ extend Registry
17
+
18
+ # Default ordered client authentication methods (RFC 6749 §2.3) used when
19
+ # +client_authentication+ is not configured.
20
+ DEFAULT_METHODS = %i[client_secret_basic client_secret_post none].freeze
21
+
22
+ # Maps the current method names back onto the deprecated +client_credentials+
23
+ # names so the +client_credentials_methods+ alias can keep returning the
24
+ # legacy symbols that external callers (e.g. doorkeeper-openid_connect) map
25
+ # from. Names without a legacy equivalent (e.g. +:none+, custom methods) are
26
+ # returned unchanged — consumers that don't recognise them drop them, just
27
+ # as they did before.
28
+ LEGACY_NAME_FOR = {
29
+ client_secret_basic: :from_basic,
30
+ client_secret_post: :from_params,
31
+ }.freeze
32
+
33
+ # Converts resolved Method objects into the legacy client_credentials
34
+ # symbol names for the deprecated +client_credentials_methods+ alias.
35
+ def self.to_legacy_client_credentials_names(methods)
36
+ methods.map { |method| LEGACY_NAME_FOR.fetch(method.name, method.name) }
37
+ end
38
+
39
+ register(
40
+ :none,
41
+ Doorkeeper::OAuth::ClientAuthentication::None,
42
+ )
43
+
44
+ register(
45
+ :client_secret_post,
46
+ Doorkeeper::OAuth::ClientAuthentication::ClientSecretPost,
47
+ )
48
+
49
+ register(
50
+ :client_secret_basic,
51
+ Doorkeeper::OAuth::ClientAuthentication::ClientSecretBasic,
52
+ )
53
+
54
+ # Registered but not part of DEFAULT_METHODS: servers opt in through the
55
+ # +client_authentication+ config option. Requires the "jwt" gem at the
56
+ # moment an assertion is authenticated.
57
+ register(
58
+ :private_key_jwt,
59
+ Doorkeeper::OAuth::ClientAuthentication::PrivateKeyJwt,
60
+ )
61
+
62
+ # Converts a deprecated +client_credentials+ configuration into the client
63
+ # authentication method names / adapters understood by the registry.
64
+ # Unknown values are warned about and dropped; callables are wrapped in a
65
+ # LegacyCallable adapter. +:none+ (public client support) is appended only
66
+ # for +:from_params+ — the sole legacy method that accepted a bare
67
+ # +client_id+ without a secret — so a Basic-only configuration is not
68
+ # silently broadened.
69
+ def self.from_legacy_client_credentials(methods)
70
+ converted = methods.filter_map { |method| legacy_client_credential(method) }
71
+ converted.push(:none) if methods.include?(:from_params)
72
+ converted
73
+ end
74
+
75
+ def self.legacy_client_credential(method)
76
+ case method
77
+ when :from_basic
78
+ :client_secret_basic
79
+ when :from_params
80
+ :client_secret_post
81
+ else
82
+ legacy_extractor_or_nil(method)
83
+ end
84
+ end
85
+ private_class_method :legacy_client_credential
86
+
87
+ def self.legacy_extractor_or_nil(method)
88
+ unless method.respond_to?(:call)
89
+ Kernel.warn("[DOORKEEPER] Unknown client_credentials method detected: #{method}")
90
+ return nil
91
+ end
92
+
93
+ Kernel.warn(
94
+ "[DOORKEEPER] client_credentials callable extractors are deprecated; wrapping it in a " \
95
+ "legacy client authentication adapter. Register it via " \
96
+ "Doorkeeper::ClientAuthentication.register instead.",
97
+ )
98
+ Method.new(:legacy_callable, LegacyCallable.new(method))
99
+ end
100
+ private_class_method :legacy_extractor_or_nil
101
+ end
102
+ end
@@ -74,7 +74,7 @@ module Doorkeeper
74
74
  def self.extended(base)
75
75
  return if base.respond_to?(:builder_class)
76
76
 
77
- raise Doorkeeper::MissingConfigurationBuilderClass, "Define `self.builder_class` method " \
77
+ raise Doorkeeper::Errors::MissingConfigurationBuilderClass, "Define `self.builder_class` method " \
78
78
  "for #{base} that returns your custom Builder class to use options DSL!"
79
79
  end
80
80
  end
@@ -8,14 +8,68 @@ module Doorkeeper
8
8
  # Validates configuration options to be set properly.
9
9
  #
10
10
  def validate!
11
+ validate_client_authentication_conflict
12
+ validate_client_authentication_registered
11
13
  validate_reuse_access_token_value
12
14
  validate_token_reuse_limit
13
15
  validate_secret_strategies
14
16
  validate_pkce_code_challenge_methods
17
+ validate_custom_metadata
18
+ validate_refresh_token_flow
19
+ validate_issuer_format
20
+ validate_issuer_metadata_discoverability
15
21
  end
16
22
 
17
23
  private
18
24
 
25
+ # Warn once, at configuration time, when both the deprecated
26
+ # +client_credentials+ and the new +client_authentication+ options are
27
+ # set. +client_authentication+ takes precedence; the warning lives here
28
+ # (rather than in the memoised resolver) so it is not swallowed and
29
+ # surfaces during boot instead of on the first request.
30
+ def validate_client_authentication_conflict
31
+ return unless instance_variable_defined?(:@client_credentials_methods) &&
32
+ instance_variable_defined?(:@client_authentication)
33
+
34
+ ::Rails.logger.warn(
35
+ "[DOORKEEPER] Both client_credentials and client_authentication are set, " \
36
+ "using client_authentication",
37
+ )
38
+ end
39
+
40
+ # Warn about configured client authentication methods that are not
41
+ # registered (e.g. a typo, or an extension that failed to load). Such
42
+ # names are silently ignored when resolving the methods, which could
43
+ # otherwise leave the application with no usable authentication methods.
44
+ def validate_client_authentication_registered
45
+ # The deprecated client_credentials path already validates its own input.
46
+ return if instance_variable_defined?(:@client_credentials_methods) &&
47
+ !instance_variable_defined?(:@client_authentication)
48
+
49
+ configured = client_authentication
50
+ unknown = configured.reject { |name| Doorkeeper::ClientAuthentication.get(name) }
51
+
52
+ unless unknown.empty?
53
+ ::Rails.logger.warn(
54
+ "[DOORKEEPER] Unknown client authentication method(s) configured and will be ignored: " \
55
+ "#{unknown.map(&:inspect).join(", ")}. " \
56
+ "Ensure each method is registered (e.g. by the extension that provides it).",
57
+ )
58
+ end
59
+
60
+ # A configuration that resolves to zero usable methods fails every
61
+ # client authentication attempt (all requests fall back to no
62
+ # credentials), so make that loud rather than silent.
63
+ return unless (configured - unknown).empty?
64
+
65
+ ::Rails.logger.error(
66
+ "[DOORKEEPER] No usable client authentication methods are configured " \
67
+ "(client_authentication resolved to an empty set). All client authentication " \
68
+ "will fail. Configure at least one registered method, e.g. " \
69
+ "client_authentication #{Doorkeeper::ClientAuthentication::DEFAULT_METHODS.inspect}.",
70
+ )
71
+ end
72
+
19
73
  # Determine whether +reuse_access_token+ and a non-restorable
20
74
  # +token_secret_strategy+ have both been activated.
21
75
  #
@@ -51,7 +105,15 @@ module Doorkeeper
51
105
  end
52
106
 
53
107
  def validate_pkce_code_challenge_methods
54
- return if pkce_code_challenge_methods.all? { |method| method =~ /^plain$|^S256$/ }
108
+ methods = pkce_code_challenge_methods.map(&:to_s)
109
+ if methods.all? { |method| method.match?(/\A(?:plain|S256)\z/) }
110
+ # Persist the normalized (string) values only when the option was
111
+ # explicitly configured — the default is already normalized, and an
112
+ # unconfigured option should stay undefined rather than have
113
+ # validation flip its instance_variable_defined? signal.
114
+ @pkce_code_challenge_methods = methods if instance_variable_defined?(:@pkce_code_challenge_methods)
115
+ return
116
+ end
55
117
 
56
118
  ::Rails.logger.warn(
57
119
  "[DOORKEEPER] You have configured an invalid value for pkce_code_challenge_methods option. " \
@@ -60,6 +122,105 @@ module Doorkeeper
60
122
 
61
123
  @pkce_code_challenge_methods = ["plain", "S256"]
62
124
  end
125
+
126
+ def validate_custom_metadata
127
+ return if custom_metadata.is_a? Hash
128
+
129
+ ::Rails.logger.warn(
130
+ "[DOORKEEPER] You have configured an invalid value for custom_metadata option. " \
131
+ "It must be a Hash, and will be overridden with an empty hash.",
132
+ )
133
+
134
+ @custom_metadata = {}
135
+ end
136
+
137
+ # Warn when the refresh_token grant flow is enabled but refresh tokens
138
+ # are never issued: refresh token requests would always fail because
139
+ # there are no tokens to refresh. The flow is enabled automatically
140
+ # when +use_refresh_token+ is configured, so it doesn't need to be
141
+ # listed in +grant_flows+ explicitly.
142
+ #
143
+ # +calculate_grant_flows+ is used (rather than the raw +grant_flows+) so
144
+ # that the flow is also detected when enabled through a registered
145
+ # grant-flow alias. When +use_refresh_token+ is not configured the
146
+ # refresh_token flow is not appended implicitly, so its presence there
147
+ # means it was requested explicitly (directly or via an alias).
148
+ def validate_refresh_token_flow
149
+ return if refresh_token_enabled?
150
+ return unless calculate_grant_flows.map(&:to_s).include?("refresh_token")
151
+
152
+ ::Rails.logger.warn(
153
+ "[DOORKEEPER] You have enabled the refresh_token grant flow without " \
154
+ "configuring use_refresh_token, so refresh tokens will not be issued. " \
155
+ "Configure use_refresh_token to issue refresh tokens (the refresh_token " \
156
+ "grant flow is then enabled automatically).",
157
+ )
158
+ end
159
+
160
+ # Warn when a configured issuer is not RFC-compliant. RFC 8414 (the
161
+ # metadata issuer) and RFC 9207 (the authorization response iss parameter)
162
+ # both require an https URL with a host and no query or fragment component.
163
+ # The value is still used as-is - this is a warning, not a hard failure, so
164
+ # local setups using e.g. http://localhost keep working - but a
165
+ # non-compliant issuer produces responses that strict clients may reject.
166
+ def validate_issuer_format
167
+ return if issuer.blank?
168
+
169
+ uri =
170
+ begin
171
+ URI.parse(issuer.to_s)
172
+ rescue URI::InvalidURIError
173
+ nil
174
+ end
175
+
176
+ return if uri.is_a?(URI::HTTPS) && uri.host.present? &&
177
+ uri.query.nil? && uri.fragment.nil?
178
+
179
+ ::Rails.logger.warn(
180
+ "[DOORKEEPER] issuer #{redacted_issuer.inspect} is not RFC-compliant: " \
181
+ "RFC 8414 and RFC 9207 require an https URL with a host and no query " \
182
+ "or fragment component. It is still advertised in the metadata and " \
183
+ "emitted as the iss parameter as-is, but strict clients may reject it.",
184
+ )
185
+ end
186
+
187
+ # Warn when a path-bearing issuer cannot be discovered. RFC 8414 clients
188
+ # build the metadata URL by inserting the well-known path into the issuer,
189
+ # so an issuer of https://host/tenant is looked up at
190
+ # https://host/.well-known/oauth-authorization-server/tenant. Doorkeeper
191
+ # only serves the document at the root well-known path, so such a value is
192
+ # not discoverable (and its advertised issuer would not match the lookup).
193
+ # This is a separate concern from validate_issuer_format: a path component
194
+ # is valid per RFC 8414, just unsupported by Doorkeeper's fixed route.
195
+ def validate_issuer_metadata_discoverability
196
+ return if issuer.blank?
197
+
198
+ uri =
199
+ begin
200
+ URI.parse(issuer.to_s)
201
+ rescue URI::InvalidURIError
202
+ nil
203
+ end
204
+
205
+ return if uri&.host.blank?
206
+ return if uri.path.blank? || uri.path == "/"
207
+
208
+ ::Rails.logger.warn(
209
+ "[DOORKEEPER] issuer #{redacted_issuer.inspect} has a path component, but " \
210
+ "Doorkeeper serves its RFC 8414 metadata only at the root " \
211
+ "/.well-known/oauth-authorization-server. RFC 8414 clients derive the " \
212
+ "metadata URL from the issuer path (…/.well-known/" \
213
+ "oauth-authorization-server#{uri.path}), so they will not discover the " \
214
+ "document. Use a host-only issuer, or route the derived well-known path " \
215
+ "to Doorkeeper.",
216
+ )
217
+ end
218
+
219
+ # Redact any userinfo (e.g. a misconfigured user:pass@host) before the
220
+ # issuer is written to the log, so credentials are not leaked there.
221
+ def redacted_issuer
222
+ issuer.to_s.sub(%r{//[^/@]*@}, "//***@")
223
+ end
63
224
  end
64
225
  end
65
226
  end