Class: Ably::Auth
- Inherits:
-
Object
- Object
- Ably::Auth
- Includes:
- Modules::Conversions, Modules::HttpHelpers
- Defined in:
- lib/ably/auth.rb
Overview
Auth is responsible for authentication with Ably using basic or token authentication
Find out more about Ably authentication at: www.ably.io/documentation/general/authentication/
Constant Summary collapse
- TOKEN_DEFAULTS =
Default capability Hash object and TTL in seconds for issued tokens
{ capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze
Instance Attribute Summary collapse
-
#client_id ⇒ String
readonly
The provided client ID, used for identifying this client for presence purposes.
-
#current_token_details ⇒ Ably::Models::TokenDetails
readonly
Current Models::TokenDetails issued by this library or one of the provided callbacks used to authenticate requests.
-
#key ⇒ String
readonly
Complete API key containing both the key name and key secret, if present.
-
#key_name ⇒ String
readonly
Key name (public part of the API key), if present.
-
#key_secret ⇒ String
readonly
Key secret (private secure part of the API key), if present.
-
#options ⇒ Hash
(also: #auth_options)
readonly
Default Auth options configured for this client.
-
#token_params ⇒ Hash
readonly
Default token params used for token requests, see #request_token.
Instance Method Summary collapse
-
#auth_header ⇒ String
Auth header string used in HTTP requests to Ably Will reauthorise implicitly if required and capable.
-
#auth_params ⇒ Hash
Auth params used in URI endpoint for Realtime connections Will reauthorise implicitly if required and capable.
-
#authentication_security_requirements_met? ⇒ Boolean
Returns false when attempting to send an API Key over a non-secure connection Token auth must be used for non-secure connections.
-
#authorise(token_params = {}, auth_options = {}) ⇒ Models::TokenRequest
Ensures valid auth credentials are present for the library instance.
-
#can_assume_client_id?(assumed_client_id) ⇒ Boolean
private
True if assumed_client_id is compatible with the client’s configured or Ably assigned
client_id
. -
#client_id_validated? ⇒ Boolean
When a client has authenticated with Ably and the client is either anonymous (cannot assume a
client_id
) or has an assignedclient_id
(implicit in all operations), then this client has a validatedclient_id
, even if that client_id isnil
(anonymous). -
#configure_client_id(new_client_id) ⇒ Object
private
Configures the client ID for this client Typically this occurs following an Auth or receiving a Models::ProtocolMessage with a
client_id
in the Models::ConnectionDetails. -
#create_token_request(token_params = {}, auth_options = {}) ⇒ Models::TokenRequest
Creates and signs a token request that can then subsequently be used by any client to request a token.
-
#has_client_id? ⇒ Boolean
private
True when a client_id other than a wildcard is configured for Auth.
-
#initialize(client, token_params, auth_options) ⇒ Auth
constructor
Creates an Auth object.
-
#request_token(token_params = {}, auth_options = {}) ⇒ Ably::Models::TokenDetails
Request a Models::TokenDetails which can be used to make authenticated token based requests.
-
#token_client_id_allowed?(token_client_id) ⇒ Boolean
private
True if token provided client_id is compatible with the client’s configured
client_id
, when applicable. -
#token_renewable? ⇒ Boolean
True if prerequisites for creating a new token request are present.
-
#using_basic_auth? ⇒ Boolean
True when Basic Auth is being used to authenticate with Ably.
-
#using_token_auth? ⇒ Boolean
True when Token Auth is being used to authenticate with Ably.
Constructor Details
#initialize(client, token_params, auth_options) ⇒ Auth
Creates an Auth object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/ably/auth.rb', line 48 def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end |
Instance Attribute Details
#client_id ⇒ String (readonly)
Returns The provided client ID, used for identifying this client for presence purposes.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
#current_token_details ⇒ Ably::Models::TokenDetails (readonly)
Returns Current Models::TokenDetails issued by this library or one of the provided callbacks used to authenticate requests.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
#key ⇒ String (readonly)
Returns Complete API key containing both the key name and key secret, if present.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
#key_name ⇒ String (readonly)
Returns Key name (public part of the API key), if present.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
#key_secret ⇒ String (readonly)
Returns Key secret (private secure part of the API key), if present.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
#options ⇒ Hash (readonly) Also known as: auth_options
Returns Default Ably::Auth options configured for this client.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
#token_params ⇒ Hash (readonly)
Returns Default token params used for token requests, see #request_token.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 |
# File 'lib/ably/auth.rb', line 27 class Auth include Ably::Modules::Conversions include Ably::Modules::HttpHelpers # Default capability Hash object and TTL in seconds for issued tokens TOKEN_DEFAULTS = { capability: { '*' => ['*'] }, ttl: 60 * 60, # 1 hour in seconds renew_token_buffer: 10 # buffer to allow a token to be reissued before the token is considered expired (Ably::Models::TokenDetails::TOKEN_EXPIRY_BUFFER) }.freeze attr_reader :options, :token_params, :current_token_details alias_method :auth_options, :options # 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) # def initialize(client, token_params, ) unless .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 @client = client @options = .dup @token_params = token_params.dup @token_option = [:token] || [:token_details] @options.delete :force # Forcing token auth for every request is not a valid default if [:key] && ([:key_secret] || [: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! if [:key] 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 [:client_id] == '*' raise ArgumentError, 'A client cannot be configured with a wildcard client_id' end if has_client_id? && !token_creatable_externally? && !token_option raise ArgumentError, 'client_id cannot be provided without a complete API key or means to authenticate. An API key is needed to automatically authenticate with Ably and obtain a token' unless api_key_present? ensure_utf_8 :client_id, client_id end # If a token details object or token string is provided in the initializer # then the client can be authorised immediately using this token if token_option token_details = convert_to_token_details(token_option) if token_details token_details = (token_details) logger.debug "Auth: new token passed in to the initializer: #{token_details}" end end @options.freeze @token_params.freeze end # Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary. # # In the event that a new token request is made, the provided options are used. # # @param [Hash] token_params the token params used for future token requests # @param [Hash] auth_options the authentication options used for future token requests # @option auth_options [Boolean] :force obtains a new token even if the current token is valid # @option (see #request_token) # # @return (see #create_token_request) # # @example # # will issue a simple token request using basic auth # client = Ably::Rest::Client.new(key: 'key.id:secret') # token_details = client.auth.authorise # # # will use token request from block to authorise if not already authorised # token_details = client.auth.authorise {}, auth_callback: Proc.new do # # create token_request object # token_request # end # def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end # Request a {Ably::Models::TokenDetails} which can be used to make authenticated token based requests # # @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] # # @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: Proc.new do # # create token_request object # token_request # end # def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end # Creates and signs a token request that can then subsequently be used by any client to request a token # # @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 {https://www.ably.io/documentation/other/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 {https://www.ably.io 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=" # # } # #>> def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 def key "#{key_name}:#{key_secret}" if api_key_present? end def key_name [:key_name] end def key_secret [:key_secret] end # True when Basic Auth is being used to authenticate with Ably def using_basic_auth? !using_token_auth? end # True when Token Auth is being used to authenticate with Ably def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end def client_id @client_id || [: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] def client_id_validated? !!@client_id_validated end # Auth header string used in HTTP requests to Ably # Will reauthorise implicitly if required and capable # # @return [String] HTTP authentication value used in HTTP_AUTHORIZATION header 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 reauthorise implicitly if required and capable # # @return [Hash] Auth params for a new Realtime connection def auth_params if using_token_auth? token_auth_params else basic_auth_params end 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] def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) 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] def authentication_security_requirements_met? client.use_tls? || using_token_auth? end # True if token provided client_id is compatible with the client's configured +client_id+, when applicable # # @return [Boolean] # @api private 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 assumed_client_id is compatible with the client's configured or Ably assigned +client_id+ # # @return [Boolean] # @api private def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end 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 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end # True when a client_id other than a wildcard is configured for Auth # # @api private def has_client_id? client_id && (client_id != '*') end private def client @client end def token_option @token_option end def ensure_valid_auth_attributes(attributes) 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 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 # Basic Auth HTTP Authorization header value def basic_auth_header ensure_api_key_sent_over_secure_connection "Basic #{encode64("#{key}")}" end def split_api_key_into_key_and_secret!() api_key_parts = [:key].to_s.match(/(?<name>[\w-]+\.[\w-]+):(?<secret>[\w-]+)/) raise ArgumentError, 'key is invalid' unless api_key_parts [:key_name] = api_key_parts[:name].encode(Encoding::UTF_8) [:key_secret] = api_key_parts[:secret].encode(Encoding::UTF_8) .delete :key end # Returns the current token if it exists or authorises and retrieves a token def token_auth_string if !current_token_details && token_option # 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+ send_token_request(token_option) current_token_details.token else # Authorise will use the current token if one exists and is not expired, otherwise a new token will be issued .token end end def configure_current_token_details(token_details) @current_token_details = token_details end # Token Auth HTTP Authorization header value def token_auth_header "Bearer #{encode64(token_auth_string)}" end # Basic Auth params to authenticate the Realtime connection def basic_auth_params ensure_api_key_sent_over_secure_connection { key: key } end # Token Auth params to authenticate the Realtime connection def token_auth_params { access_token: token_auth_string } end # Sign the request params using the secret # # @return [Hash] 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 # Retrieve a token request from a specified URL, expects a JSON response # # @return [Hash] def token_request_from_auth_url(auth_url, , token_params) uri = URI.parse(auth_url) connection = Faraday.new("#{uri.scheme}://#{uri.host}", ) method = [:auth_method] || [:auth_method] || :get params = ([:auth_params] || [:auth_method] || {}).merge(token_params) response = connection.send(method) do |request| request.url uri.path request.headers = [: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}i) raise Ably::Exceptions::InvalidResponseBody, "Content Type #{response.headers['Content-Type']} is not supported by this client library" end response.body end # Use the provided token to authenticate immediately and store the token details in +current_token_details+ def (new_token_details) if new_token_details && !new_token_details.from_token_string? if !token_client_id_allowed?(new_token_details.client_id) raise Ably::Exceptions::IncompatibleClientId.new("Client ID '#{new_token_details.client_id}' in the token is incompatible with the current client ID '#{client_id}'", 400, 40012) end configure_client_id new_token_details.client_id end configure_current_token_details new_token_details 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 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 # @return [Ably::Models::TokenDetails] 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_reauthorise: true) Ably::Models::TokenDetails.new(response.body) end # Return a Hash of connection options to initiate the Faraday::Connection with # # @return [Hash] def @connection_options ||= { builder: middleware, headers: { accept: client.mime_type, user_agent: user_agent }, request: { open_timeout: 5, timeout: 10 } } end # Return a Faraday middleware stack to initiate the Faraday::Connection with # # @see http://mislav.uniqpath.com/2011/07/faraday-advanced-http/ 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 def auth_callback_present? !![:auth_callback] end def token_url_present? !![:auth_url] end def token_creatable_externally? auth_callback_present? || token_url_present? end def api_key_present? key_name && key_secret end def logger client.logger end end |
Instance Method Details
#auth_header ⇒ String
Auth header string used in HTTP requests to Ably Will reauthorise implicitly if required and capable
317 318 319 320 321 322 323 |
# File 'lib/ably/auth.rb', line 317 def auth_header if using_token_auth? token_auth_header else basic_auth_header end end |
#auth_params ⇒ Hash
Auth params used in URI endpoint for Realtime connections Will reauthorise implicitly if required and capable
329 330 331 332 333 334 335 |
# File 'lib/ably/auth.rb', line 329 def auth_params if using_token_auth? token_auth_params else basic_auth_params end end |
#authentication_security_requirements_met? ⇒ Boolean
Returns false when attempting to send an API Key over a non-secure connection Token auth must be used for non-secure connections
353 354 355 |
# File 'lib/ably/auth.rb', line 353 def authentication_security_requirements_met? client.use_tls? || using_token_auth? end |
#authorise(token_params = {}, auth_options = {}) ⇒ Models::TokenRequest
Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary.
In the event that a new token request is made, the provided options are used.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/ably/auth.rb', line 125 def (token_params = {}, = {}) ensure_valid_auth_attributes = .clone if current_token_details && !.delete(:force) return current_token_details unless current_token_details.expired? end split_api_key_into_key_and_secret! if [:key] @options = @options.merge() # update defaults token_params = (.delete(:token_params) || {}).merge(token_params) @token_params = @token_params.merge(token_params) # update defaults (request_token(token_params, )).tap do |new_token_details| logger.debug "Auth: new token following authorisation: #{new_token_details}" end end |
#can_assume_client_id?(assumed_client_id) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
True if assumed_client_id is compatible with the client’s configured or Ably assigned client_id
371 372 373 374 375 376 377 378 379 |
# File 'lib/ably/auth.rb', line 371 def can_assume_client_id?(assumed_client_id) if client_id_validated? client_id == '*' || (client_id == assumed_client_id) elsif ![:client_id] || [:client_id] == '*' true # client ID is unknown else [:client_id] == assumed_client_id end end |
#client_id_validated? ⇒ Boolean
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
309 310 311 |
# File 'lib/ably/auth.rb', line 309 def client_id_validated? !!@client_id_validated end |
#configure_client_id(new_client_id) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Configures the client ID for this client Typically this occurs following an Auth or receiving a Models::ProtocolMessage with a client_id
in the Models::ConnectionDetails
385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
# File 'lib/ably/auth.rb', line 385 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}'", 400, 40012) end @client_id_validated = true @client_id = new_client_id end |
#create_token_request(token_params = {}, auth_options = {}) ⇒ Models::TokenRequest
Creates and signs a token request that can then subsequently be used by any client to request a token
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/ably/auth.rb', line 231 def create_token_request(token_params = {}, = {}) ensure_valid_auth_attributes = .clone token_params = ([:token_params] || {}).merge(token_params) split_api_key_into_key_and_secret! if [:key] request_key_name = .delete(:key_name) || key_name request_key_secret = .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 = if [:query_time] client.time else token_params.delete(:timestamp) || Time.now end = Time.at() if .kind_of?(Integer) ttl = [ (token_params[:ttl] || TOKEN_DEFAULTS.fetch(: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 = { keyName: request_key_name, clientId: token_params[:client_id] || [:client_id] || client_id, ttl: (ttl * 1000).to_i, timestamp: (.to_f * 1000).round, capability: token_params[:capability] || TOKEN_DEFAULTS.fetch(:capability), nonce: token_params[:nonce] || SecureRandom.hex.force_encoding('UTF-8') } token_request[:capability] = JSON.dump(token_request[:capability]) if token_request[:capability].is_a?(Hash) 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 |
#has_client_id? ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
True when a client_id other than a wildcard is configured for Auth
403 404 405 |
# File 'lib/ably/auth.rb', line 403 def has_client_id? client_id && (client_id != '*') end |
#request_token(token_params = {}, auth_options = {}) ⇒ Ably::Models::TokenDetails
Request a Models::TokenDetails which can be used to make authenticated token based requests
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/ably/auth.rb', line 173 def request_token(token_params = {}, = {}) ensure_valid_auth_attributes # 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([:token_params] || {}). merge(token_params) ) = self..merge() token_request = if auth_callback = .delete(:auth_callback) auth_callback.call(token_params) elsif auth_url = .delete(:auth_url) token_request_from_auth_url(auth_url, , token_params) else create_token_request(token_params, ) end convert_to_token_details(token_request).tap do |token_details| return token_details if token_details end send_token_request(token_request) end |
#token_client_id_allowed?(token_client_id) ⇒ Boolean
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
True if token provided client_id is compatible with the client’s configured client_id
, when applicable
361 362 363 364 365 |
# File 'lib/ably/auth.rb', line 361 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 |
#token_renewable? ⇒ Boolean
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
345 346 347 |
# File 'lib/ably/auth.rb', line 345 def token_renewable? token_creatable_externally? || (api_key_present? && !token_option) end |
#using_basic_auth? ⇒ Boolean
True when Basic Auth is being used to authenticate with Ably
287 288 289 |
# File 'lib/ably/auth.rb', line 287 def using_basic_auth? !using_token_auth? end |
#using_token_auth? ⇒ Boolean
True when Token Auth is being used to authenticate with Ably
292 293 294 295 |
# File 'lib/ably/auth.rb', line 292 def using_token_auth? return [:use_token_auth] if .has_key?(:use_token_auth) !!(token_option || current_token_details || has_client_id? || token_creatable_externally?) end |