Apple SSO: Why I am getting a 'unsupported_grant_type' error? - ios

Problem
When i try to validate the code returned from the Apple SSO client flow, I keep getting a unsupported_grant_type 400 error.
The docs say that an unsupported_grant_type will be returned when The authenticated client is not authorized to use the grant type. I've enabled Apple SSO on the App Id, the Service Id, and have even verified my support email domains. What am I missing? Is there some other approval step I need to complete to get authorized?
I've tried removing params from my verification request, but still get the same error code.
Details
The SSO redirect gives me a form-encoded POST body that looks something like this: {"state"=>"x", "code"=>"y", "id_token"=>"z"}.
I then attempt to validate the token by calling validate_auth_token.
def validate_auth_token(token, is_refresh = false)
uri = URI.parse('https://appleid.apple.com/auth/token')
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
headers = { 'Content-Type': 'text/json' }
request = Net::HTTP::Post.new(uri.path, headers)
request_body = {
client_id: #client_id,
client_secret: retreive_client_secret
}
if is_refresh
request_body[:grant_type] = 'refresh_token'
request_body[:refresh_token] = token
else
request_body[:grant_type] = 'authorization_code'
request_body[:code] = token
request_body[:redirect_uri] = "https://#{Rails.application.secrets.backend_host_port}/apple"
end
request.body = request_body.to_json
response = https.request(request)
p JSON.parse response.body
end
def retreive_client_secret
cert = retreive_secret_cert
ecdsa_key = OpenSSL::PKey::EC.new cert
algorithm = 'ES256'
headers = {
'alg': algorithm,
'kid': #key_id
}
claims = {
'iss': #team_id,
'iat': Time.now.to_i,
'exp': Time.now.to_i + 5.months.to_i,
'aud': 'https://appleid.apple.com',
'sub': #client_id
}
token = JWT.encode claims, ecdsa_key, algorithm, headers
token
end
Where #client_id is the "Service ID" I submitted in the initial SSO request, #key_id is the id of the private key downloaded from the apple key dashboard, and #team_id is our apple team id. retrieve_secret_cert simply gets the cert file body used to generate the client secret.
Given all this, I would expect a TokenResponse, but keep getting the same error {"error"=>"unsupported_grant_type"} with no additional explanation.

The token validation request needs to be form encoded, not json encoded. Also, the request wasn't validating correctly when I included an alg header in the JWT, but worked after I removed it.
Here's the updated code:
def validate_auth_token(token, is_refresh = false)
uri = URI.parse('https://appleid.apple.com/auth/token')
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
request_body = {
client_id: #client_id,
client_secret: retreive_client_secret
}
if is_refresh
request_body[:grant_type] = 'refresh_token'
request_body[:refresh_token] = token
else
request_body[:grant_type] = 'authorization_code'
request_body[:code] = token
request_body[:redirect_uri] = "https://#{Rails.application.secrets.backend_host_port}/auth"
end
request = Net::HTTP::Post.new(uri.path)
request.set_form_data(request_body)
response = https.request(request)
JSON.parse response.body
end
def retreive_client_secret
cert = retreive_secret_cert
ecdsa_key = OpenSSL::PKey::EC.new cert
algorithm = 'ES256'
headers = {
'kid': #key_id
}
claims = {
'iss': #team_id,
'iat': Time.now.to_i,
'exp': Time.now.to_i + 5.minutes.to_i,
'aud': 'https://appleid.apple.com',
'sub': #client_id
}
token = JWT.encode claims, ecdsa_key, algorithm, headers
token
end
Thank you sudhakar19 for pointing out the encoding error.

Related

Sign In with Apple, decoded Apple response

