I'm attempting to port the GitHub Apps sample starter code from Ruby to Python, but I'm running into trouble whilst generating the required JWT. The Ruby script looks like this, and works fine:
require 'openssl'
require 'jwt' # https://rubygems.org/gems/jwt
private_pem = File.read(YOUR_PATH_TO_PEM)
private_key = OpenSSL::PKey::RSA.new(private_pem)
payload = {
iat: Time.now.to_i,
exp: Time.now.to_i + (10 * 60),
iss: GITHUB_APP_IDENTIFIER
}
jwt = JWT.encode(payload, private_key, "RS256")
puts jwt
My Python script is as follows and produces the A JSON web token could not be decoded error when used against the GitHub API:
import os
import time
import jwt
APP_IDENTIFIER = os.environ["GITHUB_APP_IDENTIFIER"]
with open('./PRIVATE_KEY.pem', 'r') as f:
PRIVATE_KEY = f.read()
payload = {"iat": int(time.time()),
"exp": int(time.time()) + (10*60),
"iss": APP_IDENTIFIER}
print(jwt.encode(payload, PRIVATE_KEY, algorithm='RS256'))
When I tried printing the private keys from both scripts, I found that the Ruby version had an extra newline character. I tried adding this to the private key in the Python script, but it didn't change the output.
My best guess is that the difference is something to do with the OpenSSL::PKey::RSA.new call, but I'm not sure what that does to the key.
jwt.encode() will return you bytes in Python 3 which probably ends up having str() called on it somewhere in the sending pipeline. Calling str() on bytes objects in Python 3 can result in potentially surprising behaviour:
>>> a = b'hello'
>>> str(a)
"b'hello'"
The correct way to turn bytes into a string in Python 3 is to use:
>>> a.decode('utf-8')
'hello'
I added the call to decode on the end of my jwt.encode line and the API suddenly had no problems decoding the JWT.
Related
In my pure Ruby app one of the components to create a token for my request authentication to an external API is to create signature which is HMAC value that is created using the api_key and the secret_key. The signature contains the following elements that are each separated by a new line \n (except the last line) and are in the same order as below list:
ts = '1529342939277'
nonce = '883b170c-a768-41a1-ae6d-c626323aa128'
host = 'ws.idms.lexisnexis.com'
resource_path = '/restws/identity/v3/accounts/11111/workflows/rdp.test.workflow/conversations'
body_hash = 'fQoIAs0IO4vNleZVE9tcI3Ni7h+niT+GrrgEHsKZOyM='
API_KEY = '6njQLkz7uCiz1ZeJ1bFCWX4DFVTfKQXa'
SECRET_KEY = 'CcdaZEt7co647iJoEc5G29CHtlo7T9M3'
# create string signature separated by new line
signature = [ts, nonce, host, resource_path, body_hash].join("\n")
# create HMAC for signature
mac = Base64.strict_encode64(OpenSSL::HMAC.hexdigest('SHA256', API_KEY, signature))
2.7.0 :146 > mac
=> "ZDE4NDQxZDdiNmZkODNiODgyODI4Nzc2OTQ3OGFlMjVhZTMyNThhZTZlMTRiMjkxMzI0NmQ5NzljNDJkZWVhZg=="
According to the docs the signature should be Syb6i+sRygAGCgxLQJ4NwwKcT5Mnkh4r3QXgwZ3vmcE= but I'm getting ZDE4NDQxZDdiNmZkODNiODgyODI4Nzc2OTQ3OGFlMjVhZTMyNThhZTZlMTRiMjkxMzI0NmQ5NzljNDJkZWVhZg== instead. Where did I go wrong?
I've got an example how to do it in Java if something will be unclear: https://gist.github.com/mrmuscle1234/20c9d46d163fee66528449c0ea8419a7
I've been struggling to sign the jwt and I'm not familiar with the ruby file provided by apple on WWDC.. the code reads
require "base64"
require "jwt"
ISSUER_ID = "your-ID"
KEY_ID = "your-KeyID"
private_key = OpenSSL::PKey.read(File.read())
token = JWT.encode(
{
iss: ISSUER_ID,
exp: Time.now.to_i + 20 * 60,
aud: "appstoreconnect-v1"
},
private_key,
"ES256",
header_fields={
kid: KEY_ID }
)
puts token
the code keeps giving me this error when I run it on terminal.
enter image description here
my goal is simple, I just want to return some data from the GET api but am struggling with the 401 error on postman.
You have a syntax error in your code according to the screenshot you posted.
There's also what appears to be an error or misconfiguration in your code sample.
The gem most frequently used to encode/decode JWTs in ruby is here; this is the gem you're using in your example (via require "jwt") There are a number of examples on that page you can reference, but if you look thru the README you'll note they mention you can only use a kid with RSA and you're not using RSA in your example.
Try this:
token = JWT.encode(
{
iss: ISSUER_ID,
exp: Time.now.to_i + 20 * 60,
aud: "appstoreconnect-v1"
},
private_key,
"ES256"
)
Here's a link to using a JWK with the kid and the RSA algorithm.
Search for ecdsa_key on that same page for examples with ES256 (you'll find the one I posted above).
Seeing the original example from the WWDC docs might help provide additional context for the correct configuration but I'm not sure where to find that document. If it's public and you can link to it I can follow up.
I'm trying to verify a link that will expire in a week. I have an activator_token stored in the database, which will be used to generate the link in this format: http://www.example.com/activator_token. (And not activation tokens generated by Devise or Authlogic.)
Is there a way to make this activator token expire (in a week or so) without comparing with updated_at or some other date. Something like an encoded token, which will return nil when decoded after a week. Can any existing modules in Ruby do this? I don't want to store the generated date in the database or in an external store like Redis and compare it with Time.now. I want it to be very simple, and wanted to know if something like this already exists, before writing the logic again.
What you want to use is: https://github.com/jwt/ruby-jwt .
Here is some boilerplate code so you can try it out yourself.
require 'jwt'
# generate your keys when deploying your app.
# Doing so using a rake task might be a good idea
# How to persist and load the keys is up to you!
rsa_private = OpenSSL::PKey::RSA.generate 2048
rsa_public = rsa_private.public_key
# do this when you are about to send the email
exp = Time.now.to_i + 4 * 3600
payload = {exp: exp, discount: '9.99', email: 'user#example.com'}
# when generating an invite email, this is the token you want to incorporate in
# your link as a parameter
token = JWT.encode payload, rsa_private, 'RS256'
puts token
puts token.length
# this goes into your controller
begin
#token = params[:token]
decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
puts decoded_token.first
# continue with your business logic
rescue JWT::ExpiredSignature
# Handle expired token
# inform the user his invite link has expired!
puts "Token expired"
end
I decode (secret_key,client_id, path) into signature by following code :
require 'rubygems'
require 'base64'
require 'cgi'
require 'hmac-sha1'
#client_id = "asdkasdlda"
#secret = "3fdsdsfxds"
binary_key = Base64.decode64(#secret)
params.update({"client" => #client_id})
path = uri_path + "?" + params.collect{|k,v| "#{k}=#{v}"}.inject{|initial,cur| initial + "&" + cur}
digest = HMAC::SHA1.new(binary_key).update(path).digest
digest = Base64.encode64(digest).gsub(/[+\/]/, {"+" => "-", "/" => "_"}).delete("=")
return "#{path}&sig=#{digest}"
So, this code generates sig and path. we send request to server in following way:
/api/v1/customers/sign_in.json?user[email]=amit1656789#gmail.com&user[password]=[FILTERED]&client=asdkasdlda&sig=JSdP5xUHhgS8ZbKApBOIlsJKg_Q
Now, on server side, i want to decode this params["sign"] into app_id, secret_key and path means reverse process of above code. But i am not found any reverse process of this. Means
(app_id, secret, path) => "signature"
"signature" => (app_id, secret, path) /* Here i stuck */
First thing you should know:
"signature" => (app_id, secret, path)
This is not possible. It is not how MACs of any kind work. The signature does not contain the data. Signatures are meant to be sent alongside the message that they sign.
For secure HMAC, you should never send the secret with the message that you sign. It is also not possible to figure out a secret from the signature, except by repeatedly guessing what the value might be.
The usual way to confirm a signature is to follow the same process on the server, signing the same message, using the same secret (which the server should already have), and compare the signatures. You have made it difficult for yourself because you have signed the params as you sent them, and then put the signature on the end. You have to re-construct the message.
First, you need to use whatever web server library you can to get the request URI including the query string
signed_uri = "/api/v1/customers/sign_in.json?user[email]=amit1656789#gmail.com&user[password]=[FILTERED]&client=asdkasdlda&sig=JSdP5xUHhgS8ZbKApBOIlsJKg_Q"
Then split it into the message and its signature (I'll leave that to you, but just a regular expression ought to work):
message = "/api/v1/customers/sign_in.json?user[email]=amit1656789#gmail.com&user[password]=[FILTERED]&client=asdkasdlda"
signature = "JSdP5xUHhgS8ZbKApBOIlsJKg_Q"
To decode this signature back to the original digest (for easy comparison), just reverse the replace and encoding you did at the end on the client:
client_digest = Base64.decode64(
signature.gsub(/[-_]/, {"-" => "+", "_" => "/"}) )
Then on the server (where you should already have a value for #secret), calculate what you expect the signature to be:
#secret = '3fdsdsfxds'
binary_key = Base64.decode64(#secret)
server_digest = HMAC::SHA1.new(binary_key).update( message ).digest
if server_digest == client_digest
puts "The message was signed correctly"
else
puts "ERROR: The message or signature is not correct!"
end
Is there any library in Ruby that generates the Signature, 'X-PAYPAL-AUTHORIZATION' header that is required to make calls on behalf of the account holder who has authorized us through the paypal Permissions API.
I am done with the permissions flow and get the required access token, tokenSecret. I feel I am generating the signature incorrectly as all my calls with the the generated 'X-PAYPAL-AUTHORIZATION' fail. They give the following errors:
For NVP call I get:
You do not have permissions to make this API call
And for the GetBasicPersonalData call I get:
Authentication failed. API credentials are incorrect.
Has anyone gone through this in Ruby? What is best way to generate signature. Paypal has just provided some SDK in Paypal, Java, but not the algorithm to generate signature.
Thanks,
Nilesh
Take a look at the PayPal Permissions gem.
https://github.com/moshbit/paypal_permissions
Specifically lib/paypal_permissions/x_pp_authorization.rb
require 'cgi'
require 'openssl'
require 'base64'
class Hash
def to_paypal_permissions_query
collect do |key, value|
"#{key}=#{value}"
end.sort * '&'
end
end
module ActiveMerchant #:nodoc:
module Billing #:nodoc:
module XPPAuthorization
public
def x_pp_authorization_header url, api_user_id, api_password, access_token, access_token_verifier
timestamp = Time.now.to_i.to_s
signature = x_pp_authorization_signature url, api_user_id, api_password, timestamp, access_token, access_token_verifier
{ 'X-PAYPAL-AUTHORIZATION' => "token=#{access_token},signature=#{signature},timestamp=#{timestamp}" }
end
public
def x_pp_authorization_signature url, api_user_id, api_password, timestamp, access_token, access_token_verifier
# no query params, but if there were, this is where they'd go
query_params = {}
key = [
paypal_encode(api_password),
paypal_encode(access_token_verifier),
].join("&")
params = query_params.dup.merge({
"oauth_consumer_key" => api_user_id,
"oauth_version" => "1.0",
"oauth_signature_method" => "HMAC-SHA1",
"oauth_token" => access_token,
"oauth_timestamp" => timestamp,
})
sorted_query_string = params.to_paypal_permissions_query
base = [
"POST",
paypal_encode(url),
paypal_encode(sorted_query_string)
].join("&")
base = base.gsub /%([0-9A-F])([0-9A-F])/ do
"%#{$1.downcase}#{$2.downcase}" # hack to match PayPal Java SDK bit for bit
end
digest = OpenSSL::HMAC.digest('sha1', key, base)
Base64.encode64(digest).chomp
end
# The PayPalURLEncoder java class percent encodes everything other than 'a-zA-Z0-9 _'.
# Then it converts ' ' to '+'.
# Ruby's CGI.encode takes care of the ' ' and '*' to satisfy PayPal
# (but beware, URI.encode percent encodes spaces, and does nothing with '*').
# Finally, CGI.encode does not encode '.-', which we need to do here.
def paypal_encode str
s = str.dup
CGI.escape(s).gsub('.', '%2E').gsub('-', '%2D')
end
end
end
end
Sample parameters:
url = 'https://svcs.sandbox.paypal.com/Permissions/GetBasicPersonalData'
api_user_id = 'caller_1234567890_biz_api1.yourdomain.com'
api_password = '1234567890'
access_token = 'YJGjMOmTUqVPlKOd1234567890-jdQV3eWCOLuCQOyDK1234567890'
access_token_verifier = 'PgUjnwsMhuuUuZlPU1234567890'
The X-PAYPAL-AUTHORIZATION header [is] generated with URL "https://svcs.paypal.com/Permissions/GetBasicPersonalData". (see page 23, and chapter 7, at the link)
NVP stating "You do not have permissions to make this API call" means your API credentials are correct, just that your account does not have permission for the particular API you are trying to call. Something between the two calls you are submitting is not using the same API credentials.
For NVP call I get:
What NVP call?
TransactionSearch (see comments below)
Also, if you haven't already done so, you will want to use the sandbox APP-ID for testing in the sandbox, and you will need to apply for an app-id with Developer Technical Services (DTS) at PayPal to get an App-ID for live.
EDIT:
To use the TransactionSearch API, all you should be submitting is below. You do not need to specify any extra headers.
USER=xxxxxxxxxxxxxxxxxx
PWD=xxxxxxxxxxxxxxxxxx
SIGNATURE=xxxxxxxxxxxxxxxxxx
METHOD=TransactionSearch
VERSION=86.0
STARTDATE=2009-10-11T00:00:00Z
TRANSACTIONID=1234567890
//And for submitting API calls on bob's behalf, if his PayPal email was bob#bob.com:
SUBJECT=bob#bob.com