standard_id-apple 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a77a4c61dc7a649544a240e29a318dd89b3973a429fb796992214f043bdd176b
4
- data.tar.gz: a119c315995c98edac369d3d1c96d645b1871811c343e81a3f30e90480d354b8
3
+ metadata.gz: 3c67afb888da7a7a20542d46149eaf86215d02de9c097a757b98223f4c992811
4
+ data.tar.gz: b905edd16718f7fee932b567250a7caa5136d1cf64f82030668b7bcb8dd40d87
5
5
  SHA512:
6
- metadata.gz: 04a5fc1120b372888042832a64c3508f4dd2e85e6807da34365186ba77ed54846641c58c04880d0f2652776f0429d2abc57c8f613bd21ca417b19750723cc764
7
- data.tar.gz: 609d9235c817d7e9d6805b8b9f91f19f7f213d7475b3d48578cbc4d28065b9a20762fa68968c6634db454bb3e53d842edb5699037b811f6214c29cebb1a7b489
6
+ metadata.gz: fdd51606e4a08272c10bae448cecbf0472e8449a5679229013138b5fed0abef6f2e404ef1ef495731a1744c58538fbfc1c141af974ad02c825212a5a51adffc1
7
+ data.tar.gz: 58e35ebc3804fd52719774db5a63229918e0c89462dca2b7f654a8a57820389549044c1ca42e27cf46155afc6617942084e1ce41b650866c9ba0a049e7599289
data/README.md CHANGED
@@ -26,11 +26,11 @@ Configure Apple credentials via the StandardId configuration block:
26
26
 
27
27
  ```ruby
28
28
  StandardId.configure do |config|
29
- config.social.apple_client_id = ENV["APPLE_CLIENT_ID"]
30
- config.social.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"] # optional
31
- config.social.apple_team_id = ENV["APPLE_TEAM_ID"]
32
- config.social.apple_key_id = ENV["APPLE_KEY_ID"]
33
- config.social.apple_private_key = ENV["APPLE_PRIVATE_KEY_PEM"]
29
+ config.apple_client_id = ENV["APPLE_CLIENT_ID"]
30
+ config.apple_mobile_client_id = ENV["APPLE_MOBILE_CLIENT_ID"] # optional
31
+ config.apple_team_id = ENV["APPLE_TEAM_ID"]
32
+ config.apple_key_id = ENV["APPLE_KEY_ID"]
33
+ config.apple_private_key = ENV["APPLE_PRIVATE_KEY_PEM"]
34
34
  end
35
35
  ```
36
36
 
@@ -12,40 +12,47 @@ module StandardId
12
12
  JWKS_URI = "#{ISSUER}/auth/keys".freeze
13
13
  DEFAULT_SCOPE = "name email".freeze
14
14
  DEFAULT_RESPONSE_MODE = "form_post".freeze
15
+ AUTHORIZATION_PARAM_DEFAULTS = {
16
+ scope: DEFAULT_SCOPE,
17
+ response_mode: DEFAULT_RESPONSE_MODE
18
+ }.freeze
15
19
 
16
20
  class << self
17
21
  def provider_name
18
22
  "apple"
19
23
  end
20
24
 
21
- def authorization_url(state:, redirect_uri:, **options)
22
- scope = options[:scope] || DEFAULT_SCOPE
23
- response_mode = options[:response_mode] || DEFAULT_RESPONSE_MODE
25
+ def supported_authorization_params
26
+ [:nonce, :scope, :response_mode]
27
+ end
24
28
 
29
+ def authorization_url(state:, redirect_uri:, **options)
25
30
  ensure_basic_credentials!
26
31
 
27
32
  query = {
28
- client_id: StandardId.config.social.apple_client_id,
29
- redirect_uri: redirect_uri,
33
+ client_id: StandardId.config.apple_client_id,
34
+ redirect_uri:,
30
35
  response_type: "code",
31
- scope: scope,
32
- response_mode: response_mode,
33
- state: state
36
+ state:
34
37
  }
35
38
 
36
- "#{AUTH_ENDPOINT}?#{URI.encode_www_form(query)}"
39
+ supported_authorization_params.each do |param|
40
+ query[param] = options[param] || AUTHORIZATION_PARAM_DEFAULTS[param]
41
+ end
42
+
43
+ "#{AUTH_ENDPOINT}?#{URI.encode_www_form(query.compact)}"
37
44
  end
38
45
 
39
- def get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, **options)
40
- client_id = options[:client_id] || StandardId.config.social.apple_client_id
46
+ def get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, nonce: nil, **options)
47
+ client_id = options[:client_id] || StandardId.config.apple_client_id
41
48
 
42
49
  if id_token.present?
43
50
  build_response(
44
- verify_id_token(id_token: id_token, client_id: client_id),
51
+ verify_id_token(id_token: id_token, client_id: client_id, nonce: nonce),
45
52
  tokens: { id_token: id_token }
46
53
  )
47
54
  elsif code.present?
48
- exchange_code_for_user_info(code: code, redirect_uri: redirect_uri, client_id: client_id)
55
+ exchange_code_for_user_info(code: code, redirect_uri: redirect_uri, client_id: client_id, nonce: nonce)
49
56
  elsif access_token.present?
