class OmniAuth::Strategies::OpenIDConnect

Constants

RESPONSE_TYPE_EXCEPTIONS

Public Instance Methods

authorization_code() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 150
def authorization_code
  params['code']
end
authorize_uri() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 162
def authorize_uri
  client.redirect_uri = redirect_uri
  opts = {
    response_type: options.response_type,
    response_mode: options.response_mode,
    scope: options.scope,
    state: new_state,
    login_hint: params['login_hint'],
    ui_locales: params['ui_locales'],
    claims_locales: params['claims_locales'],
    prompt: options.prompt,
    nonce: (new_nonce if options.send_nonce),
    hd: options.hd,
    acr_values: options.acr_values,
  }

  opts.merge!(options.extra_authorize_params) unless options.extra_authorize_params.empty?

  client.authorization_uri(opts.reject { |_k, v| v.nil? })
end
callback_phase() click to toggle source
Calls superclass method
# File lib/omniauth/strategies/openid_connect.rb, line 110
def callback_phase
  error = params['error_reason'] || params['error']
  error_description = params['error_description'] || params['error_reason']
  invalid_state = params['state'].to_s.empty? || params['state'] != stored_state

  raise CallbackError, error: params['error'], reason: error_description, uri: params['error_uri'] if error
  raise CallbackError, error: :csrf_detected, reason: "Invalid 'state' parameter" if invalid_state

  return unless valid_response_type?

  options.issuer = issuer if options.issuer.nil? || options.issuer.empty?

  verify_id_token!(params['id_token']) if configured_response_type == 'id_token'
  discover!
  client.redirect_uri = redirect_uri

  return id_token_callback_phase if configured_response_type == 'id_token'

  client.authorization_code = authorization_code
  access_token
  super
rescue CallbackError => e
  fail!(e.error, e)
rescue ::Rack::OAuth2::Client::Error => e
  fail!(e.response[:error], e)
rescue ::Timeout::Error, ::Errno::ETIMEDOUT => e
  fail!(:timeout, e)
rescue ::SocketError => e
  fail!(:failed_to_connect, e)
end
client() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 96
def client
  @client ||= ::OpenIDConnect::Client.new(client_options)
end
config() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 100
def config
  @config ||= ::OpenIDConnect::Discovery::Provider::Config.discover!(options.issuer)
end
end_session_uri() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 154
def end_session_uri
  return unless end_session_endpoint_is_valid?

  end_session_uri = URI(client_options.end_session_endpoint)
  end_session_uri.query = encoded_post_logout_redirect_uri
  end_session_uri.to_s
end
other_phase() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 141
def other_phase
  if logout_path_pattern.match?(current_path)
    options.issuer = issuer if options.issuer.to_s.empty?
    discover!
    return redirect(end_session_uri) if end_session_uri
  end
  call_app!
end
public_key() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 183
def public_key
  @public_key ||= begin
    if options.discovery
      config.jwks
    elsif configured_public_key
      configured_public_key
    elsif client_options.jwks_uri
      fetch_key
    end
  end
end
request_phase() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 104
def request_phase
  options.issuer = issuer if options.issuer.to_s.empty?
  discover!
  redirect authorize_uri
end
secret() click to toggle source

Some OpenID providers use the OAuth2 client secret as the shared secret, but Keycloak uses a separate key that's stored inside the database.

# File lib/omniauth/strategies/openid_connect.rb, line 197
def secret
  options.jwt_secret || base64_decoded_jwt_secret || client_options.secret
end
uid() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 63
def uid
  user_info.raw_attributes[options.uid_field.to_sym] || user_info.sub
end

Private Instance Methods

access_token() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 241
def access_token
  return @access_token if @access_token

  @access_token = client.access_token!(
    scope: (options.scope if options.send_scope_to_token_endpoint),
    client_auth_method: options.client_auth_method
  )

  verify_id_token!(@access_token.id_token) if configured_response_type == 'code'

  @access_token
end
base64_decoded_jwt_secret() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 203
def base64_decoded_jwt_secret
  return unless options.jwt_secret_base64

  Base64.decode64(options.jwt_secret_base64)
end
client_options() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 312
def client_options
  options.client_options
end
configured_public_key() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 345
def configured_public_key
  @configured_public_key ||= begin
    if options.client_jwk_signing_key
      parse_jwk_key(options.client_jwk_signing_key)
    elsif options.client_x509_signing_key
      parse_x509_key(options.client_x509_signing_key)
    end
  end
end
configured_response_type() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 413
def configured_response_type
  @configured_response_type ||= options.response_type.to_s
end
decode(str) click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 366
def decode(str)
  UrlSafeBase64.decode64(str).unpack1('B*').to_i(2).to_s
end
decode!(id_token, key) click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 292
def decode!(id_token, key)
  ::OpenIDConnect::ResponseObject::IdToken.decode(id_token, key)
end
decode_id_token(id_token) click to toggle source

Unlike ::OpenIDConnect::ResponseObject::IdToken.decode, this method splits the decoding and verification of JWT into two steps. First, we decode the JWT without verifying it to determine the algorithm used to sign. Then, we verify it using the appropriate public key (e.g. if algorithm is RS256) or shared secret (e.g. if algorithm is HS256). This works around a limitation in the openid_connect gem: github.com/nov/openid_connect/issues/61