I've implemented 'Sign In with Apple' from this source (https://gist.github.com/aamishbaloch/2f0e5d94055e1c29c0585d2f79a8634e?permalink_comment_id=3328115) taking into account the comments of NipunShaji and aj3sh. But it doesn't works because Apple sends incomplete data: I recieve
decoded = {'iss': 'https://appleid.apple.com', 'aud': '...', 'exp': 1664463442, 'iat': 1664377042, 'sub': '.....', 'at_hash': '....', 'auth_time': 1664377030, 'nonce_supported': True}
without email data).
According to the Apple's documentation typical response contains email: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple.
What I've missed?
Additional code:
view.py file:
class AppleSocialAuthView(GenericAPIView):
serializer_class = AppleSocialAuthSerializer
permission_classes = [AllowAny]
def post(self, request):
"""
POST with "auth_token"
Send an access token as from facebook to get user information
"""
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
data = (serializer.validated_data['auth_token'])
return Response(data, status=status.HTTP_200_OK)
serializer.py file:
class Apple(BaseOAuth2):
"""apple authentication backend"""
name = 'apple'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
SCOPE_SEPARATOR = ','
ID_KEY = 'uid'
#handle_http_errors
def do_auth(self, access_token, *args, **kwargs):
"""
Finish the auth process once the access_token was retrieved
Get the email from ID token received from apple
"""
response_data = {}
client_id, client_secret = self.get_key_and_secret()
headers = {'content-type': "application/x-www-form-urlencoded"}
data = {
'client_id': client_id,
'client_secret': client_secret,
'code': access_token,
'grant_type': 'authorization_code',
'redirect_uri': settings.SOCIAL_AUTH_APPLE_REDIRECT_URL
}
res = requests.post(Apple.ACCESS_TOKEN_URL, data=data, headers=headers)
response_dict = res.json()
id_token = response_dict.get('id_token', None)
if id_token:
decoded = jwt.decode(id_token, '', options={"verify_signature": False}, verify=False)
print(decoded)
response_data.update({'email': decoded['email']}) if 'email' in decoded else None
response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None
response = kwargs.get('response') or {}
response.update(response_data)
response.update({'access_token': access_token}) if 'access_token' not in response else None
kwargs.update({'response': response, 'backend': self})
return self.strategy.authenticate(*args, **kwargs)
def get_user_details(self, response):
email = response.get('email', None)
details = {
'email': email,
}
return details
def get_key_and_secret(self):
headers = {
'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID,
'alg': 'ES256',
}
payload = {
'iss': settings.SOCIAL_AUTH_APPLE_TEAM_ID,
'iat': int(time.time()),
'exp': int(time.time()) + 15552000,
'aud': 'https://appleid.apple.com',
'sub': settings.SOCIAL_AUTH_APPLE_CLIENT_ID,
}
client_secret = jwt.encode(
payload,
settings.SOCIAL_AUTH_APPLE_CLIENT_SECRET,
# algorithm='ES256',
headers=headers
)
return settings.SOCIAL_AUTH_APPLE_CLIENT_ID, client_secret
class AppleSocialAuthSerializer(serializers.Serializer):
auth_token = serializers.CharField()
def validate_auth_token(self, auth_token):
user_data = Apple()
user_data = user_data.do_auth(auth_token)
try:
email = user_data['email']
name = user_data['name']
provider = 'apple'
return register_social_user(
provider=provider, email=email, name=name)
except Exception as identifier:
raise serializers.ValidationError(
'The token is invalid or expired. Please login again.'
)
When I test this proces on my Mac (logging into web app), the end result is that I can see on my Mac, preferences -> Apple ID, that I'm using SSO for this application.
So it looks like Apple validated this Web App.
If they do send email, only first time the user is logging in to Web App, how Web App should know next time what user to log in?
There is no single parameter that would identify the user in decoded response (like some ID, which would also appear in their first response?
Best Regards, Marek

I want to send the proper parameter to salesforce for authentication with JWT on Ruby

I am currently working on the project that I am going to integrate the application of my company and salesforce.
In my case, it seemed that using the JWT for authentication is better. So, I wanted to try it.
but I don't know how to generate JWT and send the proper request to salesforce on Ruby though I read docs.
What I wanted to do is that
1, create application on salesforce (done)
2, create X509 certification and set it on the application on salesforce. (done)
3, create JWT by using the secret key of X509 certification. (I think I've done it )
4, send post request with JWT parameter included in assertion params and grant_type(grant_type= urn:ietf:params:oauth:grant-type:jwt-bearer&) (I got an error)
when I send post request the errors says {"error":"invalid_grant","error_description":"invalid assertion"} so it occurs certainly because of the parameter I sent.
the code I randomly wrote is something like this.
require 'jwt'
require 'json'
require 'net/http'
require 'uri'
payload = {
"sub": "abel#example.com", ← my account on salesforce
"iss": "3MVG9pe2TCoA1PasbdvjabsodyoQFZTn0Rjsdbfjbasojdbn;oajs", ← the consumer key of the application on salesforce.
"aud": "https://test.salesforce.com"
}
public_key = Base64.urlsafe_encode64(
'USqTxNC7MMIeF9iegop3WeDvFL
075JSUECgYEA76FNJLeStdq+J6Fj2ZBYdDoeuDHv3iNA0nnIse9d6HnjbdrdvjmV
rT1CJuHh9gnNKg4tyjkbpc9IVj4/GF0mNUCgYEAynvj
qOYCzts4W7Bdumk6z8QULJ5QoYCrAgFtwra9R1HDcxTz+GPgJOVx2QBX+aQbDOaD
WV1s9WqE0/Lfi/VVUEzg1hZ8326buGRk1DRVG2Oa48==') ← this is public_key example of the certification.
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key
token = JWT.encode payload, rsa_private, 'RS256'
puts token
decoded_token = JWT.decode token, rsa_public, true, { algorithm: 'RS256' }
puts decoded_token
post = {
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': token
}
uri = URI.parse('https://login.salesforce.com/services/oauth2/token')
https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
response = https.post(uri.path, post.to_query)
print response.body
the PHP version of what I want to achieve is something like this.
<?php
require_once './vendor/autoload.php';
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Rsa\Sha256;
// login URL
// production: https://login.salesforce.com
// Sandbox: https://test.login.salesforce.com
define('LOGIN_URL', 'https://test.salesforce.com');
//consumer key
define('CLIENT_ID', <<consumer key of the application on salesforce>>);
//user ID
define('USER_ID', 'xxxxx#example.com');
function createjwt() {
$signer = new Sha256();
$privateKey = new Key('file://cert/server.key'); ← probably the key from certification
$time = time();
$token = (new Builder())->issuedBy(CLIENT_ID) // iss: consumer key
->permittedFor(LOGIN_URL) // aud: Salesforce login URL
->relatedTo(USER_ID) // sub: Salesforce user ID
->expiresAt($time + 3 * 60) // exp: within three mins
->getToken($signer, $privateKey);
return $token;
}
$jwt = createjwt();
echo $jwt;
function auth() {
$jwt = createjwt();
$post = array(
'grant_type' => GRANT_TYPE,
'assertion' => $jwt,
);
$curl = curl_init();
curl_setopt( $curl, CURLOPT_URL, AUTH_URL );
curl_setopt( $curl, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1 );
curl_setopt( $curl, CURLOPT_POSTFIELDS, $post );
$buf = curl_exec( $curl );
if ( curl_errno( $curl ) ) {
exit;
}
curl_close( $curl );
$json = json_decode( $buf );
$accinfo = array(
// URL to access
'instance_url' => $json->instance_url,
// Bearer token in order to access
'access_token' => $json->access_token,
);
return $accinfo;
}
$accinfo = auth();
EDIT
I changed a code a lot. But I still have different error that says 'initialize': Neither PUB key nor PRIV key: nested asn1 error (OpenSSL::PKey::RSAError)' around #private_key definition.
I read this and tried changing the string in private_key.pem to in one line but I didn't work ( maybe I did in a wrong way) and didn't understand the meaning of incorrect password (mentioned as the second answer) What causes "Neither PUB key nor PRIV key:: nested asn1 error" when building a public key in ruby?
def initialize
#cert_file = File.join(File.dirname(__FILE__), *%w[private_key.pem])
# #cert = Base64.urlsafe_encode64(#cert_file)
# print #cert_
# #cert_file = File.join(File.dirname(__FILE__), *%w[server.csr])
#base_url = "https://test.salesforce.com"
#auth_endpoint = "/services/oauth2/authorize"
#token_request_endpoint = "/services/oauth2/token"
#token_revoke_endpoint = "/services/oauth2/revoke"
#username = "my username"
#client_id = "pe2TCoA1~~~~" client_id
#private_key = OpenSSL::PKey::RSA.new(File.read(#cert_file))
# #private_key = OpenSSL::PKey::RSA.generate(private_key)
#rsa_public = #private_key.public_key
# #private_key = OpenSSL::PKey::RSA.new(File.read(#cert_file))
end
def claim_set
{
iss: #client_id,
sub: #username,
aud: #base_url,
exp: (Time.now + 3.minutes).to_i.to_s
}
end
def jwt_bearer_token
JWT.encode(self.claim_set.to_s, #rsa_public, 'RS256')
end
def request_auth
post = {body: {grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion: jwt_bearer_token}}
uri = URI.parse("#{#base_url}#{#token_request_endpoint}")
https = Net::HTTP.new(uri.host, 443)
https.use_ssl = true
response = https.post(uri.path, post.to_query)
print response.body
end
Salesforce.new.request_auth
end
Any advices are appreciated.
Thank you

The view function did not return a valid response. The return type must be a string, dict

using Spotify API and Flask I am trying to extend refresh_token validity. As a result, when I send a request to the server, I get this error:
*The view function did not return a valid response. The return type must be a string, dict, tuple, Response instance, or WSGI callable, but it was a Response.*
My code:
#app.route("/index")
def index():
if time.time() > session['expires_in']:
payload = session['refresh_token']
ref_payload = {
'grant_type': 'refresh_token',
'refresh_token':session["refresh_token"]
}
header={'Authorization': 'Basic ' + '<CLIENT_ID>:<CLIENT_SECRET'}
r = requests.post(AUTH_URL, data=ref_payload, headers=header)
return r
#app.route("/q")
def api_callback():
session.clear()
code = request.args.get('code')
res = requests.post(AUTH_URL, data={
"grant_type":"authorization_code",
"code":code,
"redirect_uri":REDIRECT_URI,
"client_id":CLIENT_ID,
"client_secret":CLIENT_SECRET
})
res_body = res.json()
session["token"] = res_body.get("access_token")#token
session["expires_in"] = res_body.get("expires_in")#time
session["refresh_token"] = res_body.get("refresh_token")#reflesh token
return redirect("index")
https://accounts.spotify.com/api/token is accepted as AUTH_URL
Most likely the problem is very commonplace, but I can't think of a solution now. Thanks in advance
I solved this problem. In my configurashion file i was create a veriable in which i encode my client_id and client_secret to base64 format:
ID_SEC = CLIENT_ID +':'+ CLIENT_SECRET
base64_encode = base64.b64encode(ID_SEC.encode()).decode()
After in the header i edit authorisation :
header={
'Content-Type':'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': 'Basic {}'.format(base64_encode)
}
And send post requests:
r = requests.post(AUTH_URL, data=ref_payload, headers=header)

Unable to create draft PayPal invoice using v2 API version

I am upgrading PayPal Invoicing feature from v1 to v2 (Because v1 is deprecated) in my Ruby on Rails application.
Since there's no official library/gem supporting v2 invoicing, so I decided to build everything as per this official documentation here: https://developer.paypal.com/docs/api/invoicing/v2.
The flow is like this:
System will get an access-token based on ClientID and ClientSecret
From this access-token, I will be generating a new invoice_number by sending curl request to: https://api.sandbox.paypal.com/v2/invoicing/generate-next-invoice-number
Upon receiving the invoice_number, I am sending curl request to create draft invoice endpoint with all the required data
curl -v -X POST https://api.sandbox.paypal.com/v2/invoicing/invoice
The issue I am facing is with the last point. I am getting 201 created response from create draft invoice endpoint but the endpoint is not returning me the complete invoice object along with Invoice ID.
Here's what I am getting:
"201"
{"rel"=>"self", "href"=>"https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-Z3K7-Y79X-36EM-ZQX8", "method"=>"GET"}
If you try opening this link, you'll see this:
{
"name":"AUTHENTICATION_FAILURE",
"message":"Authentication failed due to invalid authentication credentials or a missing Authorization header.",
"links": [
{
"href":"https://developer.paypal.com/docs/api/overview/#error",
"rel":"information_link"
}
]
}
Not sure what I am missing here!
Below is the code for reference:
require 'net/http'
require 'uri'
require 'json'
class PaypalInvoice
def initialize order
#order = order
#client_id = ENV['PAYPAL_CLIENT_ID']
#client_secret = ENV['PAYPAL_CLIENT_SECRET']
#base_url = ENV['AD_PP_ENV'] == 'sandbox' ? 'https://api.sandbox.paypal.com' : 'https://api.paypal.com'
#paypal_access_token_identifier = 'paypal_access_token'
#request_id ||= SecureRandom.uuid
end
def create_draft_invoice
raise "Paypal token not found" unless Rails.cache.exist?(#paypal_access_token_identifier)
invoice_number = "#141"
sleep 5
try = 0
uri = URI.parse(#base_url + "/v2/invoicing/invoices")
request = Net::HTTP::Post.new(uri)
request['X-PAYPAL-SANDBOX-EMAIL-ADDRESS'] = ENV['PAYPAL_CLIENT_EMAIL']
request['Authorization'] = "Bearer #{Rails.cache.fetch(#paypal_access_token_identifier)['access_token']}"
request['Content-Type'] = 'application/json'
request['PayPal-Request-Id'] = #request_id.to_s
request.body = JSON.dump({
"detail" => get_detail(invoice_number),
"invoicer" => get_invoicer,
"primary_recipients" => get_primary_recipients,
"items" => items_info,
"configuration" => {
"partial_payment" => {
"allow_partial_payment" => false,
},
"allow_tip" => false,
"tax_calculated_after_discount" => true,
"tax_inclusive" => true
}
})
req_options = {
use_ssl: uri.scheme == "https",
}
response = Net::HTTP.start(uri.host, uri.port, req_options) do |http|
http.request(request)
end
p 'method: create_draft_invoice. response: '
p response.code
p JSON.parse(response.body)
raise "Paypal token expired" if response.code.to_s == '401'
rescue RuntimeError => error
p "#{error.to_s}"
try += 1
access_token_response_status = get_new_access_token
retry if access_token_response_status.to_s == '200' and try <= 1
end
end
This:
{"rel"=>"self", "href"=>"https://api.sandbox.paypal.com/v2/invoicing/invoices/INV2-Z3K7-Y79X-36EM-ZQX8", "method"=>"GET"}
Is the endpoint for an API call, specifically 'Show invoice details': https://developer.paypal.com/docs/api/invoicing/v2/#invoices_get
Loading it in a browser w/o an Authorization: Bearer <Access-Token> header will give an AUTHENTICATION_FAILURE.
There's currently a bug in Sandbox with unconfirmed emails, so make sure your Sandbox emails are confirmed

How should i add header to HTTP GET request using ruby

Trying to add header in the request
email = "blahblah#gmail.com"
token = "abcdefghijk"
url = "http://www.somewebsite.com/request?params1=value1&params2=value2"
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port).start
request = Net::HTTP::Get.new(uri.request_uri)
request['email'] = email
request['token'] = token
response = http.request(request)
render json: response.body
The results i have gotten back is {"error":"invalid request parameters"}
I am supposed to get back a list of data in json. I tried using postman to test if the url is working and passed the email and token inside header and i got back the data that i wanted.
I am not sure where it went wrong with the code. Can anybody advise me which part did i do wrongly? Thanks a lot!
Try this.
email = "blahblah#gmail.com"
token = "abcdefghijk"
url = "http://www.somewebsite.com/request?params1=value1&params2=value2"
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port).start
request = Net::HTTP::Get.new(uri.request_uri, {'email'=>email,'token'=>token})
response = http.request(request)
render json: response.body
OR
request = Net::HTTP::Get.new(uri.request_uri)
request.add_field("email", email)
request.add_field("token", token)
response = http.request(request)
render json: response.body

Resources