50
57
  raise StandardId::InvalidRequestError, "Access token login flow is not supported for Apple"
51
58
  else
@@ -77,12 +84,12 @@ module StandardId
77
84
 
78
85
  def resolve_params(params, context: {})
79
86
  flow = context[:flow] || :web
80
- client_id = flow == :mobile ? StandardId.config.social.apple_mobile_client_id : StandardId.config.social.apple_client_id
87
+ client_id = flow == :mobile ? StandardId.config.apple_mobile_client_id : StandardId.config.apple_client_id
81
88
 
82
89
  params.merge(client_id: client_id)
83
90
  end
84
91
 
85
- def exchange_code_for_user_info(code:, redirect_uri:, client_id: StandardId.config.social.apple_client_id)
92
+ def exchange_code_for_user_info(code:, redirect_uri:, client_id: StandardId.config.apple_client_id, nonce: nil)
86
93
  ensure_full_credentials!(client_id: client_id)
87
94
  raise StandardId::InvalidRequestError, "Missing authorization code" if code.blank?
88
95
 
@@ -108,7 +115,7 @@ module StandardId
108
115
  raise StandardId::InvalidRequestError, "Apple response missing id_token" if id_token.blank?
109
116
 
110
117
  tokens = extract_token_payload(parsed_token)
111
- user_info = verify_id_token(id_token: id_token, client_id: client_id)
118
+ user_info = verify_id_token(id_token: id_token, client_id: client_id, nonce: nonce)
112
119
 
113
120
  build_response(user_info, tokens: tokens)
114
121
  rescue StandardError => e
@@ -117,7 +124,7 @@ module StandardId
117
124
  raise StandardId::OAuthError, e.message, cause: e
118
125
  end
119
126
 
120
- def verify_id_token(id_token:, client_id: StandardId.config.social.apple_client_id)
127
+ def verify_id_token(id_token:, client_id: StandardId.config.apple_client_id, nonce: nil)
121
128
  raise StandardId::InvalidRequestError, "Missing id_token" if id_token.blank?
122
129
  raise StandardId::InvalidRequestError, "Apple client_id is not configured" if client_id.blank?
123
130
 
@@ -137,6 +144,15 @@ module StandardId
137
144
  verify_aud: true
138
145
  )
139
146
 
147
+ # Validate nonce if provided (web flow with server-generated nonce)
148
+ if nonce.present?
149
+ token_nonce = verified_payload["nonce"]
150
+ if token_nonce != nonce
151
+ raise StandardId::InvalidRequestError,
152
+ "ID token nonce mismatch. Expected: #{nonce}, got: #{token_nonce}"
153
+ end
154
+ end
155
+
140
156
  {
141
157
  "sub" => verified_payload["sub"],
142
158
  "email" => verified_payload["email"],
@@ -155,7 +171,7 @@ module StandardId
155
171
 
156
172
  private
157
173
 
158
- def ensure_basic_credentials!(client_id: StandardId.config.social.apple_client_id)
174
+ def ensure_basic_credentials!(client_id: StandardId.config.apple_client_id)
159
175
  return if client_id.present?
160
176
 
161
177
  raise StandardId::InvalidRequestError, "Apple OAuth is not configured"
@@ -165,9 +181,9 @@ module StandardId
165
181
  ensure_basic_credentials!(client_id: client_id)
166
182
 
167
183
  required = [
168
- StandardId.config.social.apple_private_key,
169
- StandardId.config.social.apple_key_id,
170
- StandardId.config.social.apple_team_id
184
+ StandardId.config.apple_private_key,
185
+ StandardId.config.apple_key_id,
186
+ StandardId.config.apple_team_id
171
187
  ]
172
188
 
173
189
  return unless required.any?(&:blank?)
@@ -175,21 +191,21 @@ module StandardId
175
191
  raise StandardId::InvalidRequestError, "Apple OAuth credentials are incomplete"
176
192
  end
177
193
 
178
- def generate_client_secret(client_id: StandardId.config.social.apple_client_id)
194
+ def generate_client_secret(client_id: StandardId.config.apple_client_id)
179
195
  header = {
180
196
  alg: "ES256",
181
- kid: StandardId.config.social.apple_key_id
197
+ kid: StandardId.config.apple_key_id
182
198
  }
183
199
 
184
200
  payload = {
185
- iss: StandardId.config.social.apple_team_id,
201
+ iss: StandardId.config.apple_team_id,
186
202
  iat: Time.current.to_i,
187
203
  exp: Time.current.to_i + 3600,
188
204
  aud: ISSUER,
189
205
  sub: client_id
190
206
  }
191
207
 
192
- private_key = OpenSSL::PKey::EC.new(StandardId.config.social.apple_private_key)
208
+ private_key = OpenSSL::PKey::EC.new(StandardId.config.apple_private_key)
193
209
  JWT.encode(payload, private_key, "ES256", header)
194
210
  end
195
211
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StandardId
4
4
  module Apple
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standard_id-apple
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jaryl Sim