# File lib/omniauth/strategies/openid_connect.rb, line 262
def decode_id_token(id_token)
  decoded = JSON::JWT.decode(id_token, :skip_verification)
  algorithm = decoded.algorithm.to_sym

  keyset =
    case algorithm
    when :RS256, :RS384, :RS512
      public_key
    when :HS256, :HS384, :HS512
      secret
    end

  decoded.verify!(keyset)
  ::OpenIDConnect::ResponseObject::IdToken.new(decoded)
rescue JSON::JWK::Set::KidNotFound
  # If the JWT has a key ID (kid), then we know that the set of
  # keys supplied doesn't contain the one we want, and we're
  # done. However, if there is no kid, then we try each key
  # individually to see if one works:
  # https://github.com/nov/json-jwt/pull/92#issuecomment-824654949
  raise if decoded&.header&.key?('kid')

  decoded = decode_with_each_key!(id_token, keyset)

  raise unless decoded

  decoded

end
decode_with_each_key!(id_token, keyset) click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 296
def decode_with_each_key!(id_token, keyset)
  return unless keyset.is_a?(JSON::JWK::Set)

  keyset.each do |key|
    begin
      decoded = decode!(id_token, key)
    rescue JSON::JWS::VerificationFailed, JSON::JWS::UnexpectedAlgorithm, JSON::JWS::UnknownAlgorithm
      next
    end

    return decoded if decoded
  end

  nil
end
discover!() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 219
def discover!
  return unless options.discovery

  client_options.authorization_endpoint = config.authorization_endpoint
  client_options.token_endpoint = config.token_endpoint
  client_options.userinfo_endpoint = config.userinfo_endpoint
  client_options.jwks_uri = config.jwks_uri
  client_options.end_session_endpoint = config.end_session_endpoint if config.respond_to?(:end_session_endpoint)
end
encoded_post_logout_redirect_uri() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 376
def encoded_post_logout_redirect_uri
  return unless options.post_logout_redirect_uri

  URI.encode_www_form(
    post_logout_redirect_uri: options.post_logout_redirect_uri
  )
end
end_session_endpoint_is_valid?() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 384
def end_session_endpoint_is_valid?
  client_options.end_session_endpoint &&
    client_options.end_session_endpoint =~ URI::DEFAULT_PARSER.make_regexp
end
fetch_key() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 209
def fetch_key
  @fetch_key ||= parse_jwk_key(::OpenIDConnect.http_client.get_content(client_options.jwks_uri))
end
id_token_callback_phase() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 393
def id_token_callback_phase
  user_data = decode_id_token(params['id_token']).raw_attributes
  env['omniauth.auth'] = AuthHash.new(
    provider: name,
    uid: user_data['sub'],
    info: { name: user_data['name'], email: user_data['email'] },
    extra: { raw_info: user_data }
  )
  call_app!
end
issuer() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 213
def issuer
  resource = "#{ client_options.scheme }://#{ client_options.host }"
  resource = "#{ resource }:#{ client_options.port }" if client_options.port
  ::OpenIDConnect::Discovery::Provider.discover!(resource).issuer
end
logout_path_pattern() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 389
def logout_path_pattern
  @logout_path_pattern ||= %r{\A#{Regexp.quote(request_path)}(/logout)}
end
new_nonce() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 331
def new_nonce
  session['omniauth.nonce'] = SecureRandom.hex(16)
end
new_state() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 316
def new_state
  state = if options.state.respond_to?(:call)
            if options.state.arity == 1
              options.state.call(env)
            else
              options.state.call
            end
          end
  session['omniauth.state'] = state || SecureRandom.hex(16)
end
parse_jwk_key(key) click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 359
def parse_jwk_key(key)
  json = JSON.parse(key)
  return JSON::JWK::Set.new(json['keys']) if json.key?('keys')

  JSON::JWK.new(json)
end
parse_x509_key(key) click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 355
def parse_x509_key(key)
  OpenSSL::X509::Certificate.new(key).public_key
end
redirect_uri() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 370
def redirect_uri
  return client_options.redirect_uri unless params['redirect_uri']

  "#{ client_options.redirect_uri }?redirect_uri=#{ CGI.escape(params['redirect_uri']) }"
end
session() click to toggle source
Calls superclass method
# File lib/omniauth/strategies/openid_connect.rb, line 339
def session
  return {} if @env.nil?

  super
end
stored_nonce() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 335
def stored_nonce
  session.delete('omniauth.nonce')
end
stored_state() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 327
def stored_state
  session.delete('omniauth.state')
end
user_info() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 229
def user_info
  return @user_info if @user_info

  if access_token.id_token
    decoded = decode_id_token(access_token.id_token).raw_attributes

    @user_info = ::OpenIDConnect::ResponseObject::UserInfo.new access_token.userinfo!.raw_attributes.merge(decoded)
  else
    @user_info = access_token.userinfo!
  end
end
valid_response_type?() click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 404
def valid_response_type?
  return true if params.key?(configured_response_type)

  error_attrs = RESPONSE_TYPE_EXCEPTIONS[configured_response_type]
  fail!(error_attrs[:key], error_attrs[:exception_class].new(params['error']))

  false
end
verify_id_token!(id_token) click to toggle source
# File lib/omniauth/strategies/openid_connect.rb, line 417
def verify_id_token!(id_token)
  return unless id_token

  decode_id_token(id_token).verify!(issuer: options.issuer,
                                    client_id: client_options.identifier,
                                    nonce: stored_nonce)
end