oauth2 2.0.9 → 2.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +3 -0
- data/CHANGELOG.md +833 -182
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +262 -34
- data/FUNDING.md +74 -0
- data/LICENSE.md +110 -0
- data/README.md +923 -351
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +11 -16
- data/certs/pboling.pem +27 -0
- data/lib/oauth2/access_token.rb +233 -38
- data/lib/oauth2/auth_sanitizer.rb +36 -0
- data/lib/oauth2/authenticator.rb +43 -9
- data/lib/oauth2/client.rb +353 -97
- data/lib/oauth2/error.rb +37 -17
- data/lib/oauth2/filtered_attributes.rb +10 -0
- data/lib/oauth2/response.rb +87 -49
- data/lib/oauth2/strategy/assertion.rb +10 -7
- data/lib/oauth2/strategy/auth_code.rb +13 -3
- data/lib/oauth2/strategy/client_credentials.rb +2 -2
- data/lib/oauth2/strategy/implicit.rb +11 -3
- data/lib/oauth2/strategy/password.rb +14 -4
- data/lib/oauth2/version.rb +2 -1
- data/lib/oauth2.rb +86 -23
- data/sig/oauth2/access_token.rbs +25 -0
- data/sig/oauth2/authenticator.rbs +22 -0
- data/sig/oauth2/client.rbs +52 -0
- data/sig/oauth2/error.rbs +8 -0
- data/sig/oauth2/filtered_attributes.rbs +11 -0
- data/sig/oauth2/response.rbs +18 -0
- data/sig/oauth2/sanitized_logger.rbs +32 -0
- data/sig/oauth2/strategy.rbs +34 -0
- data/sig/oauth2/thing_filter.rbs +10 -0
- data/sig/oauth2/version.rbs +6 -0
- data/sig/oauth2.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +270 -76
- metadata.gz.sig +0 -0
- data/LICENSE +0 -22
data/RUBOCOP.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# RuboCop Usage Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A tale of two RuboCop plugin gems.
|
|
6
|
+
|
|
7
|
+
### RuboCop Gradual
|
|
8
|
+
|
|
9
|
+
This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
|
|
10
|
+
|
|
11
|
+
### RuboCop LTS
|
|
12
|
+
|
|
13
|
+
This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
|
|
14
|
+
RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
|
|
15
|
+
|
|
16
|
+
## Checking RuboCop Violations
|
|
17
|
+
|
|
18
|
+
To check for RuboCop violations in this project, always use:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle exec rake rubocop_gradual:check
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Do not use** the standard RuboCop commands like:
|
|
25
|
+
- `bundle exec rubocop`
|
|
26
|
+
- `rubocop`
|
|
27
|
+
|
|
28
|
+
## Understanding the Lock File
|
|
29
|
+
|
|
30
|
+
The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
|
|
31
|
+
|
|
32
|
+
1. Prevent new violations while gradually fixing existing ones
|
|
33
|
+
2. Track progress on code style improvements
|
|
34
|
+
3. Ensure CI builds don't fail due to pre-existing violations
|
|
35
|
+
|
|
36
|
+
## Common Commands
|
|
37
|
+
|
|
38
|
+
- **Check violations**
|
|
39
|
+
- `bundle exec rake rubocop_gradual`
|
|
40
|
+
- `bundle exec rake rubocop_gradual:check`
|
|
41
|
+
- **(Safe) Autocorrect violations, and update lockfile if no new violations**
|
|
42
|
+
- `bundle exec rake rubocop_gradual:autocorrect`
|
|
43
|
+
- **Force update the lock file (w/o autocorrect) to match violations present in code**
|
|
44
|
+
- `bundle exec rake rubocop_gradual:force_update`
|
|
45
|
+
|
|
46
|
+
## Workflow
|
|
47
|
+
|
|
48
|
+
1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
|
|
49
|
+
a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
|
|
50
|
+
2. If there are new violations, either:
|
|
51
|
+
- Fix them in your code
|
|
52
|
+
- Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
|
|
53
|
+
3. Commit the updated `.rubocop_gradual.lock` file along with your changes
|
|
54
|
+
|
|
55
|
+
## Never add inline RuboCop disables
|
|
56
|
+
|
|
57
|
+
Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
|
|
58
|
+
|
|
59
|
+
- Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
|
|
60
|
+
- Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
|
|
61
|
+
- `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
|
|
62
|
+
- If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
|
|
63
|
+
|
|
64
|
+
In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
|
|
65
|
+
|
|
66
|
+
## Benefits of rubocop_gradual
|
|
67
|
+
|
|
68
|
+
- Allows incremental adoption of code style rules
|
|
69
|
+
- Prevents CI failures due to pre-existing violations
|
|
70
|
+
- Provides a clear record of code style debt
|
|
71
|
+
- Enables focused efforts on improving code quality over time
|
data/SECURITY.md
CHANGED
|
@@ -2,25 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported Versions
|
|
4
4
|
|
|
5
|
-
| Version | Supported |
|
|
6
|
-
|
|
7
|
-
| 2.latest | ✅ |
|
|
8
|
-
| 1.latest | ✅ | 04/2023 | [Tidelift Subscription][tidelift-ref] |
|
|
9
|
-
| <= 1 | ⛔ | ⛔ | ⛔ |
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|----------|-----------|
|
|
7
|
+
| 2.0.latest | ✅ |
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
## Security contact information
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## Reporting a Vulnerability
|
|
16
|
-
|
|
17
|
-
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
|
|
11
|
+
To report a security vulnerability, please use the
|
|
12
|
+
[Tidelift security contact](https://tidelift.com/security).
|
|
18
13
|
Tidelift will coordinate the fix and disclosure.
|
|
19
14
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
Available as part of the Tidelift Subscription.
|
|
15
|
+
## Additional Support
|
|
23
16
|
|
|
24
|
-
|
|
17
|
+
If you are interested in support for versions older than the latest release,
|
|
18
|
+
please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
|
|
19
|
+
or find other sponsorship links in the [README].
|
|
25
20
|
|
|
26
|
-
[
|
|
21
|
+
[README]: README.md
|
data/certs/pboling.pem
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
|
|
3
|
+
ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
|
|
4
|
+
A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
|
|
5
|
+
DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
|
|
6
|
+
LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
|
|
7
|
+
uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
|
|
8
|
+
LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
|
|
9
|
+
mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
|
|
10
|
+
coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
|
|
11
|
+
FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
|
|
12
|
+
yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
|
|
13
|
+
to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
|
|
14
|
+
qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
|
|
15
|
+
fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
|
|
16
|
+
HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
|
|
17
|
+
A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
|
|
18
|
+
ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
|
|
19
|
+
wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
|
|
20
|
+
L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
|
|
21
|
+
GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
|
|
22
|
+
kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
|
|
23
|
+
QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
|
|
24
|
+
0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
|
|
25
|
+
DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
|
|
26
|
+
L9nRqA==
|
|
27
|
+
-----END CERTIFICATE-----
|
data/lib/oauth2/access_token.rb
CHANGED
|
@@ -1,27 +1,81 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# :nocov:
|
|
4
|
+
begin
|
|
5
|
+
# The first version of hashie that has a version file was 1.1.0
|
|
6
|
+
# The first version of hashie that required the version file at runtime was 3.2.0
|
|
7
|
+
# If it has already been loaded then this is very low cost, as Kernel.require uses maintains a cache
|
|
8
|
+
# If this it hasn't this will work to get it loaded, and then we will be able to use
|
|
9
|
+
# defined?(Hashie::Version)
|
|
10
|
+
# as a test.
|
|
11
|
+
# TODO: get rid this mess when we drop Hashie < 3.2, as Hashie will self-load its version then
|
|
12
|
+
require "hashie/version"
|
|
13
|
+
rescue LoadError
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
# :nocov:
|
|
17
|
+
|
|
3
18
|
module OAuth2
|
|
4
19
|
class AccessToken # rubocop:disable Metrics/ClassLength
|
|
5
20
|
TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
|
|
6
21
|
TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
|
|
7
22
|
TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
|
|
8
23
|
|
|
24
|
+
include FilteredAttributes
|
|
25
|
+
|
|
9
26
|
attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
|
|
10
27
|
attr_accessor :options, :refresh_token, :response
|
|
28
|
+
filtered_attributes :token, :refresh_token
|
|
11
29
|
|
|
12
30
|
class << self
|
|
13
31
|
# Initializes an AccessToken from a Hash
|
|
14
32
|
#
|
|
15
|
-
# @param [Client] client the OAuth2::Client instance
|
|
16
|
-
# @param [Hash] hash a hash
|
|
17
|
-
# @option hash [String
|
|
18
|
-
# @
|
|
33
|
+
# @param [OAuth2::Client] client the OAuth2::Client instance
|
|
34
|
+
# @param [Hash] hash a hash containing the token and other properties
|
|
35
|
+
# @option hash [String] 'access_token' the access token value
|
|
36
|
+
# @option hash [String] 'id_token' alternative key for the access token value
|
|
37
|
+
# @option hash [String] 'token' alternative key for the access token value
|
|
38
|
+
# @option hash [String] 'refresh_token' (optional) the refresh token value
|
|
39
|
+
# @option hash [Integer, String] 'expires_in' (optional) number of seconds until token expires
|
|
40
|
+
# @option hash [Integer, String] 'expires_at' (optional) epoch time in seconds when token expires
|
|
41
|
+
# @option hash [Integer, String] 'expires_latency' (optional) seconds to reduce token validity by
|
|
42
|
+
#
|
|
43
|
+
# @return [OAuth2::AccessToken] the initialized AccessToken
|
|
44
|
+
#
|
|
45
|
+
# @note The method will use the first found token key in the following order:
|
|
46
|
+
# 'access_token', 'id_token', 'token' (or their symbolic versions)
|
|
47
|
+
# @note If multiple token keys are present, a warning will be issued unless
|
|
48
|
+
# OAuth2.config.silence_extra_tokens_warning is true
|
|
49
|
+
# @note If no token keys are present, a warning will be issued unless
|
|
50
|
+
# OAuth2.config.silence_no_tokens_warning is true
|
|
51
|
+
# @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
|
|
52
|
+
# @note If snaky key conversion is being used, token_name needs to match the converted key.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# hash = { 'access_token' => 'token_value', 'refresh_token' => 'refresh_value' }
|
|
56
|
+
# access_token = OAuth2::AccessToken.from_hash(client, hash)
|
|
19
57
|
def from_hash(client, hash)
|
|
20
58
|
fresh = hash.dup
|
|
21
|
-
|
|
22
|
-
key
|
|
23
|
-
|
|
24
|
-
|
|
59
|
+
# If token_name is present, then use that key name
|
|
60
|
+
if fresh.key?(:token_name)
|
|
61
|
+
t_key = fresh[:token_name]
|
|
62
|
+
no_tokens_warning(fresh, t_key)
|
|
63
|
+
else
|
|
64
|
+
# Otherwise, if one of the supported default keys is present, use whichever has precedence
|
|
65
|
+
supported_keys = TOKEN_KEY_LOOKUP & fresh.keys
|
|
66
|
+
t_key = supported_keys[0]
|
|
67
|
+
extra_tokens_warning(supported_keys, t_key)
|
|
68
|
+
end
|
|
69
|
+
# :nocov:
|
|
70
|
+
# TODO: Get rid of this branching logic when dropping Hashie < v3.2
|
|
71
|
+
token = if !defined?(Hashie::VERSION) # i.e. <= "1.1.0"; the first Hashie to ship with a VERSION constant
|
|
72
|
+
warn("snaky_hash and oauth2 will drop support for Hashie v0 in the next major version. Please upgrade to a modern Hashie.")
|
|
73
|
+
# There is a bug in Hashie v0, which is accounts for.
|
|
74
|
+
fresh.delete(t_key) || fresh[t_key] || ""
|
|
75
|
+
else
|
|
76
|
+
fresh.delete(t_key) || ""
|
|
77
|
+
end
|
|
78
|
+
# :nocov:
|
|
25
79
|
new(client, token, fresh)
|
|
26
80
|
end
|
|
27
81
|
|
|
@@ -43,10 +97,31 @@ module OAuth2
|
|
|
43
97
|
|
|
44
98
|
warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
|
|
45
99
|
end
|
|
100
|
+
|
|
101
|
+
def no_tokens_warning(hash, key)
|
|
102
|
+
return if OAuth2.config.silence_no_tokens_warning
|
|
103
|
+
return if key && hash.key?(key)
|
|
104
|
+
|
|
105
|
+
warn(%[
|
|
106
|
+
OAuth2::AccessToken#from_hash key mismatch.
|
|
107
|
+
Custom token_name (#{key}) is not found in (#{hash.keys})
|
|
108
|
+
You may need to set `snaky: false`. See inline documentation for more info.
|
|
109
|
+
])
|
|
110
|
+
end
|
|
46
111
|
end
|
|
47
112
|
|
|
48
113
|
# Initialize an AccessToken
|
|
49
114
|
#
|
|
115
|
+
# @note For "soon-to-expire"/"clock-skew" functionality see the `:expires_latency` option.
|
|
116
|
+
# @note If no token is provided, the AccessToken will be considered invalid.
|
|
117
|
+
# This is to prevent the possibility of a token being accidentally
|
|
118
|
+
# created with no token value.
|
|
119
|
+
# If you want to create an AccessToken with no token value,
|
|
120
|
+
# you can pass in an empty string or nil for the token value.
|
|
121
|
+
# If you want to create an AccessToken with no token value and
|
|
122
|
+
# no refresh token, you can pass in an empty string or nil for the
|
|
123
|
+
# token value and nil for the refresh token, and `raise_errors: false`.
|
|
124
|
+
#
|
|
50
125
|
# @param [Client] client the OAuth2::Client instance
|
|
51
126
|
# @param [String] token the Access Token value (optional, may not be used in refresh flows)
|
|
52
127
|
# @param [Hash] opts the options to create the Access Token with
|
|
@@ -54,15 +129,22 @@ module OAuth2
|
|
|
54
129
|
# @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
|
|
55
130
|
# @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
|
|
56
131
|
# @option opts [FixNum, String] :expires_latency (nil) the number of seconds by which AccessToken validity will be reduced to offset latency, @version 2.0+
|
|
57
|
-
# @option opts [Symbol] :mode (:header) the transmission mode of the Access Token parameter value
|
|
58
|
-
# one of :header, :body or :query
|
|
132
|
+
# @option opts [Symbol, Hash, or callable] :mode (:header) the transmission mode of the Access Token parameter value:
|
|
133
|
+
# either one of :header, :body or :query; or a Hash with verb symbols as keys mapping to one of these symbols
|
|
134
|
+
# (e.g., `{get: :query, post: :header, delete: :header}`); or a callable that accepts a request-verb parameter
|
|
135
|
+
# and returns one of these three symbols.
|
|
59
136
|
# @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
|
|
137
|
+
#
|
|
138
|
+
# @example Verb-dependent Hash mode
|
|
139
|
+
# # Send token in query for GET, in header for POST/DELETE, in body for PUT/PATCH
|
|
140
|
+
# OAuth2::AccessToken.new(client, token, mode: {get: :query, post: :header, delete: :header, put: :body, patch: :body})
|
|
60
141
|
# @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
|
|
61
142
|
# Access Token value in :body or :query transmission mode
|
|
143
|
+
# @option opts [String] :token_name (nil) the name of the response parameter that identifies the access token
|
|
144
|
+
# When nil one of TOKEN_KEY_LOOKUP will be used
|
|
62
145
|
def initialize(client, token, opts = {})
|
|
63
146
|
@client = client
|
|
64
147
|
@token = token.to_s
|
|
65
|
-
|
|
66
148
|
opts = opts.dup
|
|
67
149
|
%i[refresh_token expires_in expires_at expires_latency].each do |arg|
|
|
68
150
|
instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
|
|
@@ -70,22 +152,28 @@ module OAuth2
|
|
|
70
152
|
no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
|
|
71
153
|
if no_tokens
|
|
72
154
|
if @client.options[:raise_errors]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
155
|
+
raise Error.new({
|
|
156
|
+
error: "OAuth2::AccessToken has no token",
|
|
157
|
+
error_description: "Options are: #{opts.inspect}",
|
|
158
|
+
})
|
|
159
|
+
elsif !OAuth2.config.silence_no_tokens_warning
|
|
160
|
+
warn("OAuth2::AccessToken has no token")
|
|
77
161
|
end
|
|
78
162
|
end
|
|
79
163
|
# @option opts [Fixnum, String] :expires is deprecated
|
|
80
|
-
@expires_in ||= opts.delete(
|
|
164
|
+
@expires_in ||= opts.delete("expires")
|
|
81
165
|
@expires_in &&= @expires_in.to_i
|
|
82
166
|
@expires_at &&= convert_expires_at(@expires_at)
|
|
83
167
|
@expires_latency &&= @expires_latency.to_i
|
|
84
|
-
@expires_at ||= Time.now.to_i + @expires_in if @expires_in
|
|
168
|
+
@expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
|
|
85
169
|
@expires_at -= @expires_latency if @expires_latency
|
|
86
|
-
@options = {
|
|
87
|
-
|
|
88
|
-
|
|
170
|
+
@options = {
|
|
171
|
+
mode: opts.delete(:mode) || :header,
|
|
172
|
+
header_format: opts.delete(:header_format) || "Bearer %s",
|
|
173
|
+
param_name: opts.delete(:param_name) || "access_token",
|
|
174
|
+
}
|
|
175
|
+
@options[:token_name] = opts.delete(:token_name) if opts.key?(:token_name)
|
|
176
|
+
|
|
89
177
|
@params = opts
|
|
90
178
|
end
|
|
91
179
|
|
|
@@ -96,33 +184,40 @@ module OAuth2
|
|
|
96
184
|
@params[key]
|
|
97
185
|
end
|
|
98
186
|
|
|
99
|
-
# Whether
|
|
187
|
+
# Whether the token expires
|
|
100
188
|
#
|
|
101
189
|
# @return [Boolean]
|
|
102
190
|
def expires?
|
|
103
191
|
!!@expires_at
|
|
104
192
|
end
|
|
105
193
|
|
|
106
|
-
#
|
|
194
|
+
# Check if token is expired
|
|
107
195
|
#
|
|
108
|
-
# @return [Boolean]
|
|
196
|
+
# @return [Boolean] true if the token is expired, false otherwise
|
|
109
197
|
def expired?
|
|
110
198
|
expires? && (expires_at <= Time.now.to_i)
|
|
111
199
|
end
|
|
112
200
|
|
|
113
201
|
# Refreshes the current Access Token
|
|
114
202
|
#
|
|
115
|
-
# @
|
|
116
|
-
# @
|
|
117
|
-
|
|
118
|
-
|
|
203
|
+
# @param [Hash] params additional params to pass to the refresh token request
|
|
204
|
+
# @param [Hash] access_token_opts options that will be passed to the AccessToken initialization
|
|
205
|
+
#
|
|
206
|
+
# @yield [opts] The block to modify the refresh token request options
|
|
207
|
+
# @yieldparam [Hash] opts The options hash that can be modified
|
|
208
|
+
#
|
|
209
|
+
# @return [OAuth2::AccessToken] a new AccessToken instance
|
|
210
|
+
#
|
|
211
|
+
# @note current token's options are carried over to the new AccessToken
|
|
212
|
+
def refresh(params = {}, access_token_opts = {}, &block)
|
|
213
|
+
raise OAuth2::Error.new({error: "A refresh_token is not available"}) unless refresh_token
|
|
119
214
|
|
|
120
|
-
params[:grant_type] =
|
|
215
|
+
params[:grant_type] = "refresh_token"
|
|
121
216
|
params[:refresh_token] = refresh_token
|
|
122
|
-
new_token = @client.get_token(params, access_token_opts)
|
|
217
|
+
new_token = @client.get_token(params, access_token_opts, &block)
|
|
123
218
|
new_token.options = options
|
|
124
219
|
if new_token.refresh_token
|
|
125
|
-
# Keep it
|
|
220
|
+
# Keep it if there is one
|
|
126
221
|
else
|
|
127
222
|
new_token.refresh_token = refresh_token
|
|
128
223
|
end
|
|
@@ -130,13 +225,90 @@ module OAuth2
|
|
|
130
225
|
end
|
|
131
226
|
# A compatibility alias
|
|
132
227
|
# @note does not modify the receiver, so bang is not the default method
|
|
133
|
-
|
|
228
|
+
alias_method :refresh!, :refresh
|
|
229
|
+
|
|
230
|
+
# Revokes the token at the authorization server
|
|
231
|
+
#
|
|
232
|
+
# @param [Hash] params additional parameters to be sent during revocation
|
|
233
|
+
# @option params [String, Symbol, nil] :token_type_hint ('access_token' or 'refresh_token') hint about which token to revoke
|
|
234
|
+
# @option params [Symbol] :token_method (:post_with_query_string) overrides OAuth2::Client#options[:token_method]
|
|
235
|
+
#
|
|
236
|
+
# @yield [req] The block is passed the request being made, allowing customization
|
|
237
|
+
# @yieldparam [Faraday::Request] req The request object that can be modified
|
|
238
|
+
#
|
|
239
|
+
# @return [OAuth2::Response] OAuth2::Response instance
|
|
240
|
+
#
|
|
241
|
+
# @api public
|
|
242
|
+
#
|
|
243
|
+
# @raise [OAuth2::Error] if token_type_hint is invalid or the specified token is not available
|
|
244
|
+
#
|
|
245
|
+
# @note If the token passed to the request
|
|
246
|
+
# is an access token, the server MAY revoke the respective refresh
|
|
247
|
+
# token as well.
|
|
248
|
+
# @note If the token passed to the request
|
|
249
|
+
# is a refresh token and the authorization server supports the
|
|
250
|
+
# revocation of access tokens, then the authorization server SHOULD
|
|
251
|
+
# also invalidate all access tokens based on the same authorization
|
|
252
|
+
# grant
|
|
253
|
+
# @note If the server responds with HTTP status code 503, your code must
|
|
254
|
+
# assume the token still exists and may retry after a reasonable delay.
|
|
255
|
+
# The server may include a "Retry-After" header in the response to
|
|
256
|
+
# indicate how long the service is expected to be unavailable to the
|
|
257
|
+
# requesting client.
|
|
258
|
+
#
|
|
259
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7009
|
|
260
|
+
# @see https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
|
|
261
|
+
def revoke(params = {}, &block)
|
|
262
|
+
token_type_hint_orig = params.delete(:token_type_hint)
|
|
263
|
+
token_type_hint = nil
|
|
264
|
+
revoke_token = case token_type_hint_orig
|
|
265
|
+
when "access_token", :access_token
|
|
266
|
+
token_type_hint = "access_token"
|
|
267
|
+
token
|
|
268
|
+
when "refresh_token", :refresh_token
|
|
269
|
+
token_type_hint = "refresh_token"
|
|
270
|
+
refresh_token
|
|
271
|
+
when nil
|
|
272
|
+
if token
|
|
273
|
+
token_type_hint = "access_token"
|
|
274
|
+
token
|
|
275
|
+
elsif refresh_token
|
|
276
|
+
token_type_hint = "refresh_token"
|
|
277
|
+
refresh_token
|
|
278
|
+
end
|
|
279
|
+
else
|
|
280
|
+
raise OAuth2::Error.new({error: "token_type_hint must be one of [nil, :refresh_token, :access_token], so if you need something else consider using a subclass or entirely custom AccessToken class."})
|
|
281
|
+
end
|
|
282
|
+
raise OAuth2::Error.new({error: "#{token_type_hint || "unknown token type"} is not available for revoking"}) unless revoke_token && !revoke_token.empty?
|
|
283
|
+
|
|
284
|
+
@client.revoke_token(revoke_token, token_type_hint, params, &block)
|
|
285
|
+
end
|
|
286
|
+
# A compatibility alias
|
|
287
|
+
# @note does not modify the receiver, so bang is not the default method
|
|
288
|
+
alias_method :revoke!, :revoke
|
|
134
289
|
|
|
135
290
|
# Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
|
|
136
291
|
#
|
|
292
|
+
# @note Don't return expires_latency because it has already been deducted from expires_at
|
|
293
|
+
#
|
|
137
294
|
# @return [Hash] a hash of AccessToken property values
|
|
138
295
|
def to_hash
|
|
139
|
-
|
|
296
|
+
hsh = {
|
|
297
|
+
access_token: token,
|
|
298
|
+
refresh_token: refresh_token,
|
|
299
|
+
expires_at: expires_at,
|
|
300
|
+
mode: options[:mode],
|
|
301
|
+
header_format: options[:header_format],
|
|
302
|
+
param_name: options[:param_name],
|
|
303
|
+
}
|
|
304
|
+
hsh[:token_name] = options[:token_name] if options.key?(:token_name)
|
|
305
|
+
# TODO: Switch when dropping Ruby < 2.5 support
|
|
306
|
+
# params.transform_keys(&:to_sym) # Ruby 2.5 only
|
|
307
|
+
# Old Ruby transform_keys alternative:
|
|
308
|
+
sheesh = @params.each_with_object({}) { |(k, v), memo|
|
|
309
|
+
memo[k.to_sym] = v
|
|
310
|
+
}
|
|
311
|
+
sheesh.merge(hsh)
|
|
140
312
|
end
|
|
141
313
|
|
|
142
314
|
# Make a request with the Access Token
|
|
@@ -144,9 +316,18 @@ module OAuth2
|
|
|
144
316
|
# @param [Symbol] verb the HTTP request method
|
|
145
317
|
# @param [String] path the HTTP URL path of the request
|
|
146
318
|
# @param [Hash] opts the options to make the request with
|
|
147
|
-
#
|
|
319
|
+
# @option opts [Hash] :params additional URL parameters
|
|
320
|
+
# @option opts [Hash, String] :body the request body
|
|
321
|
+
# @option opts [Hash] :headers request headers
|
|
322
|
+
#
|
|
323
|
+
# @yield [req] The block to modify the request
|
|
324
|
+
# @yieldparam [Faraday::Request] req The request object that can be modified
|
|
325
|
+
#
|
|
326
|
+
# @return [OAuth2::Response] the response from the request
|
|
327
|
+
#
|
|
328
|
+
# @see OAuth2::Client#request
|
|
148
329
|
def request(verb, path, opts = {}, &block)
|
|
149
|
-
configure_authentication!(opts)
|
|
330
|
+
configure_authentication!(opts, verb)
|
|
150
331
|
@client.request(verb, path, opts, &block)
|
|
151
332
|
end
|
|
152
333
|
|
|
@@ -187,17 +368,31 @@ module OAuth2
|
|
|
187
368
|
|
|
188
369
|
# Get the headers hash (includes Authorization token)
|
|
189
370
|
def headers
|
|
190
|
-
{
|
|
371
|
+
{"Authorization" => options[:header_format] % token}
|
|
191
372
|
end
|
|
192
373
|
|
|
193
374
|
private
|
|
194
375
|
|
|
195
|
-
def configure_authentication!(opts)
|
|
196
|
-
|
|
376
|
+
def configure_authentication!(opts, verb)
|
|
377
|
+
mode_opt = options[:mode]
|
|
378
|
+
mode =
|
|
379
|
+
if mode_opt.respond_to?(:call)
|
|
380
|
+
mode_opt.call(verb)
|
|
381
|
+
elsif mode_opt.is_a?(Hash)
|
|
382
|
+
key = verb.to_sym
|
|
383
|
+
# Try symbol key first, then string key; default to :header when missing
|
|
384
|
+
mode_opt[key] || mode_opt[key.to_s] || :header
|
|
385
|
+
else
|
|
386
|
+
mode_opt
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
case mode
|
|
197
390
|
when :header
|
|
198
391
|
opts[:headers] ||= {}
|
|
199
392
|
opts[:headers].merge!(headers)
|
|
200
393
|
when :query
|
|
394
|
+
# OAuth 2.1 note: Bearer tokens in the query string are omitted from the spec due to security risks.
|
|
395
|
+
# Prefer the default :header mode whenever possible.
|
|
201
396
|
opts[:params] ||= {}
|
|
202
397
|
opts[:params][options[:param_name]] = token
|
|
203
398
|
when :body
|
|
@@ -209,7 +404,7 @@ module OAuth2
|
|
|
209
404
|
end
|
|
210
405
|
# @todo support for multi-part (file uploads)
|
|
211
406
|
else
|
|
212
|
-
raise("invalid :mode option of #{
|
|
407
|
+
raise("invalid :mode option of #{mode}")
|
|
213
408
|
end
|
|
214
409
|
end
|
|
215
410
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OAuth2
|
|
4
|
+
AUTH_SANITIZER = begin
|
|
5
|
+
auth_sanitizer_requirement = Gem::Requirement.new("~> 0.2", ">= 0.2.1")
|
|
6
|
+
auth_sanitizer_spec = Gem.loaded_specs["auth-sanitizer"]
|
|
7
|
+
unless auth_sanitizer_spec && auth_sanitizer_requirement.satisfied_by?(auth_sanitizer_spec.version)
|
|
8
|
+
# :nocov:
|
|
9
|
+
auth_sanitizer_spec = Gem::Specification.find_by_name("auth-sanitizer", auth_sanitizer_requirement)
|
|
10
|
+
# :nocov:
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
auth_sanitizer_loader_path = File.join(
|
|
14
|
+
auth_sanitizer_spec.full_gem_path,
|
|
15
|
+
"lib/auth_sanitizer/loader.rb"
|
|
16
|
+
)
|
|
17
|
+
unless File.file?(auth_sanitizer_loader_path)
|
|
18
|
+
# :nocov:
|
|
19
|
+
raise LoadError, "oauth2 requires auth-sanitizer #{auth_sanitizer_requirement}; " \
|
|
20
|
+
"loader not found at #{auth_sanitizer_loader_path}"
|
|
21
|
+
# :nocov:
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
auth_sanitizer_loader_namespace = Module.new
|
|
25
|
+
auth_sanitizer_loader_namespace.module_eval(
|
|
26
|
+
File.read(auth_sanitizer_loader_path),
|
|
27
|
+
auth_sanitizer_loader_path,
|
|
28
|
+
1
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
auth_sanitizer_loader_namespace.
|
|
32
|
+
const_get(:AuthSanitizer).
|
|
33
|
+
const_get(:Loader).
|
|
34
|
+
load_isolated
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/oauth2/authenticator.rb
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require "base64"
|
|
4
4
|
|
|
5
5
|
module OAuth2
|
|
6
|
+
# Builds and applies client authentication to token and revoke requests.
|
|
7
|
+
#
|
|
8
|
+
# Depending on the selected mode, credentials are applied as Basic Auth
|
|
9
|
+
# headers, request body parameters, or only the client_id is sent (TLS).
|
|
6
10
|
class Authenticator
|
|
11
|
+
include FilteredAttributes
|
|
12
|
+
|
|
13
|
+
# @return [Symbol, String] Authentication mode (e.g., :basic_auth, :request_body, :tls_client_auth, :private_key_jwt)
|
|
14
|
+
# @return [String, nil] Client identifier
|
|
15
|
+
# @return [String, nil] Client secret (filtered in inspected output)
|
|
7
16
|
attr_reader :mode, :id, :secret
|
|
17
|
+
filtered_attributes :secret
|
|
8
18
|
|
|
19
|
+
# Create a new Authenticator
|
|
20
|
+
#
|
|
21
|
+
# @param [String, nil] id Client identifier
|
|
22
|
+
# @param [String, nil] secret Client secret
|
|
23
|
+
# @param [Symbol, String] mode Authentication mode
|
|
9
24
|
def initialize(id, secret, mode)
|
|
10
25
|
@id = id
|
|
11
26
|
@secret = secret
|
|
@@ -14,7 +29,7 @@ module OAuth2
|
|
|
14
29
|
|
|
15
30
|
# Apply the request credentials used to authenticate to the Authorization Server
|
|
16
31
|
#
|
|
17
|
-
# Depending on configuration, this might be as request params or as an
|
|
32
|
+
# Depending on the configuration, this might be as request params or as an
|
|
18
33
|
# Authorization header.
|
|
19
34
|
#
|
|
20
35
|
# User-provided params and header take precedence.
|
|
@@ -36,40 +51,59 @@ module OAuth2
|
|
|
36
51
|
end
|
|
37
52
|
end
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
class << self
|
|
55
|
+
# Encodes a Basic Authorization header value for the provided credentials.
|
|
56
|
+
#
|
|
57
|
+
# @param [String] user The client identifier
|
|
58
|
+
# @param [String] password The client secret
|
|
59
|
+
# @return [String] The value to use for the Authorization header
|
|
60
|
+
def encode_basic_auth(user, password)
|
|
61
|
+
"Basic #{Base64.strict_encode64("#{user}:#{password}")}"
|
|
62
|
+
end
|
|
41
63
|
end
|
|
42
64
|
|
|
43
65
|
private
|
|
44
66
|
|
|
45
67
|
# Adds client_id and client_secret request parameters if they are not
|
|
46
68
|
# already set.
|
|
69
|
+
#
|
|
70
|
+
# @param [Hash] params Request parameters
|
|
71
|
+
# @return [Hash] Updated parameters including client_id and client_secret
|
|
47
72
|
def apply_params_auth(params)
|
|
48
73
|
result = {}
|
|
49
|
-
result[
|
|
50
|
-
result[
|
|
74
|
+
result["client_id"] = id unless id.nil?
|
|
75
|
+
result["client_secret"] = secret unless secret.nil?
|
|
51
76
|
result.merge(params)
|
|
52
77
|
end
|
|
53
78
|
|
|
54
|
-
# When using schemes that don't require the client_secret to be passed
|
|
79
|
+
# When using schemes that don't require the client_secret to be passed (e.g., TLS Client Auth),
|
|
55
80
|
# we don't want to send the secret
|
|
81
|
+
#
|
|
82
|
+
# @param [Hash] params Request parameters
|
|
83
|
+
# @return [Hash] Updated parameters including only client_id
|
|
56
84
|
def apply_client_id(params)
|
|
57
85
|
result = {}
|
|
58
|
-
result[
|
|
86
|
+
result["client_id"] = id unless id.nil?
|
|
59
87
|
result.merge(params)
|
|
60
88
|
end
|
|
61
89
|
|
|
62
90
|
# Adds an `Authorization` header with Basic Auth credentials if and only if
|
|
63
91
|
# it is not already set in the params.
|
|
92
|
+
#
|
|
93
|
+
# @param [Hash] params Request parameters (may include :headers)
|
|
94
|
+
# @return [Hash] Updated parameters with Authorization header
|
|
64
95
|
def apply_basic_auth(params)
|
|
65
96
|
headers = params.fetch(:headers, {})
|
|
66
97
|
headers = basic_auth_header.merge(headers)
|
|
67
98
|
params.merge(headers: headers)
|
|
68
99
|
end
|
|
69
100
|
|
|
101
|
+
# Build the Basic Authorization header.
|
|
102
|
+
#
|
|
70
103
|
# @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
|
|
104
|
+
# @return [Hash] Header hash containing the Authorization entry
|
|
71
105
|
def basic_auth_header
|
|
72
|
-
{
|
|
106
|
+
{"Authorization" => self.class.encode_basic_auth(id, secret)}
|
|
73
107
|
end
|
|
74
108
|
end
|
|
75
109
|
end
|