Class: X::OAuth2Tokens

Inherits:
Object
  • Object
show all
Defined in:
x-core/lib/x/core/oauth2_tokens.rb

Overview

The OAuth 2.0 tokens one refresh issued, or the exchange of an authorization code, which save_tokens is passed to store

It is frozen, and taken while the refresh holds its lock, so it holds the tokens of the refresh it reports, whatever refreshes follow on other threads, where the authenticator holds the tokens of the latest one.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(access_token:, refresh_token: nil, expires_at: nil, scopes: nil) ⇒ OAuth2Tokens

Initialize the tokens of a refresh

Examples:

Build the tokens of a refresh

X::OAuth2Tokens.new(access_token: "token", refresh_token: "refresh", expires_at: Time.now + 7200,
  scopes: %w[tweet.read users.read offline.access])

Parameters:

  • access_token (String) —

    the access token

  • refresh_token (String, nil) (defaults to: nil) —

    the refresh token, or nil for tokens issued without one

  • expires_at (Time, nil) (defaults to: nil) —

    the expiration time of the access token

  • scopes (Array<String>, nil) (defaults to: nil) —

    the scopes X granted the access token, or nil when they are not known

Raises:

  • (ArgumentError) —

    if the access token is not a String, the refresh token neither a String nor nil, or a token is empty, or if the expiration time is neither a Time nor nil, as it is a String for tokens read back from JSON other than with from_json, or the scopes are neither an Array of Strings that each name a scope nor nil



147
148
149
150
151
152
153
154
155
156
157
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 147

def initialize(access_token:, refresh_token: nil, expires_at: nil, scopes: nil)
  raise ArgumentError, format(NOT_A_STRING, :access_token, access_token.class) if access_token.nil?
  raise ArgumentError, format(NOT_A_STRING_OR_NIL, :refresh_token, refresh_token.class) unless refresh_token.nil? || refresh_token.is_a?(String)

  CredentialValidator.validate_required!({access_token:}, {refresh_token:, expires_at:, scopes:})
  @access_token = access_token
  @refresh_token = refresh_token
  @expires_at = expires_at
  @scopes = CredentialValidator.frozen_scopes(scopes)
  freeze
end

Instance Attribute Details

#access_token ⇒ String (readonly)

The OAuth 2.0 access token the refresh issued

Examples:

Get the access token

tokens.access_token

Returns:

  • (String) —

    the access token



99
100
101
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 99

def access_token
  @access_token
end

#expires_at ⇒ Time? (readonly)

The time the access token expires, or nil when the refresh reported no lifetime

Examples:

Get the expiration time

tokens.expires_at

Returns:

  • (Time, nil) —

    the expiration time



118
119
120
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 118

def expires_at
  @expires_at
end

#refresh_token ⇒ String? (readonly)

The OAuth 2.0 refresh token the refresh issued

It is the refresh token the refresh was sent with when X issued none. An authorization without the offline.access scope issues none, so the tokens of its exchange hold nil, and the access token acts for the user until it expires, with nothing to refresh it.

Examples:

Get the refresh token

tokens.refresh_token

Returns:

  • (String, nil) —

    the refresh token, or nil for tokens issued without one



111
112
113
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 111

def refresh_token
  @refresh_token
end

#scopes ⇒ Array<String>? (readonly)

The scopes X granted the access token

They are the scopes the user authorized, which may be fewer than the app asked for, since a user can leave some out on the consent screen, so an app that needs a scope checks for it here rather than learn of it from the 403 Forbidden of a request. A refresh that names no scopes keeps those it refreshed, as OAuth 2.0 has it.

Examples:

Check that the user let the app post

tokens.scopes.include?("tweet.write")

Returns:

  • (Array<String>, nil) —

    the scopes, frozen, or nil when X named none and none were known



130
131
132
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 130

def scopes
  @scopes
end

Class Method Details

.from_json(json) ⇒ OAuth2Tokens

Read tokens back from the JSON that to_json wrote, or the Hash that as_json gave

A store that holds JSON, such as Redis or a JSON column, holds the tokens as as_json gives them: the number of their format, each token, and the expiration time as an ISO 8601 String, which this reads back as a Time. The Hash may be keyed by Symbol, as JSON.parse(json, symbolize_names: true) and many caches give it back.

Examples:

Read tokens stored as JSON

X::OAuth2Tokens.from_json(redis.get("tokens"))

Parameters:

  • json (String, Hash{String, Symbol => Object}) —

    the JSON to_json wrote, or the Hash as_json gave

Returns:

Raises:

  • (JSON::ParserError) —

    if the String is not JSON

  • (ArgumentError) —

    if the JSON is not an object, its expiration time is not an ISO 8601 String or nil, or its tokens or scopes are not those the constructor takes

  • (UnsupportedMarshalFormat) —

    if the JSON is of a format this release does not read



56
57
58
59
60
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 56

def self.from_json(json)
  state = json_state(json)
  new(access_token: state["access_token"], refresh_token: state["refresh_token"],
    expires_at: json_time(state["expires_at"]), scopes: state["scopes"])
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Check whether other tokens are the same tokens

Examples:

Compare tokens

tokens == other

Parameters:

  • other (Object) —

    the other tokens

Returns:

  • (Boolean) —

    true if the other tokens are of the same class, with the same values



178
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 178

def ==(other) = other.instance_of?(self.class) && to_h.eql?(other.to_h)

