1
1
# frozen_string_literal: true
2
2
3
3
require 'omniauth-oauth2'
4
- require 'net/https '
4
+ require 'json/jwt '
5
5
6
6
module OmniAuth
7
7
module Strategies
8
8
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'
14
10
15
11
option :name , 'apple'
16
12
17
13
option :client_options ,
18
- site : 'https://appleid.apple.com' ,
14
+ site : ISSUER ,
19
15
authorize_url : '/auth/authorize' ,
20
16
token_url : '/auth/token' ,
21
17
auth_scheme : :request_body
@@ -24,13 +20,13 @@ def initialize(error_reason = nil, error_uri = nil)
24
20
scope : 'email name'
25
21
option :authorized_client_ids , [ ]
26
22
27
- uid { id_info [ ' sub' ] }
23
+ uid { id_info [ : sub] }
28
24
29
25
# Documentation on parameters
30
26
# https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
31
27
info do
32
28
prune! (
33
- sub : id_info [ ' sub' ] ,
29
+ sub : id_info [ : sub] ,
34
30
email : email ,
35
31
first_name : first_name ,
36
32
last_name : last_name ,
@@ -41,21 +37,21 @@ def initialize(error_reason = nil, error_uri = nil)
41
37
end
42
38
43
39
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 } )
46
42
end
47
43
48
44
def client
49
45
::OAuth2 ::Client . new ( client_id , client_secret , deep_symbolize ( options . client_options ) )
50
46
end
51
47
52
48
def email_verified
53
- value = id_info [ ' email_verified' ]
49
+ value = id_info [ : email_verified]
54
50
value == true || value == "true"
55
51
end
56
52
57
53
def is_private_email
58
- value = id_info [ ' is_private_email' ]
54
+ value = id_info [ : is_private_email]
59
55
value == true || value == "true"
60
56
end
61
57
@@ -79,54 +75,68 @@ def stored_nonce
79
75
80
76
def id_info
81
77
@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
99
82
end
100
83
end
101
84
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
115
113
end
116
114
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
119
118
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
121
122
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" )
123
133
end
124
134
125
135
def client_id
126
136
@client_id ||= if id_info . nil?
127
137
options . client_id
128
138
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]
130
140
end
131
141
end
132
142
@@ -138,7 +148,7 @@ def user_info
138
148
end
139
149
140
150
def email
141
- id_info [ ' email' ]
151
+ id_info [ : email]
142
152
end
143
153
144
154
def first_name
@@ -157,16 +167,15 @@ def prune!(hash)
157
167
end
158
168
159
169
def client_secret
160
- payload = {
170
+ jwt = JSON :: JWT . new (
161
171
iss : options . team_id ,
162
- aud : 'https://appleid.apple.com' ,
172
+ aud : ISSUER ,
163
173
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
170
179
end
171
180
172
181
def private_key
0 commit comments