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

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

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

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

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

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.

Yammer not accepting OAuth token token through header during post

I am currently trying to upgrade my application to send an OAuth token through as a header instead of through the query string as per their new requirements. When making a GET request with the OAuth token in the header, my request succeeds verifying a valid access_token. However when trying to make a post with the same token, I receive a 401 unauthorized. This post with the same access token succeeds when the access token is placed on the query string.
var request = (HttpWebRequest)WebRequest.Create(yammerurl);
request.Method = "POST";
request.Headers["Authorization"] = "Bearer " + access_token;
request.Host = "www.yammer.com";
request.ContentType = "application/json;charset=utf-8";
This is my set up for posting that is receiving an unauthorized exception and below is my set up for the GET request that succeeds. Again both of them are using the same access token and both methods work when the access token is passed through the query string.
string url = "https://www.yammer.com/api/v1/groups.json?mine=1";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Headers["Authorization"] = "Bearer " + YammerAccessToken;
request.Host = "www.yammer.com";
Does anybody know if my setup for posting is incorrect or if there is another parameter I need to add?
Is it because you are not specifying the HTTP verb GET in the second one? This is how I accomplish it in PHP and it works fine...
I call this in my main php file...
$ymuser = yammer_user_by_email('myemail#test.com');
this function is in my inc file...
function yammer_user_by_email($email, $token = null){
global $YAMMER_ADMIN_TOKEN;
$user = yammer_api_get('https://www.yammer.com/api/v1/users/by_email.json?email='.$email, $YAMMER_ADMIN_TOKEN);
return $user[0];
}
All of my http calls are routed through here, and the admin token is applied to the header...
function yammer_api_call($url, $method = 'GET', $body = '', $token){
if ($token == null) {
if (!$_SESSION['yammer_token'] || !$_SESSION['yammer_token']->access_token->token) return false;
$token = $_SESSION['yammer_token']->access_token->token;
}
if ($method == 'GET'){
$opts = array('http' =>
array(
'method' => $method,
'header' => "Host: www.yammer.com\r\n"
."Authorization: Bearer " . $token . "\r\n"
)
);
}else{
$opts = array('http' =>
array(
'method' => $method,
'header' => "Content-Type: application/x-www-form-urlencoded\r\n"
."Host: www.yammer.com\r\n"
."Authorization: Bearer " . $token . "\r\n"
."Content-Length: " . strlen($body) . "\r\n",
'content' => $body,
'timeout' => 60
)
);
}
$context = stream_context_create($opts);
$resp = file_get_contents($url, false, $context);
//print($resp);
$resp_obj = json_decode($resp);
return $resp_obj;
}

OAuth H9 Google Health

I am attempting to gain three-legged Oauth access, but I can't get the first step to work. My code so far:
include("OAuth.php");
$consumer_key = "anonymous";
$consumer_secret = "anonymous";
define("URI", "http://www.google.com");
$request_token_url = URI.'/accounts/OAuthGetRequestToken?scope=https%3A%2F%2Fwww.google.com%2Fh9%2Ffeeds%2F';
$parsed = parse_url($request_token_url);
$params = array();
$oauth_consumer = new OAuthConsumer($consumer_key, $consumer_secret, NULL);
$req_req = OAuthRequest::from_consumer_and_token($oauth_consumer, NULL, "GET", $request_token_url, $params);
$sig_method = new OAuthSignatureMethod_HMAC_SHA1();
$req_req->sign_request($sig_method, $oauth_consumer, NULL);
$request = $req_req->to_url();
$session = curl_init($request);
curl_setopt($session, CURLOPT_RETURNTRANSFER, 1);
// Make the request
$response = curl_exec($session);
//Error Handling:
// there is an error while executing the request,
if (!$response) {
$response = curl_error($curl);
}
curl_close($session);
parse_str($response, $params);
$oauth_token = $params['oauth_token'];
$oauth_token_secret = $params['oauth_token_secret'];
$_SESSION['CONSUMER_KEY'] = $consumer_key;
$_SESSION['CONSUMER_SECRET'] = $consumer_secret;
$_SESSION['REQUEST_TOKEN'] = $oauth_token;
$_SESSION['REQUEST_TOKEN_SECRET'] = $oauth_token_secret;
print_r($_SESSION);
I'm using OAuth.php.
The returning array does not give me anything:
Array (
[CONSUMER_KEY] => googlecodesamples.com
[CONSUMER_SECRET] => [REQUEST_TOKEN] => [REQUEST_TOKEN_SECRET] =>
)
I found this on the Google Oauth Reference
If your application is not registered, select HMAC-SHA1 and use the following key and secret:
consumer key: "anonymous" consumer
secret: "anonymous"
I have altered the consumer_key and consumer_secret variables but the returning array remains empty.
I'm not sure what I'm doing wrong this is a basic H9 sandbox development procedure; any advice would help.
Well I have figured this one out,
When I printed the response of the curl I got a message which said:
This URL has moved here:
https://www.google.com/accounts/OAuthGetRequestToken?oauth_consumer_key=anonymous%20%20%20%20[amp;oauth_nonce]%20=%3E%20828f80d4cec64b5b6fcca5010e2aa952%20%20%20%20[amp;oauth_signature]%20=%3E%20H+WrK1WIhyFEkrHRBvjpzcVLFvs=%20%20%20%20[amp;oauth_signature_method]%20=%3E%20HMAC-SHA1%20%20%20%20[amp;oauth_timestamp]%20=%3E%201282773417%20%20%20%20[amp;oauth_version]%20=%3E%201.0%20%20%20%20[amp;scope]%20=%3E%20https://www.google.com/h9/feeds/
So once I changed the $request_token_url to this, it worked like a charm and I finally have one-leg!! two left :)

Resources