oauth2 1.4.7 → 2.0.20
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 +0 -0
- data/CHANGELOG.md +811 -76
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +24 -23
- data/CONTRIBUTING.md +221 -0
- data/FUNDING.md +74 -0
- data/IRP.md +107 -0
- data/{LICENSE → LICENSE.txt} +2 -2
- data/OIDC.md +167 -0
- data/README.md +1468 -166
- data/REEK +2 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +24 -0
- data/THREAT_MODEL.md +94 -0
- data/lib/oauth2/access_token.rb +276 -40
- data/lib/oauth2/auth_sanitizer.rb +36 -0
- data/lib/oauth2/authenticator.rb +51 -10
- data/lib/oauth2/client.rb +444 -124
- data/lib/oauth2/error.rb +63 -24
- data/lib/oauth2/filtered_attributes.rb +10 -0
- data/lib/oauth2/response.rb +138 -43
- data/lib/oauth2/strategy/assertion.rb +71 -41
- data/lib/oauth2/strategy/auth_code.rb +28 -5
- data/lib/oauth2/strategy/base.rb +2 -0
- data/lib/oauth2/strategy/client_credentials.rb +6 -4
- data/lib/oauth2/strategy/implicit.rb +20 -3
- data/lib/oauth2/strategy/password.rb +17 -5
- data/lib/oauth2/version.rb +2 -59
- data/lib/oauth2.rb +103 -12
- 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 +5 -0
- data/sig/oauth2.rbs +9 -0
- data.tar.gz.sig +0 -0
- metadata +293 -102
- metadata.gz.sig +4 -0
- data/lib/oauth2/mac_token.rb +0 -130
- data/spec/helper.rb +0 -37
- data/spec/oauth2/access_token_spec.rb +0 -216
- data/spec/oauth2/authenticator_spec.rb +0 -84
- data/spec/oauth2/client_spec.rb +0 -506
- data/spec/oauth2/mac_token_spec.rb +0 -117
- data/spec/oauth2/response_spec.rb +0 -90
- data/spec/oauth2/strategy/assertion_spec.rb +0 -58
- data/spec/oauth2/strategy/auth_code_spec.rb +0 -107
- data/spec/oauth2/strategy/base_spec.rb +0 -5
- data/spec/oauth2/strategy/client_credentials_spec.rb +0 -69
- data/spec/oauth2/strategy/implicit_spec.rb +0 -26
- data/spec/oauth2/strategy/password_spec.rb +0 -55
- data/spec/oauth2/version_spec.rb +0 -23
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
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|----------|-----------|
|
|
7
|
+
| 1.latest | ✅ |
|
|
8
|
+
|
|
9
|
+
## Security contact information
|
|
10
|
+
|
|
11
|
+
To report a security vulnerability, please use the
|
|
12
|
+
[Tidelift security contact](https://tidelift.com/security).
|
|
13
|
+
Tidelift will coordinate the fix and disclosure.
|
|
14
|
+
|
|
15
|
+
More detailed explanation of the process is in [IRP.md][IRP].
|
|
16
|
+
|
|
17
|
+
## Additional Support
|
|
18
|
+
|
|
19
|
+
If you are interested in support for versions older than the latest release,
|
|
20
|
+
please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
|
|
21
|
+
or find other sponsorship links in the [README].
|
|
22
|
+
|
|
23
|
+
[README]: README.md
|
|
24
|
+
[IRP]: IRP.md
|
data/THREAT_MODEL.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Threat Model Outline for oauth2 Ruby Gem
|
|
2
|
+
|
|
3
|
+
## 1. Overview
|
|
4
|
+
This document outlines the threat model for the `oauth2` Ruby gem, which implements OAuth 2.0, 2.1, and OIDC Core protocols. The gem is used to facilitate secure authorization and authentication in Ruby applications.
|
|
5
|
+
|
|
6
|
+
## 2. Assets to Protect
|
|
7
|
+
- OAuth access tokens, refresh tokens, and ID tokens
|
|
8
|
+
- User credentials (if handled)
|
|
9
|
+
- Client secrets and application credentials
|
|
10
|
+
- Sensitive user data accessed via OAuth
|
|
11
|
+
- Private keys and certificates (for signing/verifying tokens)
|
|
12
|
+
|
|
13
|
+
## 3. Potential Threat Actors
|
|
14
|
+
- External attackers (internet-based)
|
|
15
|
+
- Malicious OAuth clients or resource servers
|
|
16
|
+
- Insiders (developers, maintainers)
|
|
17
|
+
- Compromised dependencies
|
|
18
|
+
|
|
19
|
+
## 4. Attack Surfaces
|
|
20
|
+
- OAuth endpoints (authorization, token, revocation, introspection)
|
|
21
|
+
- HTTP request/response handling
|
|
22
|
+
- Token storage and management
|
|
23
|
+
- Configuration files and environment variables
|
|
24
|
+
- Dependency supply chain
|
|
25
|
+
|
|
26
|
+
## 5. Threats and Mitigations
|
|
27
|
+
|
|
28
|
+
### 5.1 Token Leakage
|
|
29
|
+
- **Threat:** Tokens exposed via logs, URLs, or insecure storage
|
|
30
|
+
- **Mitigations:**
|
|
31
|
+
- Avoid logging sensitive tokens
|
|
32
|
+
- Use secure storage mechanisms
|
|
33
|
+
- Never expose tokens in URLs
|
|
34
|
+
|
|
35
|
+
### 5.2 Token Replay and Forgery
|
|
36
|
+
- **Threat:** Attackers reuse or forge tokens
|
|
37
|
+
- **Mitigations:**
|
|
38
|
+
- Validate token signatures and claims
|
|
39
|
+
- Use short-lived tokens and refresh tokens
|
|
40
|
+
- Implement token revocation
|
|
41
|
+
|
|
42
|
+
### 5.3 Insecure Communication
|
|
43
|
+
- **Threat:** Data intercepted via MITM attacks
|
|
44
|
+
- **Mitigations:**
|
|
45
|
+
- Enforce HTTPS for all communications
|
|
46
|
+
- Validate SSL/TLS certificates
|
|
47
|
+
|
|
48
|
+
### 5.4 Client Secret Exposure
|
|
49
|
+
- **Threat:** Client secrets leaked in code or version control
|
|
50
|
+
- **Mitigations:**
|
|
51
|
+
- Store secrets in environment variables or secure vaults
|
|
52
|
+
- Never commit secrets to source control
|
|
53
|
+
|
|
54
|
+
### 5.5 Dependency Vulnerabilities
|
|
55
|
+
- **Threat:** Vulnerabilities in third-party libraries
|
|
56
|
+
- **Mitigations:**
|
|
57
|
+
- Regularly update dependencies
|
|
58
|
+
- Use tools like `bundler-audit` for vulnerability scanning
|
|
59
|
+
|
|
60
|
+
### 5.6 Improper Input Validation
|
|
61
|
+
- **Threat:** Injection attacks via untrusted input
|
|
62
|
+
- **Mitigations:**
|
|
63
|
+
- Validate and sanitize all inputs
|
|
64
|
+
- Use parameterized queries and safe APIs
|
|
65
|
+
|
|
66
|
+
### 5.7 Request-Target Trust Boundary Expansion
|
|
67
|
+
- **Threat:** Applications may pass untrusted or insufficiently validated absolute URLs into request paths that can carry OAuth credentials or authenticated state.
|
|
68
|
+
- **Risk:** This can expand trust boundaries, contributing to token leakage, authenticated requests to unintended hosts, or SSRF-like pivoting in the surrounding application.
|
|
69
|
+
- **Mitigations:**
|
|
70
|
+
- Prefer relative paths where practical
|
|
71
|
+
- Do not pass untrusted absolute URLs into token-bearing clients
|
|
72
|
+
- Validate or allowlist outbound request targets at the application layer
|
|
73
|
+
- Treat request-target validation as a separate concern from log redaction and token storage
|
|
74
|
+
|
|
75
|
+
### 5.8 Insufficient Logging and Monitoring
|
|
76
|
+
- **Threat:** Attacks go undetected
|
|
77
|
+
- **Mitigations:**
|
|
78
|
+
- Log security-relevant events (without sensitive data)
|
|
79
|
+
- Monitor for suspicious activity
|
|
80
|
+
|
|
81
|
+
## 6. Assumptions
|
|
82
|
+
- The gem is used in a secure environment with up-to-date Ruby and dependencies
|
|
83
|
+
- End-users are responsible for secure configuration and deployment
|
|
84
|
+
|
|
85
|
+
## 7. Out of Scope
|
|
86
|
+
- Security of external OAuth providers
|
|
87
|
+
- Application-level business logic
|
|
88
|
+
|
|
89
|
+
## 8. References
|
|
90
|
+
- [OAuth 2.0 Threat Model and Security Considerations (RFC 6819)](https://tools.ietf.org/html/rfc6819)
|
|
91
|
+
- [OWASP Top Ten](https://owasp.org/www-project-top-ten/)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
This outline should be reviewed and updated regularly as the project evolves.
|
data/lib/oauth2/access_token.rb
CHANGED
|
@@ -1,57 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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
|
+
|
|
1
18
|
module OAuth2
|
|
2
|
-
class AccessToken
|
|
3
|
-
|
|
4
|
-
|
|
19
|
+
class AccessToken # rubocop:disable Metrics/ClassLength
|
|
20
|
+
TOKEN_KEYS_STR = %w[access_token id_token token accessToken idToken].freeze
|
|
21
|
+
TOKEN_KEYS_SYM = %i[access_token id_token token accessToken idToken].freeze
|
|
22
|
+
TOKEN_KEY_LOOKUP = TOKEN_KEYS_STR + TOKEN_KEYS_SYM
|
|
23
|
+
|
|
24
|
+
include FilteredAttributes
|
|
25
|
+
|
|
26
|
+
attr_reader :client, :token, :expires_in, :expires_at, :expires_latency, :params
|
|
27
|
+
attr_accessor :options, :refresh_token, :response
|
|
28
|
+
filtered_attributes :token, :refresh_token
|
|
5
29
|
|
|
6
|
-
# Should these methods be deprecated?
|
|
7
30
|
class << self
|
|
8
31
|
# Initializes an AccessToken from a Hash
|
|
9
32
|
#
|
|
10
|
-
# @param [Client] the OAuth2::Client instance
|
|
11
|
-
# @param [Hash] a hash
|
|
12
|
-
# @
|
|
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)
|
|
13
57
|
def from_hash(client, hash)
|
|
14
|
-
|
|
15
|
-
|
|
58
|
+
fresh = hash.dup
|
|
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:
|
|
79
|
+
new(client, token, fresh)
|
|
16
80
|
end
|
|
17
81
|
|
|
18
82
|
# Initializes an AccessToken from a key/value application/x-www-form-urlencoded string
|
|
19
83
|
#
|
|
20
84
|
# @param [Client] client the OAuth2::Client instance
|
|
21
85
|
# @param [String] kvform the application/x-www-form-urlencoded string
|
|
22
|
-
# @return [AccessToken] the
|
|
86
|
+
# @return [AccessToken] the initialized AccessToken
|
|
23
87
|
def from_kvform(client, kvform)
|
|
24
88
|
from_hash(client, Rack::Utils.parse_query(kvform))
|
|
25
89
|
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Having too many is sus, and may lead to bugs. Having none is fine (e.g. refresh flow doesn't need a token).
|
|
94
|
+
def extra_tokens_warning(supported_keys, key)
|
|
95
|
+
return if OAuth2.config.silence_extra_tokens_warning
|
|
96
|
+
return if supported_keys.length <= 1
|
|
97
|
+
|
|
98
|
+
warn("OAuth2::AccessToken.from_hash: `hash` contained more than one 'token' key (#{supported_keys}); using #{key.inspect}.")
|
|
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
|
|
26
111
|
end
|
|
27
112
|
|
|
28
|
-
#
|
|
113
|
+
# Initialize an AccessToken
|
|
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`.
|
|
29
124
|
#
|
|
30
125
|
# @param [Client] client the OAuth2::Client instance
|
|
31
|
-
# @param [String] token the Access Token value
|
|
126
|
+
# @param [String] token the Access Token value (optional, may not be used in refresh flows)
|
|
32
127
|
# @param [Hash] opts the options to create the Access Token with
|
|
33
128
|
# @option opts [String] :refresh_token (nil) the refresh_token value
|
|
34
129
|
# @option opts [FixNum, String] :expires_in (nil) the number of seconds in which the AccessToken will expire
|
|
35
130
|
# @option opts [FixNum, String] :expires_at (nil) the epoch time in seconds in which AccessToken will expire
|
|
36
|
-
# @option opts [
|
|
37
|
-
#
|
|
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+
|
|
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.
|
|
38
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})
|
|
39
141
|
# @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
|
|
40
142
|
# Access Token value in :body or :query transmission mode
|
|
41
|
-
|
|
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
|
|
145
|
+
def initialize(client, token, opts = {})
|
|
42
146
|
@client = client
|
|
43
147
|
@token = token.to_s
|
|
44
148
|
opts = opts.dup
|
|
45
|
-
[
|
|
149
|
+
%i[refresh_token expires_in expires_at expires_latency].each do |arg|
|
|
46
150
|
instance_variable_set("@#{arg}", opts.delete(arg) || opts.delete(arg.to_s))
|
|
47
151
|
end
|
|
48
|
-
@
|
|
152
|
+
no_tokens = (@token.nil? || @token.empty?) && (@refresh_token.nil? || @refresh_token.empty?)
|
|
153
|
+
if no_tokens
|
|
154
|
+
if @client.options[:raise_errors]
|
|
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")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
# @option opts [Fixnum, String] :expires is deprecated
|
|
164
|
+
@expires_in ||= opts.delete("expires")
|
|
49
165
|
@expires_in &&= @expires_in.to_i
|
|
50
166
|
@expires_at &&= convert_expires_at(@expires_at)
|
|
51
|
-
@
|
|
52
|
-
@
|
|
53
|
-
|
|
54
|
-
|
|
167
|
+
@expires_latency &&= @expires_latency.to_i
|
|
168
|
+
@expires_at ||= Time.now.to_i + @expires_in if @expires_in && !@expires_in.zero?
|
|
169
|
+
@expires_at -= @expires_latency if @expires_latency
|
|
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
|
+
|
|
55
177
|
@params = opts
|
|
56
178
|
end
|
|
57
179
|
|
|
@@ -62,40 +184,131 @@ module OAuth2
|
|
|
62
184
|
@params[key]
|
|
63
185
|
end
|
|
64
186
|
|
|
65
|
-
# Whether
|
|
187
|
+
# Whether the token expires
|
|
66
188
|
#
|
|
67
189
|
# @return [Boolean]
|
|
68
190
|
def expires?
|
|
69
191
|
!!@expires_at
|
|
70
192
|
end
|
|
71
193
|
|
|
72
|
-
#
|
|
194
|
+
# Check if token is expired
|
|
73
195
|
#
|
|
74
|
-
# @return [Boolean]
|
|
196
|
+
# @return [Boolean] true if the token is expired, false otherwise
|
|
75
197
|
def expired?
|
|
76
|
-
expires? && (expires_at
|
|
198
|
+
expires? && (expires_at <= Time.now.to_i)
|
|
77
199
|
end
|
|
78
200
|
|
|
79
201
|
# Refreshes the current Access Token
|
|
80
202
|
#
|
|
81
|
-
# @
|
|
82
|
-
# @
|
|
83
|
-
|
|
84
|
-
|
|
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
|
|
85
214
|
|
|
86
|
-
params[:grant_type] =
|
|
215
|
+
params[:grant_type] = "refresh_token"
|
|
87
216
|
params[:refresh_token] = refresh_token
|
|
88
|
-
new_token = @client.get_token(params)
|
|
217
|
+
new_token = @client.get_token(params, access_token_opts, &block)
|
|
89
218
|
new_token.options = options
|
|
90
|
-
|
|
219
|
+
if new_token.refresh_token
|
|
220
|
+
# Keep it if there is one
|
|
221
|
+
else
|
|
222
|
+
new_token.refresh_token = refresh_token
|
|
223
|
+
end
|
|
91
224
|
new_token
|
|
92
225
|
end
|
|
226
|
+
# A compatibility alias
|
|
227
|
+
# @note does not modify the receiver, so bang is not the default method
|
|
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
|
|
93
289
|
|
|
94
290
|
# Convert AccessToken to a hash which can be used to rebuild itself with AccessToken.from_hash
|
|
95
291
|
#
|
|
292
|
+
# @note Don't return expires_latency because it has already been deducted from expires_at
|
|
293
|
+
#
|
|
96
294
|
# @return [Hash] a hash of AccessToken property values
|
|
97
295
|
def to_hash
|
|
98
|
-
|
|
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)
|
|
99
312
|
end
|
|
100
313
|
|
|
101
314
|
# Make a request with the Access Token
|
|
@@ -103,9 +316,18 @@ module OAuth2
|
|
|
103
316
|
# @param [Symbol] verb the HTTP request method
|
|
104
317
|
# @param [String] path the HTTP URL path of the request
|
|
105
318
|
# @param [Hash] opts the options to make the request with
|
|
106
|
-
# @
|
|
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
|
|
107
329
|
def request(verb, path, opts = {}, &block)
|
|
108
|
-
configure_authentication!(opts)
|
|
330
|
+
configure_authentication!(opts, verb)
|
|
109
331
|
@client.request(verb, path, opts, &block)
|
|
110
332
|
end
|
|
111
333
|
|
|
@@ -146,17 +368,31 @@ module OAuth2
|
|
|
146
368
|
|
|
147
369
|
# Get the headers hash (includes Authorization token)
|
|
148
370
|
def headers
|
|
149
|
-
{
|
|
371
|
+
{"Authorization" => options[:header_format] % token}
|
|
150
372
|
end
|
|
151
373
|
|
|
152
374
|
private
|
|
153
375
|
|
|
154
|
-
def configure_authentication!(opts)
|
|
155
|
-
|
|
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
|
|
156
390
|
when :header
|
|
157
391
|
opts[:headers] ||= {}
|
|
158
392
|
opts[:headers].merge!(headers)
|
|
159
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.
|
|
160
396
|
opts[:params] ||= {}
|
|
161
397
|
opts[:params][options[:param_name]] = token
|
|
162
398
|
when :body
|
|
@@ -164,11 +400,11 @@ module OAuth2
|
|
|
164
400
|
if opts[:body].is_a?(Hash)
|
|
165
401
|
opts[:body][options[:param_name]] = token
|
|
166
402
|
else
|
|
167
|
-
opts[:body]
|
|
403
|
+
opts[:body] += "&#{options[:param_name]}=#{token}"
|
|
168
404
|
end
|
|
169
405
|
# @todo support for multi-part (file uploads)
|
|
170
406
|
else
|
|
171
|
-
raise("invalid :mode option of #{
|
|
407
|
+
raise("invalid :mode option of #{mode}")
|
|
172
408
|
end
|
|
173
409
|
end
|
|
174
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.1", ">= 0.1.3")
|
|
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
|