Skip to content

Commit

Permalink
Merge pull request #101 from nov/feathre/json-jwt
Browse files Browse the repository at this point in the history
replace ruby-jwt with json-jwt for jwks caching
  • Loading branch information
nov authored Jan 17, 2023
2 parents eddf23b + b32669f commit 72e8b1f
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 125 deletions.
2 changes: 1 addition & 1 deletion lib/omniauth/apple/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module OmniAuth
module Apple
VERSION = "1.2.2"
VERSION = '1.3.0'
end
end
123 changes: 66 additions & 57 deletions lib/omniauth/strategies/apple.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
# frozen_string_literal: true

require 'omniauth-oauth2'
require 'net/https'
require 'json/jwt'

module OmniAuth
module Strategies
class Apple < OmniAuth::Strategies::OAuth2
class JWTFetchingFailed < CallbackError
def initialize(error_reason = nil, error_uri = nil)
super :jwks_fetching_failed, error_reason, error_uri
end
end
ISSUER = 'https://appleid.apple.com'

option :name, 'apple'

option :client_options,
site: 'https://appleid.apple.com',
site: ISSUER,
authorize_url: '/auth/authorize',
token_url: '/auth/token',
auth_scheme: :request_body
Expand All @@ -24,13 +20,13 @@ def initialize(error_reason = nil, error_uri = nil)
scope: 'email name'
option :authorized_client_ids, []

uid { id_info['sub'] }
uid { id_info[:sub] }

# Documentation on parameters
# https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
info do
prune!(
sub: id_info['sub'],
sub: id_info[:sub],
email: email,
first_name: first_name,
last_name: last_name,
Expand All @@ -41,21 +37,21 @@ def initialize(error_reason = nil, error_uri = nil)
end

extra do
id_token = request.params['id_token'] || access_token&.params&.dig('id_token')
prune!(raw_info: {id_info: id_info, user_info: user_info, id_token: id_token})
id_token_str = request.params['id_token'] || access_token&.params&.dig('id_token')
prune!(raw_info: {id_info: id_info, user_info: user_info, id_token: id_token_str})
end

def client
::OAuth2::Client.new(client_id, client_secret, deep_symbolize(options.client_options))
end

def email_verified
value = id_info['email_verified']
value = id_info[:email_verified]
value == true || value == "true"
end

def is_private_email
value = id_info['is_private_email']
value = id_info[:is_private_email]
value == true || value == "true"
end

Expand All @@ -79,54 +75,68 @@ def stored_nonce

def id_info
@id_info ||= if request.params&.key?('id_token') || access_token&.params&.key?('id_token')
id_token = request.params['id_token'] || access_token.params['id_token']
if (verification_key = fetch_jwks)
jwt_options = {
verify_iss: true,
iss: 'https://appleid.apple.com',
verify_iat: true,
verify_aud: true,
aud: [options.client_id].concat(options.authorized_client_ids),
algorithms: ['RS256'],
jwks: verification_key
}
payload, _header = ::JWT.decode(id_token, nil, true, jwt_options)
verify_nonce!(payload)
payload
else
{}
end
id_token_str = request.params['id_token'] || access_token.params['id_token']
id_token = JSON::JWT.decode(id_token_str, :skip_verification)
verify_id_token! id_token
id_token
end
end

def fetch_jwks
conn = Faraday.new(headers: {user_agent: 'ruby/omniauth-apple'}) do |c|
c.response :json, parser_options: { symbolize_names: true }
c.adapter Faraday.default_adapter
end
res = conn.get 'https://appleid.apple.com/auth/keys'
if res.success?
res.body
else
raise JWTFetchingFailed.new('HTTP Error when fetching JWKs')
end
rescue JWTFetchingFailed, Faraday::Error => e
fail!(:jwks_fetching_failed, e) and nil
def verify_id_token!(id_token)
jwk = fetch_jwk! id_token.kid
verify_signature! id_token, jwk
verify_claims! id_token
end

def fetch_jwk!(kid)
JSON::JWK::Set::Fetcher.fetch File.join(ISSUER, 'auth/keys'), kid: kid
rescue => e
raise CallbackError.new(:jwks_fetching_failed, e)
end

def verify_signature!(id_token, jwk)
id_token.verify! jwk
rescue => e
raise CallbackError.new(:id_token_signature_invalid, e)
end

def verify_claims!(id_token)
verify_iss!(id_token)
verify_aud!(id_token)
verify_iat!(id_token)
verify_exp!(id_token)
verify_nonce!(id_token) if id_token[:nonce_supported]
end

def verify_iss!(id_token)
invalid_claim! :iss unless id_token[:iss] == ISSUER
end

def verify_nonce!(payload)
return unless payload['nonce_supported']
def verify_aud!(id_token)
invalid_claim! :aud unless [options.client_id].concat(options.authorized_client_ids).include?(id_token[:aud])
end

return if payload['nonce'] && payload['nonce'] == stored_nonce
def verify_iat!(id_token)
invalid_claim! :iat unless id_token[:iat] <= Time.now.to_i
end

fail!(:nonce_mismatch, CallbackError.new(:nonce_mismatch, 'nonce mismatch'))
def verify_exp!(id_token)
invalid_claim! :exp unless id_token[:exp] >= Time.now.to_i
end

def verify_nonce!(id_token)
invalid_claim! :nonce unless id_token[:nonce] && id_token[:nonce] == stored_nonce
end

def invalid_claim!(claim)
raise CallbackError.new(:id_token_claims_invalid, "#{claim} invalid")
end

def client_id
@client_id ||= if id_info.nil?
options.client_id
else
id_info['aud'] if options.authorized_client_ids.include? id_info['aud']
id_info[:aud] if options.authorized_client_ids.include? id_info[:aud]
end
end

Expand All @@ -138,7 +148,7 @@ def user_info
end

def email
id_info['email']
id_info[:email]
end

def first_name
Expand All @@ -157,16 +167,15 @@ def prune!(hash)
end

def client_secret
payload = {
jwt = JSON::JWT.new(
iss: options.team_id,
aud: 'https://appleid.apple.com',
aud: ISSUER,
sub: client_id,
iat: Time.now.to_i,
exp: Time.now.to_i + 60
}
headers = { kid: options.key_id }

::JWT.encode(payload, private_key, 'ES256', headers)
iat: Time.now,
exp: Time.now + 60
)
jwt.kid = options.key_id
jwt.sign(private_key).to_s
end

def private_key
Expand Down
2 changes: 1 addition & 1 deletion omniauth-apple.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_dependency 'omniauth-oauth2'
spec.add_dependency 'jwt'
spec.add_dependency 'json-jwt'
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 13.0"
spec.add_development_dependency "rspec", "~> 3.9"
Expand Down
Loading

0 comments on commit 72e8b1f

Please sign in to comment.