Class: StandardId::Providers::Apple

Inherits:
Base
  • Object
show all
Defined in:
lib/standard_id/providers/apple.rb

Constant Summary collapse

ISSUER =
"https://appleid.apple.com".freeze
AUTH_ENDPOINT =
"#{ISSUER}/auth/authorize".freeze
TOKEN_ENDPOINT =
"#{ISSUER}/auth/token".freeze
JWKS_URI =
"#{ISSUER}/auth/keys".freeze
DEFAULT_SCOPE =
"name email".freeze
DEFAULT_RESPONSE_MODE =
"form_post".freeze

Class Method Summary collapse

Methods inherited from Base

callback_path, setup

Class Method Details

.authorization_url(state:, redirect_uri:, **options) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/standard_id/providers/apple.rb', line 22

def authorization_url(state:, redirect_uri:, **options)
  scope = options[:scope] || DEFAULT_SCOPE
  response_mode = options[:response_mode] || DEFAULT_RESPONSE_MODE

  ensure_basic_credentials!

  query = {
    client_id: StandardId.config.apple_client_id,
    redirect_uri: redirect_uri,
    response_type: "code",
    scope: scope,
    response_mode: response_mode,
    state: state
  }

  "#{AUTH_ENDPOINT}?#{URI.encode_www_form(query)}"
end

.config_schemaObject



55
56
57
58
59
60
61
62
63
# File 'lib/standard_id/providers/apple.rb', line 55

def config_schema
  {
    apple_client_id: { type: :string, default: nil },
    apple_mobile_client_id: { type: :string, default: nil },
    apple_private_key: { type: :string, default: nil },
    apple_key_id: { type: :string, default: nil },
    apple_team_id: { type: :string, default: nil }
  }
end

.default_scopeObject



65
66
67
# File 'lib/standard_id/providers/apple.rb', line 65

def default_scope
  DEFAULT_SCOPE
end

.exchange_code_for_user_info(code:, redirect_uri:, client_id: StandardId.config.apple_client_id) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/standard_id/providers/apple.rb', line 84

def (code:, redirect_uri:, client_id: StandardId.config.apple_client_id)
  ensure_full_credentials!(client_id: client_id)
  raise StandardId::InvalidRequestError, "Missing authorization code" if code.blank?

  token_response = HttpClient.post_form(TOKEN_ENDPOINT, {
    client_id: client_id,
    client_secret: generate_client_secret(client_id: client_id),
    code: code,
    grant_type: "authorization_code",
    redirect_uri: redirect_uri
  })

  unless token_response.is_a?(Net::HTTPSuccess)
    error_body = JSON.parse(token_response.body) rescue {}
    raise StandardId::InvalidRequestError, "Failed to exchange Apple authorization code: #{error_body['error']}"
  end

  parsed_token = JSON.parse(token_response.body)
  id_token = parsed_token["id_token"]
  raise StandardId::InvalidRequestError, "Apple response missing id_token" if id_token.blank?

  tokens = extract_token_payload(parsed_token)
   = verify_id_token(id_token: id_token, client_id: client_id)

  build_response(, tokens: tokens)
rescue StandardError => e
  raise e if e.is_a?(StandardId::OAuthError)
  raise StandardId::OAuthError, e.message, cause: e
end

.get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, **options) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/standard_id/providers/apple.rb', line 40

def (code: nil, id_token: nil, access_token: nil, redirect_uri: nil, **options)
  client_id = options[:client_id] || StandardId.config.apple_client_id

  if id_token.present?
    build_response(
      verify_id_token(id_token: id_token, client_id: client_id),
      tokens: { id_token: id_token }
    )
  elsif code.present?
    (code: code, redirect_uri: redirect_uri, client_id: client_id)
  else
    raise StandardId::InvalidRequestError, "Either code or id_token must be provided"
  end
end

.provider_nameObject



18
19
20
# File 'lib/standard_id/providers/apple.rb', line 18

def provider_name
  "apple"
end

.resolve_params(params, context: {}) ⇒ Object



77
78
79
80
81
82
# File 'lib/standard_id/providers/apple.rb', line 77

def resolve_params(params, context: {})
  flow = context[:flow] || :web
  client_id = flow == :mobile ? StandardId.config.apple_mobile_client_id : StandardId.config.apple_client_id

  params.merge(client_id: client_id)
end

.skip_csrf?Boolean

Returns:

  • (Boolean)


69
70
71
# File 'lib/standard_id/providers/apple.rb', line 69

def skip_csrf?
  true
end

.supports_mobile_callback?Boolean

Returns:

  • (Boolean)


73
74
75
# File 'lib/standard_id/providers/apple.rb', line 73

def supports_mobile_callback?
  true
end

.verify_id_token(id_token:, client_id: StandardId.config.apple_client_id) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/standard_id/providers/apple.rb', line 114

def verify_id_token(id_token:, client_id: StandardId.config.apple_client_id)
  raise StandardId::InvalidRequestError, "Missing id_token" if id_token.blank?
  if client_id.blank?
    raise StandardId::InvalidRequestError, "Apple client_id is not configured"
  end

  decoded_token = JWT.decode(id_token, nil, false)
  header = decoded_token[1]

  jwk = fetch_jwk(kid: header["kid"])

  verified_payload, = JWT.decode(
    id_token,
    jwk.public_key,
    true,
    algorithm: "RS256",
    iss: ISSUER,
    verify_iss: true,
    aud: client_id,
    verify_aud: true
  )

  {
    "sub" => verified_payload["sub"],
    "email" => verified_payload["email"],
    "email_verified" => verified_payload["email_verified"],
    "is_private_email" => verified_payload["is_private_email"]
  }.compact
rescue JWT::InvalidAudError => e
  raise StandardId::InvalidRequestError, "Invalid Apple ID token audience: #{e.message}"
rescue JWT::DecodeError => e
  raise StandardId::InvalidRequestError, "Invalid Apple ID token: #{e.message}"
rescue StandardError => e
  raise e if e.is_a?(StandardId::OAuthError)
  raise StandardId::OAuthError, e.message, cause: e
end