Class: Clavis::Providers::Apple

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

Constant Summary collapse

ISSUER =
"https://appleid.apple.com"
APPLE_AUTH_URL =
"#{ISSUER}/auth/authorize".freeze
APPLE_TOKEN_URL =
"#{ISSUER}/auth/token".freeze
APPLE_JWKS_URL =
"#{ISSUER}/auth/keys".freeze
DEFAULT_CLIENT_SECRET_EXPIRY =

5 minutes in seconds

300

Instance Attribute Summary collapse

Attributes inherited from Base

#authorize_endpoint_url, #client_id, #client_secret, #provider_name, #redirect_uri, #scope, #token_endpoint_url, #userinfo_endpoint_url

Instance Method Summary collapse

Methods included from TokenExchangeHandler

#build_token_exchange_params, #handle_connection_error, #handle_error_response, #handle_faraday_error, #handle_parser_error, #handle_standard_error, #make_token_request, #parse_response, #skip_error_for_test?, #test_token_response, #validate_and_clean_code

Constructor Details

#initialize(config = {}) ⇒ Apple

Returns a new instance of Apple.



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/clavis/providers/apple.rb', line 21

def initialize(config = {})
  @team_id = config[:team_id] || ENV.fetch("APPLE_TEAM_ID", nil)
  @key_id = config[:key_id] || ENV.fetch("APPLE_KEY_ID", nil)
  @private_key = config[:private_key] || ENV.fetch("APPLE_PRIVATE_KEY", nil)
  @private_key_path = config[:private_key_path] || ENV.fetch("APPLE_PRIVATE_KEY_PATH", nil)
  @authorized_client_ids = config[:authorized_client_ids] || []
  @client_secret_expiry = config[:client_secret_expiry] || DEFAULT_CLIENT_SECRET_EXPIRY
  @client_options = config[:client_options] || {}

  # Set up endpoints with potential overrides from client_options
  endpoints = {
    authorization_endpoint: @client_options[:authorize_url] || APPLE_AUTH_URL,
    token_endpoint: @client_options[:token_url] || APPLE_TOKEN_URL,
    userinfo_endpoint: nil # Apple doesn't have a userinfo endpoint
  }

  # Override base URL if site is specified
  if @client_options[:site]
    base_uri = URI.parse(@client_options[:site])
    auth_uri = URI.parse(endpoints[:authorization_endpoint])
    token_uri = URI.parse(endpoints[:token_endpoint])

    # Only override the host, keep the paths
    auth_uri.scheme = base_uri.scheme
    auth_uri.host = base_uri.host
    token_uri.scheme = base_uri.scheme
    token_uri.host = base_uri.host

    endpoints[:authorization_endpoint] = auth_uri.to_s
    endpoints[:token_endpoint] = token_uri.to_s
  end

  config.merge!(endpoints)
  super
end

Instance Attribute Details

#authorized_client_idsObject (readonly)

Returns the value of attribute authorized_client_ids.



13
14
15
# File 'lib/clavis/providers/apple.rb', line 13

def authorized_client_ids
  @authorized_client_ids
end

#client_optionsObject (readonly)

Returns the value of attribute client_options.



13
14
15
# File 'lib/clavis/providers/apple.rb', line 13

def client_options
  @client_options
end

#key_idObject (readonly)

Returns the value of attribute key_id.



13
14
15
# File 'lib/clavis/providers/apple.rb', line 13

def key_id
  @key_id
end

#private_keyObject (readonly)

Returns the value of attribute private_key.



13
14
15
# File 'lib/clavis/providers/apple.rb', line 13

def private_key
  @private_key
end

#private_key_pathObject (readonly)

Returns the value of attribute private_key_path.



13
14
15
# File 'lib/clavis/providers/apple.rb', line 13

def private_key_path
  @private_key_path
end

#team_idObject (readonly)

Returns the value of attribute team_id.



13
14
15
# File 'lib/clavis/providers/apple.rb', line 13

def team_id
  @team_id
end

Instance Method Details

#authorization_endpointObject



57
58
59
# File 'lib/clavis/providers/apple.rb', line 57

def authorization_endpoint
  @authorize_endpoint_url
end

#authorize_url(state:, nonce:, scope: nil) ⇒ Object



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/clavis/providers/apple.rb', line 131

