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
97 98 99 100 101 102 103 104 105 106 107 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 97 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_id @client_secret = client_secret @access_token = access_token @refresh_token = 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
50 51 52 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 50 def client_id @client_id end |
#expires_at ⇒ Time? (readonly)
The expiration time of the access token
56 57 58 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 56 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.
65 66 67 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 65 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.
122 123 124 125 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 122 def headers(_request) refresh_expired_token(connection) {AUTHENTICATION_HEADER => "Bearer #{access_token}"} end |
#inspect ⇒ String
Summarize the authenticator for the console without revealing credentials
133 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 133 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.
171 172 173 174 175 176 177 178 179 180 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 171 def refresh! raise UnsupportedOperation, NO_REFRESH_TOKEN unless refresh_token tokens = @mutex.synchronize do adopt_stored_tokens refresh(connection, @token_headers) end report_refresh(tokens, nil) tokens end |
#token_expired? ⇒ Boolean
Check if the access token has expired or will expire soon
141 142 143 144 145 |
# File 'x-core/lib/x/core/oauth2_authenticator.rb', line 141 def token_expired? return false if expires_at.nil? Time.now >= expires_at - EXPIRATION_BUFFER end |