Class: X::OAuth2Authenticator
- Inherits:
-
Authenticator
- Object
- Authenticator
- X::OAuth2Authenticator
- Includes:
- OAuth2Refresh
- Defined in:
- x-core/lib/x/core/oauth2_authenticator.rb
Overview
Handles OAuth 2.0 authentication, refreshing the access token when it expires
X issues a new refresh token with each access token and accepts a refresh token once, so an authenticator refreshes under a lock, and the authenticator of a client passes the tokens each refresh issued, as OAuth2Tokens, to the save_tokens of that client and of each copy of it that shares the authenticator, so that they can be stored.
Processes that share the tokens of a user, storing each refresh with save_tokens, read the store with load_tokens, which a refresh calls under its lock before it refreshes: X accepts a refresh token once, and a process that refreshed with the one another had already spent would be refused. The tokens a refresh takes from the store are not passed to save_tokens, since they came from it.
X issues no refresh token for an authorization without the offline.access scope, so an authenticator built without one authenticates as the user until its access token expires, and refreshes nothing: a request sent with an access token that expired is sent as it is, for the API to reject with Unauthorized, and one the API rejects is not sent again.
Instance Attribute Summary collapse
-
#client_id ⇒ String
readonly
The OAuth 2.0 client ID.
-
#expires_at ⇒ Time?
readonly
The expiration time of the access token.
-
#scopes ⇒ Array<String>?
readonly
The scopes X granted the access token, as last refreshed.
Instance Method Summary collapse
-
#headers(_request) ⇒ Hash{String => String}
Generate the authentication header, refreshing an expired token first.
-
#initialize(client_id:, access_token:, refresh_token: nil, client_secret: nil, expires_at: nil, scopes: nil, load_tokens: nil) ⇒ OAuth2Authenticator
constructor
Initialize a new OAuth 2.0 authenticator.
-
#inspect ⇒ String
Summarize the authenticator for the console without revealing credentials.
-
#refresh! ⇒ OAuth2Tokens
Refresh the access token using the refresh token.
-
#token_expired? ⇒ Boolean
Check if the access token has expired or will expire soon.
Constructor Details
#initialize(client_id:, access_token:, refresh_token: nil, client_secret: nil, expires_at: nil, scopes: nil, load_tokens: nil) ⇒ OAuth2Authenticator
Initialize a new OAuth 2.0 authenticator
98 99 100 101 102 103 104 105 106 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 98 def initialize(client_id:, access_token:, refresh_token: nil, client_secret: nil, expires_at: nil, scopes: nil, load_tokens: nil) CredentialValidator.validate_required!({client_id:, access_token:}, {refresh_token:, client_secret:, expires_at:, scopes:}) initialize_refresh(load_tokens) @client_id, @client_secret = SettingValidator.frozen(client_id), SettingValidator.frozen(client_secret) @access_token, @refresh_token = SettingValidator.frozen(access_token), SettingValidator.frozen(refresh_token) @expires_at, @scopes = expires_at, CredentialValidator.frozen_scopes(scopes) @connection, @token_url, @token_headers = Connection.new, TOKEN_URL, {} @clients = ObjectSpace::WeakMap.new end |
Instance Attribute Details
#client_id ⇒ String (readonly)
The OAuth 2.0 client ID
51 52 53 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 51 def client_id @client_id end |
#expires_at ⇒ Time? (readonly)
The expiration time of the access token
57 58 59 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 57 def expires_at @expires_at end |
#scopes ⇒ Array<String>? (readonly)
The scopes X granted the access token, as last refreshed
A refresh that names no scopes keeps those the authenticator held, as OAuth 2.0 has it.
66 67 68 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 66 def scopes @scopes end |
Instance Method Details
#headers(_request) ⇒ Hash{String => String}
Generate the authentication header, refreshing an expired token first
An authenticator that holds no refresh token sends an access token that expired as it is, for the API to reject.
121 122 123 124 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 121 def headers(_request) refresh_expired_token(connection) {AUTHENTICATION_HEADER => "Bearer #{access_token}"} end |
#inspect ⇒ String
Summarize the authenticator for the console without revealing credentials
132 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 132 def inspect = "#<#{self.class} client_id=#{client_id.inspect} expires_at=#{expires_at.inspect}>" |
#refresh! ⇒ OAuth2Tokens
Refresh the access token using the refresh token
The authenticator holds the new tokens once it returns, and the authenticator of a client has passed them to the save_tokens of the clients that share it. The tokens it returns are those of this refresh, frozen, the same object save_tokens is passed, so they are a set that belongs together, whatever refreshes follow on other threads.
A refresh reads the tokens in storage first, with load_tokens, and refreshes with the refresh token there when it is another. When X refuses the refresh for a refresh token another process spent, and the storage holds another, the tokens there are returned in place of an error, and are not passed to save_tokens.
A save_tokens that raises, as one whose storage is briefly down may, raises TokenReportFailed once each has been passed the tokens, which holds them, since the refresh token they replaced is spent and the authenticator holds them alone, with the error save_tokens raised as its cause.
170 171 172 173 174 175 176 177 178 179 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 170 def refresh! raise UnsupportedOperation, NO_REFRESH_TOKEN unless refresh_token tokens = @mutex.synchronize do adopt_stored_tokens refresh(connection, nil) end report_refresh(tokens, nil) tokens end |
#token_expired? ⇒ Boolean
Check if the access token has expired or will expire soon
140 141 142 143 144 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 140 def token_expired? return false if expires_at.nil? Time.now >= expires_at - EXPIRATION_BUFFER end |