Class: X::OAuth2Authorization
- Inherits:
-
Object
- Object
- X::OAuth2Authorization
- Includes:
- CredentialHolder
- Defined in:
- x-core/lib/x/core/oauth2_authorization.rb
Overview
Authorizes an app to act for a user with the OAuth 2.0 authorization code flow and PKCE
The flow takes two requests to the app: one that sends the user to X to authorize it, and one that X redirects the user back to. The state and code verifier of the first must reach the second, so store them, as in the session, and build the authorization again with them when X redirects back.
Constant Summary collapse
- AUTHORIZATION_URL =
The page that asks a user to authorize an app
"https://x.com/i/oauth2/authorize"- DEFAULT_SCOPES =
The scopes that read posts and users, and keep a refresh token to act for the user after the access token expires
%w[tweet.read users.read offline.access].freeze
Instance Attribute Summary collapse
-
#client_id ⇒ String
readonly
The OAuth 2.0 client ID of the app.
-
#code_verifier ⇒ String
readonly
The PKCE code verifier, to store until X redirects back.
-
#redirect_uri ⇒ String
readonly
The URL X redirects the user back to, as registered for the app.
-
#scopes ⇒ Array<String>
readonly
The scopes the app asks the user for.
-
#state ⇒ String
readonly
The value that ties the redirect back from X to this authorization.
Instance Method Summary collapse
-
#client(callback, **options) ⇒ Client
Exchange the code of the redirect back from X for a client.
-
#initialize(client_id:, redirect_uri:, client_secret: nil, scopes: DEFAULT_SCOPES, state: SecureRandom.urlsafe_base64(STATE_BYTES), code_verifier: SimpleOAuth::OAuth2::PKCE.generate.verifier, base_url: Client::DEFAULT_BASE_URL, proxy_url: nil, open_timeout: Client::DEFAULT_OPEN_TIMEOUT, read_timeout: Client::DEFAULT_READ_TIMEOUT, write_timeout: Client::DEFAULT_WRITE_TIMEOUT, keep_alive_timeout: Client::DEFAULT_KEEP_ALIVE_TIMEOUT, debug_output: nil, headers: {}) ⇒ OAuth2Authorization
constructor
Initialize an authorization.
-
#inspect ⇒ String
Summarize the authorization for the console without revealing its secrets.
-
#tokens(callback) ⇒ OAuth2Tokens
Exchange the code of the redirect back from X for the tokens of the user.
-
#url ⇒ String
The page on X that asks the user to authorize the app, to redirect the user to.
Constructor Details
#initialize(client_id:, redirect_uri:, client_secret: nil, scopes: DEFAULT_SCOPES, state: SecureRandom.urlsafe_base64(STATE_BYTES), code_verifier: SimpleOAuth::OAuth2::PKCE.generate.verifier, base_url: Client::DEFAULT_BASE_URL, proxy_url: nil, open_timeout: Client::DEFAULT_OPEN_TIMEOUT, read_timeout: Client::DEFAULT_READ_TIMEOUT, write_timeout: Client::DEFAULT_WRITE_TIMEOUT, keep_alive_timeout: Client::DEFAULT_KEEP_ALIVE_TIMEOUT, debug_output: nil, headers: {}) ⇒ OAuth2Authorization
Initialize an authorization
A new state and code verifier are generated unless they are given. The authorization code is exchanged for tokens at the origin of the base URL given, with the proxy, timeouts, keep-alive timeout, debug output, and headers given, which a client built with #client is given too.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 150 def initialize(client_id:, redirect_uri:, client_secret: nil, scopes: DEFAULT_SCOPES, state: SecureRandom.urlsafe_base64(STATE_BYTES), code_verifier: SimpleOAuth::OAuth2::PKCE.generate.verifier, base_url: Client::DEFAULT_BASE_URL, proxy_url: nil, open_timeout: Client::DEFAULT_OPEN_TIMEOUT, read_timeout: Client::DEFAULT_READ_TIMEOUT, write_timeout: Client::DEFAULT_WRITE_TIMEOUT, keep_alive_timeout: Client::DEFAULT_KEEP_ALIVE_TIMEOUT, debug_output: nil, headers: {}) validate!(client_id:, redirect_uri:, client_secret:, scopes:, state:) @client_id = client_id @client_secret = client_secret @redirect_uri = redirect_uri @scopes = CredentialValidator.frozen_scopes(scopes) @state = state @pkce = SimpleOAuth::OAuth2::PKCE.new(verifier: code_verifier) @code_verifier = code_verifier @settings = {base_url: SettingValidator.base_url!(base_url), proxy_url:, open_timeout:, read_timeout:, write_timeout:, keep_alive_timeout:, debug_output:, headers: SettingValidator.headers!(headers)} @connection = Connection.new(**@settings.except(:base_url, :headers)) end |
Instance Attribute Details
#client_id ⇒ String (readonly)
The OAuth 2.0 client ID of the app
71 72 73 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 71 def client_id @client_id end |
#code_verifier ⇒ String (readonly)
The PKCE code verifier, to store until X redirects back
103 104 105 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 103 def code_verifier @code_verifier end |
#redirect_uri ⇒ String (readonly)
The URL X redirects the user back to, as registered for the app
78 79 80 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 78 def redirect_uri @redirect_uri end |
#scopes ⇒ Array<String> (readonly)
The scopes the app asks the user for
They are a frozen copy of the ones given, so that changing the Array the authorization was given never changes what it asks for, or the scopes of the tokens it builds when X names none.
89 90 91 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 89 def scopes @scopes end |
#state ⇒ String (readonly)
The value that ties the redirect back from X to this authorization
96 97 98 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 96 def state @state end |
Instance Method Details
#client(callback, **options) ⇒ Client
Exchange the code of the redirect back from X for a client
The options are checked before the code is exchanged, since X accepts it once, so an option the client refuses, such as a misspelled keyword, raises before the code is spent rather than after, with the tokens it was exchanged for lost.
The save_tokens of the client is passed the OAuth2Tokens of the exchange before the client is returned, as it is passed those of each refresh after, so that a callable that stores them stores every refresh token X issues, the first among them; a refresh token held by the client alone would be lost with it, and the user would have to authorize the app again. Without offline.access, which issues no refresh token, it is passed tokens whose refresh token is nil, so it stores the access token that acts for the user until it expires. A callable that raises, as one whose storage is briefly down may, raises TokenReportFailed, which holds the client and the tokens, so neither is lost with the code.
The code is exchanged at the origin of the base URL of the client, the base_url of the options or else that of the authorization, as the client refreshes its tokens there, and through the proxy, with the timeouts, keep-alive timeout, debug output, and headers of the client, so a client given a proxy reaches X through it from the first request of its tokens.
246 247 248 249 250 251 252 253 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 246 def client(callback, **) # steep:ignore DifferentMethodParameterKind given = .keys & CREDENTIALS raise ArgumentError, format(CREDENTIALS_GIVEN_MESSAGE, given.join(", ")) unless given.empty? checked = Client.new(**@settings, **) # refuses an option before the code, which X accepts once, is spent tokens = tokens_from(exchange(callback, checked.base_url, checked.headers, connection_for())) client_of(tokens, ).tap { |client| report_exchange(client, tokens) } end |
#inspect ⇒ String
Summarize the authorization for the console without revealing its secrets
172 173 174 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 172 def inspect "#<#{self.class} client_id=#{client_id.inspect} redirect_uri=#{redirect_uri.inspect} scopes=#{scopes}>" end |
#tokens(callback) ⇒ OAuth2Tokens
Exchange the code of the redirect back from X for the tokens of the user
An authorization code works once, so call this, or #client, once for each redirect.
The tokens are the OAuth2Tokens a client passes save_tokens and reads from load_tokens, which are stored for each user, so they leave out the client ID, and the client secret of a confidential client, which are the app's and kept once, apart from them; pass them beside the tokens to a client built of them, which refreshes the access token with them.
209 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 209 def tokens(callback) = tokens_from(exchange(callback, base_url, @settings.fetch(:headers))) |
#url ⇒ String
The page on X that asks the user to authorize the app, to redirect the user to
182 183 184 |
# File 'x-core/lib/x/core/oauth2_authorization.rb', line 182 def url oauth2_client(base_url).(redirect_uri:, pkce: @pkce, state:, scope: scopes) end |