#as_json ⇒ Hash{String => Integer, String, Array<String>, nil}

The tokens as JSON writes them, led by the number of their format

JSON holds no Time, so the expiration time is written as an ISO 8601 String in UTC, to the nanosecond, which from_json reads back as a Time equal to it. It holds the tokens themselves, since tokens are written as JSON to be stored, so what JSON wrote is kept as secret as the tokens are.

Examples:

Store the tokens of a refresh in Redis

X::Client.new(**credentials, save_tokens: ->(tokens) { redis.set("tokens", tokens.to_json) })

Returns:

  • (Hash{String => Integer, String, Array<String>, nil}) —

    the number of the format, the access token, refresh token, expiration time, and scopes



200
201
202
203
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 200

def as_json(*)
  {"format" => MARSHAL_FORMAT, "access_token" => access_token, "refresh_token" => refresh_token,
   "expires_at" => expires_at&.getutc&.iso8601(FRACTION_DIGITS), "scopes" => scopes}
end

#encode_with(coder) ⇒ void

This method returns an undefined value.

Write the state Marshal writes as YAML

YAML would write the instance variables of the tokens, and read them back into tokens that are not frozen, so they say how they are written: the number of their format, then each of them, under the name to_h gives it.

Examples:

Write tokens as YAML

YAML.dump(tokens)

Parameters:

  • coder (Psych::Coder) —

    the coder YAML writes the tokens with



260
261
262
263
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 260

def encode_with(coder)
  coder["format"] = MARSHAL_FORMAT
  to_h.each { |key, value| coder[key.to_s] = value }
end

#hash ⇒ Integer

The hash of the tokens, for use as a Hash key

Examples:

Get the hash

tokens.hash

Returns:

  • (Integer) —

    the hash



187
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 187

def hash = [self.class, to_h].hash

#init_with(coder) ⇒ void

This method returns an undefined value.

Restore tokens YAML read, frozen, as Marshal restores them

Examples:

Read tokens written as YAML

YAML.unsafe_load(File.read("tokens.yml")).expires_at

Parameters:

  • coder (Psych::Coder) —

    the coder YAML read the tokens with

Raises:



273
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 273

def init_with(coder) = marshal_load([coder["format"], coder.map.transform_keys(&:to_sym)])

#inspect ⇒ String

Summarize the tokens for the console without revealing them

Examples:

Inspect tokens

tokens.inspect # => #<X::OAuth2Tokens expires_at=nil>

Returns:

  • (String) —

    the class name and expiration time



220
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 220

def inspect = "#<#{self.class} expires_at=#{expires_at.inspect}>"

#marshal_dump ⇒ Array(Integer, Hash{Symbol => String, Time, Array<String>, nil})

The state Marshal writes

What is written is plain data, led by the number of its format, so that tokens written by one release of 1.x are read by a later one: the tokens, their expiration time, and their scopes, as to_h gives them. It holds the tokens themselves, since tokens are marshalled to be stored, so what Marshal wrote is kept as secret as the tokens are.

Examples:

Store the tokens of a refresh

X::Client.new(**credentials, save_tokens: ->(tokens) { File.binwrite("tokens", Marshal.dump(tokens)) })

Returns:

  • (Array(Integer, Hash{Symbol => String, Time, Array<String>, nil})) —

    the number of the format, then the tokens as a Hash



233
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 233

def marshal_dump = [MARSHAL_FORMAT, to_h]

#marshal_load(state) ⇒ void

This method returns an undefined value.

Restore tokens Marshal read, built as the constructor builds them, frozen

Examples:

Read stored tokens

Marshal.load(File.binread("tokens")).expires_at

Parameters:

  • state (Array) —

    the state Marshal wrote

Raises:



243
244
245
246
247
248
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 243

def marshal_load(state)
  format, tokens = state #: [Integer, {access_token: String, refresh_token: String?, expires_at: Time?, scopes: Array[String]?}]
  raise UnsupportedMarshalFormat, "#{self.class} reads format #{MARSHAL_FORMAT} of Marshal, not #{format.inspect}" unless MARSHAL_FORMAT.eql?(format)

  initialize(**tokens.slice(:access_token, :refresh_token, :expires_at, :scopes)) # steep:ignore InsufficientKeywordArguments
end

#to_h ⇒ Hash{Symbol => String, Time, Array<String>, nil}

The tokens as a Hash, to store, and to build a client of

Its keys are keywords X::Client.new and X::OAuth2Authenticator.new take, so that the tokens build a client, as in X::Client.new(client_id:, **tokens.to_h), and a later release of 1.x adds a key to it only as both take it.

Examples:

Store the tokens

store.save(**tokens.to_h)

Returns:

  • (Hash{Symbol => String, Time, Array<String>, nil}) —

    the access token, refresh token, expiration time, and scopes



169
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 169

def to_h = {access_token:, refresh_token:, expires_at:, scopes:}

#to_json(state = nil) ⇒ String

The tokens as JSON, which from_json reads back

Examples:

Store the tokens of a refresh in Redis

X::Client.new(**credentials, save_tokens: ->(tokens) { redis.set("tokens", tokens.to_json) })

Parameters:

  • state (JSON::State, nil) (defaults to: nil) —

    the state a JSON encoder passes, which the Hash of as_json is given

Returns:

  • (String) —

    the tokens as a JSON object



212
# File 'x-core/lib/x/core/oauth2_tokens.rb', line 212

def to_json(state = nil) = as_json.to_json(state)