diff --git a/docs/middleware/request/authentication.md b/docs/middleware/request/authentication.md index 70da6e317..867289dc2 100644 --- a/docs/middleware/request/authentication.md +++ b/docs/middleware/request/authentication.md @@ -10,7 +10,12 @@ top_link: ./list --- The `Faraday::Request::Authorization` middleware allows you to automatically add an `Authorization` header -to your requests. It also features a handy helper to manage Basic authentication. +to your requests. It also features 2 specialised sub-classes that provide useful extra features for Basic Authentication +and Token Authentication requests. + +### Any Authentication + +The generic `Authorization` middleware allows you to add any type of Authorization header. ```ruby Faraday.new(...) do |conn| @@ -22,18 +27,30 @@ end You can also provide a proc, which will be evaluated on each request: + ```ruby + Faraday.new(...) do |conn| + conn.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token } + end + ``` + +### Basic Authentication + +`BasicAuthentication` adds a 'Basic' type Authorization header to a Faraday request. + ```ruby Faraday.new(...) do |conn| - conn.request :authorization, 'Bearer', -> { MyAuthStorage.get_auth_token } + conn.request :basic_auth, 'username', 'password' end ``` -### Basic Authentication +### Token Authentication -The middleware will automatically Base64 encode your Basic username and password: +`TokenAuthentication` adds a 'Token' type Authorization header to a Faraday request. +You can optionally provide a hash of `options` that will be appended to the token. +This is not used anymore in modern web and have been replaced by Bearer tokens. ```ruby Faraday.new(...) do |conn| - conn.request :authorization, :basic, 'username', 'password' + conn.request :token_auth, 'authentication-token', **options end ``` diff --git a/lib/faraday/connection.rb b/lib/faraday/connection.rb index 1b035677b..6344f6a0c 100644 --- a/lib/faraday/connection.rb +++ b/lib/faraday/connection.rb @@ -283,6 +283,77 @@ def #{method}(url = nil, body = nil, headers = nil, &block) RUBY end + # Sets up the Authorization header with these credentials, encoded + # with base64. + # + # @param login [String] The authentication login. + # @param pass [String] The authentication password. + # + # @example + # + # conn.basic_auth 'Aladdin', 'open sesame' + # conn.headers['Authorization'] + # # => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" + # + # @return [void] + def basic_auth(login, pass) + warn <<~TEXT + WARNING: `Faraday::Connection#basic_auth` is deprecated; it will be removed in version 2.0. + While initializing your connection, use `#request(:basic_auth, ...)` instead. + See https://lostisland.github.io/faraday/middleware/authentication for more usage info. + TEXT + set_authorization_header(:basic_auth, login, pass) + end + + # Sets up the Authorization header with the given token. + # + # @param token [String] + # @param options [Hash] extra token options. + # + # @example + # + # conn.token_auth 'abcdef', foo: 'bar' + # conn.headers['Authorization'] + # # => "Token token=\"abcdef\", + # foo=\"bar\"" + # + # @return [void] + def token_auth(token, options = nil) + warn <<~TEXT + WARNING: `Faraday::Connection#token_auth` is deprecated; it will be removed in version 2.0. + While initializing your connection, use `#request(:token_auth, ...)` instead. + See https://lostisland.github.io/faraday/middleware/authentication for more usage info. + TEXT + set_authorization_header(:token_auth, token, options) + end + + # Sets up a custom Authorization header. + # + # @param type [String] authorization type + # @param token [String, Hash] token. A String value is taken literally, and + # a Hash is encoded into comma-separated key/value pairs. + # + # @example + # + # conn.authorization :Bearer, 'mF_9.B5f-4.1JqM' + # conn.headers['Authorization'] + # # => "Bearer mF_9.B5f-4.1JqM" + # + # conn.authorization :Token, token: 'abcdef', foo: 'bar' + # conn.headers['Authorization'] + # # => "Token token=\"abcdef\", + # foo=\"bar\"" + # + # @return [void] + def authorization(type, token) + warn <<~TEXT + WARNING: `Faraday::Connection#authorization` is deprecated; it will be removed in version 2.0. + While initializing your connection, use `#request(:authorization, ...)` instead. + See https://lostisland.github.io/faraday/middleware/authentication for more usage info. + TEXT + set_authorization_header(:authorization, type, token) + end + # Check if the adapter is parallel-capable. # # @yield if the adapter isn't parallel-capable, or if no adapter is set yet. @@ -508,6 +579,14 @@ def with_uri_credentials(uri) yield(Utils.unescape(uri.user), Utils.unescape(uri.password)) end + def set_authorization_header(header_type, *args) + header = Faraday::Request + .lookup_middleware(header_type) + .header(*args) + + headers[Faraday::Request::Authorization::KEY] = header + end + def proxy_from_env(url) return if Faraday.ignore_env_proxy diff --git a/lib/faraday/request/authorization.rb b/lib/faraday/request/authorization.rb index d6aeaf2f7..f0c6c4671 100644 --- a/lib/faraday/request/authorization.rb +++ b/lib/faraday/request/authorization.rb @@ -10,6 +10,35 @@ class Authorization < Faraday::Middleware KEY = 'Authorization' end + # @param type [String, Symbol] + # @param token [String, Symbol, Hash] + # @return [String] a header value + def self.header(type, token) + case token + when String, Symbol + "#{type} #{token}" + when Hash + build_hash(type.to_s, token) + else + raise ArgumentError, + "Can't build an Authorization #{type}" \ + "header from #{token.inspect}" + end + end + + # @param type [String] + # @param hash [Hash] + # @return [String] type followed by comma-separated key=value pairs + # @api private + def self.build_hash(type, hash) + comma = ', ' + values = [] + hash.each do |key, value| + values << "#{key}=#{value.to_s.inspect}" + end + "#{type} #{values * comma}" + end + # @param app [#call] # @param type [String, Symbol] Type of Authorization # @param params [Array] parameters to build the Authorization header. @@ -19,9 +48,16 @@ class Authorization < Faraday::Middleware def initialize(app, type, *params) @type = type @params = params + @header_value = self.class.header(type, params[0]) unless params[0].is_a? Proc super(app) end + # @param env [Faraday::Env] + # def call(env) + # env.request_headers[KEY] = @header_value unless env.request_headers[KEY] + # @app.call(env) + # end + # # @param env [Faraday::Env] def on_request(env) return if env.request_headers[KEY] @@ -35,6 +71,8 @@ def on_request(env) # @param params [Array] # @return [String] a header value def header_from(type, *params) + return @header_value if @header_value + if type.to_s.casecmp('basic').zero? && params.size == 2 basic_header_from(*params) elsif params.size != 1 diff --git a/lib/faraday/request/basic_authentication.rb b/lib/faraday/request/basic_authentication.rb new file mode 100644 index 000000000..61c9a5bc3 --- /dev/null +++ b/lib/faraday/request/basic_authentication.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'base64' + +module Faraday + class Request + # Authorization middleware for Basic Authentication. + class BasicAuthentication < load_middleware(:authorization) + # @param login [String] + # @param pass [String] + # + # @return [String] a Basic Authentication header line + def self.header(login, pass) + value = Base64.encode64([login, pass].join(':')) + value.delete!("\n") + super(:Basic, value) + end + end + end +end diff --git a/lib/faraday/request/token_authentication.rb b/lib/faraday/request/token_authentication.rb new file mode 100644 index 000000000..f28264b1c --- /dev/null +++ b/lib/faraday/request/token_authentication.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Faraday + class Request + # TokenAuthentication is a middleware that adds a 'Token' header to a + # Faraday request. + class TokenAuthentication < load_middleware(:authorization) + # Public + def self.header(token, options = nil) + options ||= {} + options[:token] = token + super(:Token, options) + end + + def initialize(app, token, options = nil) + super(app, token, options) + end + end + end +end diff --git a/spec/faraday/connection_spec.rb b/spec/faraday/connection_spec.rb index 4ff56335e..0a1298117 100644 --- a/spec/faraday/connection_spec.rb +++ b/spec/faraday/connection_spec.rb @@ -141,6 +141,28 @@ end end + describe 'basic_auth' do + subject { conn } + + context 'calling the #basic_auth method' do + before { subject.basic_auth 'Aladdin', 'open sesame' } + + it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } + end + + context 'adding basic auth info to url' do + let(:url) { 'http://Aladdin:open%20sesame@sushi.com/fish' } + + it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } + end + end + + describe '#token_auth' do + before { subject.token_auth('abcdef', nonce: 'abc') } + + it { expect(subject.headers['Authorization']).to eq('Token nonce="abc", token="abcdef"') } + end + describe '#build_exclusive_url' do context 'with relative path' do subject { conn.build_exclusive_url('sake.html') } @@ -583,6 +605,7 @@ context 'after manual changes' do before do + subject.basic_auth('', '') subject.headers['content-length'] = 12 subject.params['b'] = '2' subject.options[:open_timeout] = 10 diff --git a/spec/faraday/request/authorization_spec.rb b/spec/faraday/request/authorization_spec.rb index 234861d68..9bc23f03d 100644 --- a/spec/faraday/request/authorization_spec.rb +++ b/spec/faraday/request/authorization_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Faraday::Request::Authorization do let(:conn) do Faraday.new do |b| - b.request :authorization, auth_type, *auth_config + b.request auth_type, *auth_config b.adapter :test do |stub| stub.get('/auth-echo') do |env| [200, {}, env[:request_headers]['Authorization']] @@ -14,10 +14,10 @@ shared_examples 'does not interfere with existing authentication' do context 'and request already has an authentication header' do - let(:response) { conn.get('/auth-echo', nil, authorization: 'OAuth oauth_token') } + let(:response) { conn.get('/auth-echo', nil, authorization: 'Token token="bar"') } it 'does not interfere with existing authorization' do - expect(response.body).to eq('OAuth oauth_token') + expect(response.body).to eq('Token token="bar"') end end end @@ -25,7 +25,7 @@ let(:response) { conn.get('/auth-echo') } describe 'basic_auth' do - let(:auth_type) { :basic } + let(:auth_type) { :basic_auth } context 'when passed correct params' do let(:auth_config) { %w[aladdin opensesame] } @@ -44,29 +44,51 @@ end end + describe 'token_auth' do + let(:auth_type) { :token_auth } + + context 'when passed correct params' do + let(:auth_config) { 'quux' } + + it { expect(response.body).to eq('Token token="quux"') } + + include_examples 'does not interfere with existing authentication' + end + + context 'when other values are provided' do + let(:auth_config) { ['baz', { foo: 42 }] } + + it { expect(response.body).to match(/^Token /) } + it { expect(response.body).to match(/token="baz"/) } + it { expect(response.body).to match(/foo="42"/) } + + include_examples 'does not interfere with existing authentication' + end + end + describe 'authorization' do - let(:auth_type) { :Bearer } + let(:auth_type) { :authorization } - context 'when passed a string' do - let(:auth_config) { ['custom'] } + context 'when passed two strings' do + let(:auth_config) { ['custom', 'abc def'] } - it { expect(response.body).to eq('Bearer custom') } + it { expect(response.body).to eq('custom abc def') } include_examples 'does not interfere with existing authentication' end - context 'when passed a proc' do - let(:auth_config) { [-> { 'custom_from_proc' }] } + context 'when passed a string and a hash' do + let(:auth_config) { ['baz', { foo: 42 }] } - it { expect(response.body).to eq('Bearer custom_from_proc') } + it { expect(response.body).to eq('baz foo="42"') } include_examples 'does not interfere with existing authentication' end - context 'when passed too many arguments' do - let(:auth_config) { %w[baz foo] } + context 'when passed a string and a proc' do + let(:auth_config) { ['Bearer', -> { 'custom_from_proc' }] } - it { expect { response }.to raise_error(ArgumentError) } + it { expect(response.body).to eq('Bearer custom_from_proc') } include_examples 'does not interfere with existing authentication' end