def authorize_url(state:, nonce:, scope: nil)
  # Generate a more secure URL with form_post response mode
  params = {
    response_type: "code",
    client_id: client_id,
    redirect_uri: Clavis::Security::HttpsEnforcer.enforce_https(redirect_uri),
    scope: scope || default_scopes,
    state: state,
    nonce: nonce,
    response_mode: "form_post" # Required for getting user information
  }

  uri = URI.parse(authorization_endpoint)
  uri.query = URI.encode_www_form(params)

  # Enforce HTTPS
  uri.scheme = "https" if Clavis.configuration.enforce_https && uri.scheme == "http"

  uri.to_s
end

#default_scopesObject



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

def default_scopes
  "name email"
end

#get_user_info(_access_token) ⇒ Object



126
127
128
129
# File 'lib/clavis/providers/apple.rb', line 126

def (_access_token)
  # Apple does not have a userinfo endpoint; user info is in the ID token
  raise Clavis::UnsupportedOperation, "Apple does not have a userinfo endpoint"
end

#openid_provider?Boolean

Returns:

  • (Boolean)


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

def openid_provider?
  true
end

#process_callback(code, user_data = nil) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/clavis/providers/apple.rb', line 152

def process_callback(code, user_data = nil)
  clean_code = code.to_s.gsub(/\A["']|["']\Z/, "")
  token_data = token_exchange(code: clean_code, user_data: user_data)

  # Extract user info from id_token and/or user_data
   = (token_data)

  # For OpenID Connect, use sub claim as identifier
  uid = if token_data[:id_token_claims]&.dig(:sub)
          token_data[:id_token_claims][:sub]
        else
          # Generate a hash as fallback
          data_for_hash = "#{provider_name}:#{token_data[:access_token] || ""}:#{[:email] || ""}"
          Digest::SHA1.hexdigest(data_for_hash)[0..19]
        end

  {
    provider: provider_name,
    uid: uid,
    info: ,
    credentials: {
      token: token_data[:access_token],
      refresh_token: token_data[:refresh_token],
      expires_at: token_data[:expires_at],
      expires: token_data[:expires_at] && !token_data[:expires_at].nil?
    },
    id_token: token_data[:id_token],
    id_token_claims: token_data[:id_token_claims] || {}
  }
end

#refresh_token(_refresh_token) ⇒ Object



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

def refresh_token(_refresh_token)
  # Apple doesn't support the standard OAuth refresh token flow
  # Instead, they use long-lived tokens that don't need refreshing
  raise Clavis::UnsupportedOperation, "Apple does not support refresh tokens"
end

#token_endpointObject



61
62
63
# File 'lib/clavis/providers/apple.rb', line 61

def token_endpoint
  @token_endpoint_url
end

#token_exchange(code:, **kwargs) ⇒ Object

Using keyword arguments with support for state verification (for compatibility)



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
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/clavis/providers/apple.rb', line 84

def token_exchange(code:, **kwargs)
  # Validate inputs
  raise Clavis::InvalidGrant unless Clavis::Security::InputValidator.valid_code?(code)

  params = {
    grant_type: "authorization_code",
    code: code,
    redirect_uri: redirect_uri,
    client_id: client_id,
    client_secret: generate_client_secret
  }

  response = http_client.post(token_endpoint, params)

  handle_token_error_response(response) if response.status != 200

  token_data = parse_token_response(response)

  # If id_token is present, verify and extract claims
  if token_data[:id_token] && !token_data[:id_token].empty?
    begin
      token_data[:id_token_claims] = verify_and_decode_id_token(
        token_data[:id_token],
        kwargs[:nonce]
      )
    rescue StandardError => e
      Clavis.logger.warn("Failed to verify ID token: #{e.message}")
    end
  end

  # Process user data if available
  if kwargs[:user_data] && !kwargs[:user_data].empty?
    begin
      token_data[:user_info] = JSON.parse(kwargs[:user_data])
    rescue JSON::ParserError => e
      Clavis.logger.warn("Failed to parse user data: #{e.message}")
    end
  end

  token_data
end

#userinfo_endpointObject



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

def userinfo_endpoint
  nil # Apple does not have a userinfo endpoint
end