Skip to content

Commit 72e8b1f

Browse files
authored
Merge pull request #101 from nov/feathre/json-jwt
replace ruby-jwt with json-jwt for jwks caching
2 parents eddf23b + b32669f commit 72e8b1f

File tree

5 files changed

+190
-125
lines changed

5 files changed

+190
-125
lines changed

lib/omniauth/apple/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module OmniAuth
22
module Apple
3-
VERSION = "1.2.2"
3+
VERSION = '1.3.0'
44
end
55
end

lib/omniauth/strategies/apple.rb

+66-57
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
# frozen_string_literal: true
22

33
require 'omniauth-oauth2'
4-
require 'net/https'
4+
require 'json/jwt'
55

66
module OmniAuth
77
module Strategies
88
class Apple < OmniAuth::Strategies::OAuth2
9-
class JWTFetchingFailed < CallbackError
10-
def initialize(error_reason = nil, error_uri = nil)
11-
super :jwks_fetching_failed, error_reason, error_uri
12-
end
13-
end
9+
ISSUER = 'https://appleid.apple.com'
1410

1511
option :name, 'apple'
1612

1713
option :client_options,
18-
site: 'https://appleid.apple.com',
14+
site: ISSUER,
1915
authorize_url: '/auth/authorize',
2016
token_url: '/auth/token',
2117
auth_scheme: :request_body
@@ -24,13 +20,13 @@ def initialize(error_reason = nil, error_uri = nil)
2420
scope: 'email name'
2521
option :authorized_client_ids, []
2622

27-
uid { id_info['sub'] }
23+
uid { id_info[:sub] }
2824

