1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 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
|
require "oauth2"
require "omniauth"
require "securerandom"
require "socket" # for SocketError
require "timeout" # for Timeout::Error
module OmniAuth
module Strategies
# Authentication strategy for connecting with APIs constructed using
# the [OAuth 2.0 Specification](http://tools.ietf.org/html/draft-ietf-oauth-v2-10).
# You must generally register your application with the provider and
# utilize an application id and secret in order to authenticate using
# OAuth 2.0.
class OAuth2
include OmniAuth::Strategy
def self.inherited(subclass)
OmniAuth::Strategy.included(subclass)
end
args %i[client_id client_secret]
option :client_id, nil
option :client_secret, nil
option :client_options, {}
option :authorize_params, {}
option :authorize_options, %i[scope state]
option :token_params, {}
option :token_options, []
option :auth_token_params, {}
option :provider_ignores_state, false
option :pkce, false
option :pkce_verifier, nil
option :pkce_options, {
:code_challenge => proc { |verifier|
Base64.urlsafe_encode64(
Digest::SHA2.digest(verifier),
:padding => false,
)
},
:code_challenge_method => "S256",
}
attr_accessor :access_token
def client
::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
end
credentials do
hash = {"token" => access_token.token}
hash["refresh_token"] = access_token.refresh_token if access_token.expires? && access_token.refresh_token
hash["expires_at"] = access_token.expires_at if access_token.expires?
hash["expires"] = access_token.expires?
hash
end
def request_phase
redirect client.auth_code.authorize_url({:redirect_uri => callback_url}.merge(authorize_params))
end
def authorize_params # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
options.authorize_params[:state] = SecureRandom.hex(24)
if OmniAuth.config.test_mode
@env ||= {}
@env["rack.session"] ||= {}
end
params = options.authorize_params
.merge(options_for("authorize"))
.merge(pkce_authorize_params)
session["omniauth.pkce.verifier"] = options.pkce_verifier if options.pkce
session["omniauth.state"] = params[:state]
params
end
def token_params
options.token_params.merge(options_for("token")).merge(pkce_token_params)
end
def callback_phase # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
error = request.params["error_reason"] || request.params["error"]
if !options.provider_ignores_state && (request.params["state"].to_s.empty? || request.params["state"] != session.delete("omniauth.state"))
fail!(:csrf_detected, CallbackError.new(:csrf_detected, "CSRF detected"))
elsif error
fail!(error, CallbackError.new(request.params["error"], request.params["error_description"] || request.params["error_reason"], request.params["error_uri"]))
else
self.access_token = build_access_token
self.access_token = access_token.refresh! if access_token.expired?
super
end
rescue ::OAuth2::Error, CallbackError => e
fail!(:invalid_credentials, e)
rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
fail!(:timeout, e)
rescue ::SocketError => e
fail!(:failed_to_connect, e)
end
protected
def pkce_authorize_params
return {} unless options.pkce
options.pkce_verifier = SecureRandom.hex(64)
# NOTE: see https://tools.ietf.org/html/rfc7636#appendix-A
{
:code_challenge => options.pkce_options[:code_challenge]
.call(options.pkce_verifier),
:code_challenge_method => options.pkce_options[:code_challenge_method],
}
end
def pkce_token_params
return {} unless options.pkce
{:code_verifier => session.delete("omniauth.pkce.verifier")}
end
def build_access_token
verifier = request.params["code"]
client.auth_code.get_token(verifier, {:redirect_uri => callback_url}.merge(token_params.to_hash(:symbolize_keys => true)), deep_symbolize(options.auth_token_params))
end
def deep_symbolize(options)
options.each_with_object({}) do |(key, value), hash|
hash[key.to_sym] = value.is_a?(Hash) ? deep_symbolize(value) : value
end
end
def options_for(option)
hash = {}
options.send(:"#{option}_options").select { |key| options[key] }.each do |key|
hash[key.to_sym] = if options[key].respond_to?(:call)
options[key].call(env)
else
options[key]
end
end
hash
end
# An error that is indicated in the OAuth 2.0 callback.
# This could be a `redirect_uri_mismatch` or other
class CallbackError < StandardError
attr_accessor :error, :error_reason, :error_uri
def initialize(error, error_reason = nil, error_uri = nil)
self.error = error
self.error_reason = error_reason
self.error_uri = error_uri
end
def message
[error, error_reason, error_uri].compact.join(" | ")
end
end
end
end
end
OmniAuth.config.add_camelization "oauth2", "OAuth2"
|