class Ably::Auth
Creates Ably
{Ably::Models::TokenRequest} objects and obtains Ably
Tokens from Ably
to subsequently issue to less trusted clients.
Constants
- API_KEY_REGEX
- AUTH_OPTIONS_KEYS
Supported AuthOption keys, see www.ably.com/docs/realtime/types#auth-options TODO: Review
client_id
usage embedded incorrectly within AuthOptions.This is legacy code to configure a client with a client_id from the ClientOptions
TODO: Review inclusion of use_token_auth, ttl,
token_params
in auth options- TOKEN_DEFAULTS
Default capability Hash object and TTL in seconds for issued tokens
Attributes
Public Class Methods
Creates an Auth
object
@param [Ably::Rest::Client] client {Ably::Rest::Client} this Auth
object uses @param [Hash] token_params
the token params used as a default for future token requests @param [Hash] auth_options
the authentication options used as a default future token requests @option (see request_token
)
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 53 def initialize(client, token_params, auth_options) unless auth_options.kind_of?(Hash) raise ArgumentError, 'Expected auth_options to be a Hash' end unless token_params.kind_of?(Hash) raise ArgumentError, 'Expected token_params to be a Hash' end # Ensure instance variables are defined @client_id = nil @client_id_validated = nil ensure_valid_auth_attributes auth_options @client = client @options = auth_options.dup @token_params = token_params.dup @token_option = options[:token] || options[:token_details] if options[:key] && (options[:key_secret] || options[:key_name]) raise ArgumentError, 'key and key_name or key_secret are mutually exclusive. Provider either a key or key_name & key_secret' end split_api_key_into_key_and_secret! options if options[:key] store_and_delete_basic_auth_key_from_options! options if using_basic_auth? && !api_key_present? raise ArgumentError, 'key is missing. Either an API key, token, or token auth method must be provided' end if options[:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id, only a token can have a wildcard client_id privilege' end if has_client_id? && !token_creatable_externally? && !token_option @client_id = ensure_utf_8(:client_id, client_id) if client_id end # If a token details object or token string is provided in the initializer # then the client can be authorized immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details begin token_details = authorize_with_token(token_details) logger.debug { "Auth: new token passed in to the initializer: #{token_details}" } rescue StandardError => e logger.error { "Auth: Implicit authorization using the provided token failed: #{e}" } end end end @options.freeze @token_params.freeze end
Public Instance Methods
Auth
header string used in HTTP requests to Ably
Will reauthorize implicitly if required and capable
@return [String] HTTP authentication value used in HTTP_AUTHORIZATION header
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 405 def auth_header if using_token_auth? token_auth_header else basic_auth_header end end
Auth
params used in URI endpoint for Realtime
connections Will reauthorize implicitly if required and capable
@return [Hash] Auth
params for a new Realtime
connection
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 428 def auth_params if using_token_auth? token_auth_params else basic_auth_params end end
Returns false when attempting to send an API Key over a non-secure connection Token auth must be used for non-secure connections
@return [Boolean]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 452 def authentication_security_requirements_met? client.use_tls? || using_token_auth? end
True if assumed_client_id is compatible with the client’s configured or Ably
assigned client_id
@return [Boolean] @api private
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 470 def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif !options[:client_id] || options[:client_id] == '*' true # client ID is unknown else options[:client_id] == assumed_client_id end end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 385 def client_id @client_id || options[:client_id] end
When a client has authenticated with Ably
and the client is either anonymous (cannot assume a client_id
) or has an assigned client_id
(implicit in all operations), then this client has a validated client_id
, even if that client_id
is nil
(anonymous)
Once validated by Ably
, the client library will enforce the use of the client_id
identity provided by Ably
, rejecting messages with an invalid client_id
immediately
@return [Boolean]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 397 def client_id_validated? !!@client_id_validated end
Configures the client ID for this client Typically this occurs following an Auth
or receiving a {Ably::Models::ProtocolMessage} with a client_id
in the {Ably::Models::ConnectionDetails}
@api private
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 484 def configure_client_id(new_client_id) # If new client ID from Ably is a wildcard, but preconfigured clientId is set, then keep the existing clientId if has_client_id? && new_client_id == '*' @client_id_validated = true return end # If client_id is defined and not a wildcard, prevent it changing, this is not supported if client_id && client_id != '*' && new_client_id != client_id raise Ably::Exceptions::IncompatibleClientId.new("Client ID is immutable once configured for a client. Client ID cannot be changed to '#{new_client_id}'") end @client_id_validated = true @client_id = new_client_id end
Creates and signs an {Ably::Models::TokenRequest} based on the specified (or if none specified, the client library stored) ‘token_params` and `auth_options`. Note this can only be used when the API key value is available locally. Otherwise, the {Ably::Models::TokenRequest} must be obtained from the key owner. Use this to generate an {Ably::Models::TokenRequest} in order to implement an Ably
Token request callback for use by other clients. Both `token_params` and `auth_options` are optional. When omitted or null, the default token parameters and authentication options for the client library are used, as specified in the `client_options` when the client library was instantiated, or later updated with an explicit authorize request. Values passed in are used instead of, rather than being merged with, the default values. To understand why an {Ably::Models::TokenRequest} may be issued to clients in favor of a token, see Token Authentication explained.
@spec RSA9
@param [Hash] token_params
the token params used in the token request @option token_params
[String] :client_id A client ID to associate with this token. The generated token may be used to authenticate as this client_id
@option token_params
[Integer] :ttl validity time in seconds for the requested {Ably::Models::TokenDetails}. Limits may apply, see {www.ably.com/docs/general/authentication} @option token_params
[Hash] :capability canonicalised representation of the resource paths and associated operations @option token_params
[Time] :timestamp the time of the request @option token_params
[String] :nonce an unquoted, unescaped random string of at least 16 characters
@param [Hash] auth_options
the authentication options for the token request @option auth_options
[String] :key API key comprising the key name and key secret in a single string @option auth_options
[String] :client_id client ID identifying this connection to other clients (will use client_id
specified when library was instanced if provided) @option auth_options
[Boolean] :query_time when true will query the {www.ably.com Ably} system for the current time instead of using the local time @option auth_options
[Hash] :token_params convenience to pass in token_params
within the auth_options
argument, especially useful when setting default token_params
in the client constructor
@return [Models::TokenRequest]
@example
client.auth.create_token_request({ ttl: 3600 }, { id: 'asd.asd' }) #<Ably::Models::TokenRequest:0x007fd5d919df78 # @hash={ # :id=>"asds.adsa", # :clientId=>nil, # :ttl=>3600000, # :timestamp=>1428973674000, # :capability=>"{\"*\":[\"*\"]}", # :nonce=>"95e543b88299f6bae83df9b12fbd1ecd", # :mac=>"881oZHeFo6oMim7....uE56a8gUxHw=" # } #>>
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 309 def create_token_request(token_params = {}, auth_options = {}) ensure_valid_auth_attributes auth_options auth_options = auth_options.dup token_params = (auth_options[:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! auth_options if auth_options[:key] request_key_name = auth_options.delete(:key_name) || key_name request_key_secret = auth_options.delete(:key_secret) || key_secret raise Ably::Exceptions::TokenRequestFailed, 'Key Name and Key Secret are required to generate a new token request' unless request_key_name && request_key_secret ensure_current_time_is_based_on_server_time if auth_options[:query_time] timestamp = token_params.delete(:timestamp) || current_time timestamp = Time.at(timestamp) if timestamp.kind_of?(Integer) token_request = { keyName: request_key_name, timestamp: (timestamp.to_f * 1000).round, nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_client_id = token_params[:client_id] || auth_options[:client_id] || client_id token_request[:clientId] = token_client_id if token_client_id if token_params[:ttl] token_ttl = [ token_params[:ttl], Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER + TOKEN_DEFAULTS.fetch(:renew_token_buffer) # never issue a token that will be immediately considered expired due to the buffer ].max token_request[:ttl] = (token_ttl * 1000).to_i end token_request[:capability] = token_params[:capability] if token_params[:capability] if token_request[:capability].is_a?(Hash) lexicographic_ordered_capabilities = Hash[ token_request[:capability].sort_by { |key, value| key }.map do |key, value| [key, value.sort] end ] token_request[:capability] = JSON.dump(lexicographic_ordered_capabilities) end token_request[:mac] = sign_params(token_request, request_key_secret) # Undocumented feature to request a persisted token token_request[:persisted] = token_params[:persisted] if token_params[:persisted] Models::TokenRequest.new(token_request) end
Extra headers that may be used during authentication
@return [Hash] headers
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 416 def extra_auth_headers if client_id && using_basic_auth? { 'X-Ably-ClientId' => Base64.urlsafe_encode64(client_id) } else {} end end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 362 def key "#{key_name}:#{key_secret}" if api_key_present? end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 366 def key_name @key_name end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 370 def key_secret @key_secret end
Calls the requestToken REST API endpoint to obtain an Ably
Token according to the specified ‘token_params` and `auth_options`. Both `token_params` and `auth_options` are optional. When omitted or null, the default token parameters and authentication options for the client library are used, as specified in the `client_options` when the client library was instantiated, or later updated with an explicit authorize request. Values passed in are used instead of, rather than being merged with, the default values. To understand why an Ably
{Ably::Models::TokenRequest} may be issued to clients in favor of a token, see Token Authentication explained.
@spec RSA8e
@param [Hash] auth_options
(see create_token_request
) @option auth_options
[String] :auth_url a URL to be used to GET or POST a set of token request params, to obtain a signed token request @option auth_options
[Hash] :auth_headers a set of application-specific headers to be added to any request made to the auth_url
@option auth_options
[Hash] :auth_params a set of application-specific query params to be added to any request made to the auth_url
@option auth_options
[Symbol] :auth_method (:get) HTTP method to use with auth_url
, must be either :get
or :post
@option auth_options
[Proc] :auth_callback when provided, the Proc will be called with the token params hash as the first argument, whenever a new token is required.
The Proc should return a token string, {Ably::Models::TokenDetails} or JSON equivalent, {Ably::Models::TokenRequest} or JSON equivalent
@param [Hash] token_params
(see create_token_request
) @option (see create_token_request
)
@return [Ably::Models::TokenDetails] A {Ably::Models::TokenDetails} object. RSA16
@example
# simple token request using basic auth client = Ably::Rest::Client.new(key: 'key.id:secret') token_details = client.auth.request_token # token request with token params client.auth.request_token ttl: 1.hour # token request using auth block token_details = client.auth.request_token {}, auth_callback: lambda do |token_params| # create token_request object token_request end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 229 def request_token(token_params = {}, auth_options = {}) ensure_valid_auth_attributes auth_options # Token param precedence (lowest to highest): # Auth default => client_id => auth_options[:token_params] arg => token_params arg token_params = self.token_params.merge( (client_id ? { client_id: client_id } : {}). merge(auth_options[:token_params] || {}). merge(token_params) ) auth_options = self.options.merge(auth_options) token_request = if auth_callback = auth_options.delete(:auth_callback) begin Timeout::timeout(client.auth_request_timeout) do auth_callback.call(token_params) end rescue StandardError => err raise Ably::Exceptions::AuthenticationFailed.new("auth_callback failed: #{err.message}", nil, nil, err, fallback_status: 500, fallback_code: Ably::Exceptions::Codes::CONNECTION_NOT_ESTABLISHED_NO_TRANSPORT_HANDLE) end elsif auth_url = auth_options.delete(:auth_url) begin Timeout::timeout(client.auth_request_timeout) do token_request_from_auth_url(auth_url, auth_options, token_params) end rescue StandardError => err raise Ably::Exceptions::AuthenticationFailed.new("auth_url failed: #{err.message}", nil, nil, err, fallback_status: 500, fallback_code: Ably::Exceptions::Codes::CONNECTION_NOT_ESTABLISHED_NO_TRANSPORT_HANDLE) end else create_token_request(token_params, auth_options) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end
True if token provided client_id
is compatible with the client’s configured client_id
, when applicable
@return [Boolean] @api private
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 460 def token_client_id_allowed?(token_client_id) return true if client_id.nil? # no explicit client_id specified for this client return true if client_id == '*' || token_client_id == '*' # wildcard supported always token_client_id == client_id end
True if prerequisites for creating a new token request are present
One of the following criterion must be met:
-
Valid API key and token option not provided as token options cannot be determined
-
Authentication callback for new token requests
-
Authentication URL for new token requests
@return [Boolean]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 444 def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) end
Private Instance Methods
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 775 def api_key_present? key_name && key_secret end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 763 def auth_callback_present? !!options[:auth_callback] end
Basic Auth
HTTP Authorization header value
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 588 def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 507 def client @client end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 622 def configure_current_token_details(token_details) @current_token_details = token_details end
Return a Hash of connection options to initiate the Faraday::Connection with
@return [Hash]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 732 def connection_options @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end
Returns a TokenDetails object if the provided token_details_obj argument is a TokenDetails object, Token String or TokenDetails JSON object. If the token_details_obj is not a Token or TokenDetails nil
is returned
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 707 def convert_to_token_details(token_details_obj) case token_details_obj when Ably::Models::TokenDetails return token_details_obj when Hash return Ably::Models::TokenDetails.new(token_details_obj) if IdiomaticRubyWrapper(token_details_obj).has_key?(:issued) when String return Ably::Models::TokenDetails.new(token: token_details_obj) end end
Returns the current device clock time unless the the server time has previously been requested with query_time: true and the @server_time_offset is configured
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 526 def current_time if @server_time_offset Time.now + @server_time_offset else Time.now end end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 583 def ensure_api_key_sent_over_secure_connection raise Ably::Exceptions::InsecureRequest, 'Cannot use Basic Auth over non-TLS connections' unless authentication_security_requirements_met? end
Get the difference in time between the server and the local clock and store this for future time requests
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 536 def ensure_current_time_is_based_on_server_time server_time = client.time @server_time_offset = server_time.to_f - Time.now.to_f end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 541 def ensure_valid_auth_attributes(attributes) (attributes.keys.map(&:to_s) - AUTH_OPTIONS_KEYS).tap do |unsupported_keys| raise ArgumentError, "The key(s) #{unsupported_keys.map { |k| ":#{k}" }.join(', ')} are not valid AuthOptions" unless unsupported_keys.empty? end if attributes[:timestamp] unless attributes[:timestamp].kind_of?(Time) || attributes[:timestamp].kind_of?(Numeric) raise ArgumentError, ':timestamp must be a Time or positive Integer value of seconds since epoch' end end if attributes[:ttl] unless attributes[:ttl].kind_of?(Numeric) && attributes[:ttl].to_f > 0 raise ArgumentError, ':ttl must be a positive Numeric value representing time to live in seconds' end end if attributes[:auth_headers] unless attributes[:auth_headers].kind_of?(Hash) raise ArgumentError, ':auth_headers must be a valid Hash' end end if attributes[:auth_params] unless attributes[:auth_params].kind_of?(Hash) raise ArgumentError, ':auth_params must be a valid Hash' end end if attributes[:auth_method] unless %(get post).include?(attributes[:auth_method].to_s) raise ArgumentError, ':auth_method must be either :get or :post' end end if attributes[:auth_callback] unless attributes[:auth_callback].respond_to?(:call) raise ArgumentError, ':auth_callback must be a Proc' end end end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 779 def logger client.logger end
Return a Faraday middleware stack to initiate the Faraday::Connection with
@see mislav.uniqpath.com/2011/07/faraday-advanced-http/
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 749 def middleware @middleware ||= Faraday::RackBuilder.new do |builder| setup_outgoing_middleware builder # Raise exceptions if response code is invalid builder.use Ably::Rest::Middleware::ExternalExceptions setup_incoming_middleware builder, logger # Set Faraday's HTTP adapter builder.adapter Faraday.default_adapter end end
@return [Ably::Models::TokenDetails]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 719 def send_token_request(token_request) token_request = Ably::Models::TokenRequest(token_request) response = client.post("/keys/#{token_request.key_name}/requestToken", token_request.attributes, send_auth_header: false, disable_automatic_reauthorize: true) Ably::Models::TokenDetails.new(response.body) end
Sign the request params using the secret
@return [Hash]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 649 def sign_params(params, secret) text = params.values_at( :keyName, :ttl, :capability, :clientId, :timestamp, :nonce ).map do |val| "#{val}\n" end.join('') encode64( OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, text) ) end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 593 def split_api_key_into_key_and_secret!(options) api_key_parts = options[:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts options[:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) options[:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) options.delete :key end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 603 def store_and_delete_basic_auth_key_from_options!(options) @key_name = options.delete(:key_name) @key_secret = options.delete(:key_secret) end
Token Auth
HTTP Authorization header value
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 627 def token_auth_header "Bearer #{encode64(token_auth_string)}" end
Returns the current token if it exists or authorizes and retrieves a token
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 609 def token_auth_string if !current_token_details && token_option logger.debug { "Auth: Token auth string missing, authorizing implicitly now" } # A TokenRequest was configured in the ClientOptions +:token field+ and no current token exists # Note: If a Token or TokenDetails is provided in the initializer, the token is stored in +current_token_details+ authorize_with_token send_token_request(token_option) current_token_details.token else # Authorize will use the current token if one exists and is not expired, otherwise a new token will be issued authorize_when_necessary.token end end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 771 def token_creatable_externally? auth_callback_present? || token_url_present? end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 511 def token_option @token_option end
Retrieve a token request from a specified URL, expects a JSON or text response
@return [Hash]
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 669 def token_request_from_auth_url(auth_url, auth_options, token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", connection_options) method = auth_options[:auth_method] || options[:auth_method] || :get params = (auth_options[:auth_params] || options[:auth_params] || {}).merge(token_params) response = connection.public_send(method) do |request| request.url uri.path request.headers = auth_options[:auth_headers] || {} if method.to_s.downcase == 'post' request.body = params else request.params = (Addressable::URI.parse(uri.to_s).query_values || {}).merge(params) end end if !response.body.kind_of?(Hash) && !response.headers['Content-Type'].to_s.match(%r{text/plain|application/jwt}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end
# File lib/submodules/ably-ruby/lib/ably/auth.rb, line 767 def token_url_present? !!options[:auth_url] end