2925
# Documentation on parameters
3026
# https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
3127
info do
3228
prune!(
33-
sub: id_info['sub'],
29+
sub: id_info[:sub],
3430
email: email,
3531
first_name: first_name,
3632
last_name: last_name,
@@ -41,21 +37,21 @@ def initialize(error_reason = nil, error_uri = nil)
4137
end
4238

4339
extra do
44-
id_token = request.params['id_token'] || access_token&.params&.dig('id_token')
45-
prune!(raw_info: {id_info: id_info, user_info: user_info, id_token: id_token})
40+
id_token_str = request.params['id_token'] || access_token&.params&.dig('id_token')
41+
prune!(raw_info: {id_info: id_info, user_info: user_info, id_token: id_token_str})
4642
end
4743

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

5248
def email_verified
53-
value = id_info['email_verified']
49+
value = id_info[:email_verified]
5450
value == true || value == "true"
5551
end
5652

5753
def is_private_email
58-
value = id_info['is_private_email']
54+
value = id_info[:is_private_email]
5955
value == true || value == "true"
6056
end
6157

@@ -79,54 +75,68 @@ def stored_nonce
7975

8076
def id_info
8177
@id_info ||= if request.params&.key?('id_token') || access_token&.params&.key?('id_token')
82-
id_token = request.params['id_token'] || access_token.params['id_token']
83-
if (verification_key = fetch_jwks)
84-
jwt_options = {
85-
verify_iss: true,
86-
iss: 'https://appleid.apple.com',
87-
verify_iat: true,
88-
verify_aud: true,
89-
aud: [options.client_id].concat(options.authorized_client_ids),
90-
algorithms: ['RS256'],
91-
jwks: verification_key
92-
}
93-
payload, _header = ::JWT.decode(id_token, nil, true, jwt_options)
94-
verify_nonce!(payload)
95-
payload
96-
else
97-
{}
98-
end
78+
id_token_str = request.params['id_token'] || access_token.params['id_token']
79+
id_token = JSON::JWT.decode(id_token_str, :skip_verification)
80+
verify_id_token! id_token
81+
id_token
9982
end
10083
end
10184

102-
def fetch_jwks
103-
conn = Faraday.new(headers: {user_agent: 'ruby/omniauth-apple'}) do |c|
104-
c.response :json, parser_options: { symbolize_names: true }
105-
c.adapter Faraday.default_adapter
106-
end
107-
res = conn.get 'https://appleid.apple.com/auth/keys'
108-
if res.success?
109-
res.body
110-
else
111-
raise JWTFetchingFailed.new('HTTP Error when fetching JWKs')
112-
end
113-
rescue JWTFetchingFailed, Faraday::Error => e
114-
fail!(:jwks_fetching_failed, e) and nil
85+
def verify_id_token!(id_token)
86+
jwk = fetch_jwk! id_token.kid
87+
verify_signature! id_token, jwk
88+
verify_claims! id_token
89+
end
90+
91+
def fetch_jwk!(kid)
92+
JSON::JWK::Set::Fetcher.fetch File.join(ISSUER, 'auth/keys'), kid: kid
93+
rescue => e
94+
raise CallbackError.new(:jwks_fetching_failed, e)
95+
end
96+
97+
def verify_signature!(id_token, jwk)
98+
id_token.verify! jwk
99+
rescue => e
100+
raise CallbackError.new(:id_token_signature_invalid, e)
101+
end
102+
103+
def verify_claims!(id_token)
104+
verify_iss!(id_token)
105+
verify_aud!(id_token)
106+
verify_iat!(id_token)
107+
verify_exp!(id_token)
108+
verify_nonce!(id_token) if id_token[:nonce_supported]
109+
end
110+
111+
def verify_iss!(id_token)
112+
invalid_claim! :iss unless id_token[:iss] == ISSUER
115113
end
116114

117-
def verify_nonce!(payload)
118-
return unless payload['nonce_supported']
115+
def verify_aud!(id_token)
116+
invalid_claim! :aud unless [options.client_id].concat(options.authorized_client_ids).include?(id_token[:aud])
117+
end
119118

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

122-
fail!(:nonce_mismatch, CallbackError.new(:nonce_mismatch, 'nonce mismatch'))
123+
def verify_exp!(id_token)
124+
invalid_claim! :exp unless id_token[:exp] >= Time.now.to_i
125+
end
126+
127+
def verify_nonce!(id_token)
128+
invalid_claim! :nonce unless id_token[:nonce] && id_token[:nonce] == stored_nonce
129+
end
130+
131+
def invalid_claim!(claim)
132+
raise CallbackError.new(:id_token_claims_invalid, "#{claim} invalid")
123133
end
124134

125135
def client_id
126136
@client_id ||= if id_info.nil?
127137
options.client_id
128138
else
129-
id_info['aud'] if options.authorized_client_ids.include? id_info['aud']
139+
id_info[:aud] if options.authorized_client_ids.include? id_info[:aud]
130140
end
131141
end
132142

@@ -138,7 +148,7 @@ def user_info
138148
end
139149

140150
def email
141-
id_info['email']
151+
id_info[:email]
142152
end
143153

144154
def first_name
@@ -157,16 +167,15 @@ def prune!(hash)
157167
end
158168

159169
def client_secret
160-
payload = {
170+
jwt = JSON::JWT.new(
161171
iss: options.team_id,
162-
aud: 'https://appleid.apple.com',
172+
aud: ISSUER,
163173
sub: client_id,
164-
iat: Time.now.to_i,
165-
exp: Time.now.to_i + 60
166-
}
167-
headers = { kid: options.key_id }
168-
169-
::JWT.encode(payload, private_key, 'ES256', headers)
174+
iat: Time.now,
175+
exp: Time.now + 60
176+
)
177+
jwt.kid = options.key_id
178+
jwt.sign(private_key).to_s
170179
end
171180

172181
def private_key

omniauth-apple.gemspec

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
3737
spec.require_paths = ["lib"]
3838

3939
spec.add_dependency 'omniauth-oauth2'
40-
spec.add_dependency 'jwt'
40+
spec.add_dependency 'json-jwt'
4141
spec.add_development_dependency "bundler", "~> 2.0"
4242
spec.add_development_dependency "rake", "~> 13.0"
4343
spec.add_development_dependency "rspec", "~> 3.9"

0 commit comments

Comments
 (0)