I'm trying to secure a Sinatra API.
I'm using ruby-jwt to create the JWT, but I don't know exactly what to sign it with.
I'm trying to use the user's BCrypt password_digest, but every time password_digest is called it changes, making the signature invalid when I go to verify it.
Use any kind of application secret key, not a user's bcrypt password digest.
For example, use the dot env gem and a .env file, with an entry such as:
JWT_KEY=YOURSIGNINGKEYGOESHERE
I personally generate a key by using a simple random hex string:
SecureRandom.hex(64)
The hex string contains just 0-9 and a-f, so the string is URL safe.
For RS256 public and private key strategy you can use Ruby OpenSSL lib:
Generating keys:
key = OpenSSL::PKey::RSA.new 2048
open 'private_key.pem', 'w' do |io| io.write key.to_pem end
open 'public_key.pem', 'w' do |io| io.write key.public_key.to_pem end
Load key from .pem file to sign token:
priv_key = OpenSSL::PKey::RSA.new File.read 'private_key.pem'
token = JWT.encode payload, priv_key, 'RS256'
Load key from .pem file to Verify token(Create a middleware for this):
begin
# env.fetch gets http header
bearer = env.fetch('HTTP_AUTHORIZATION').slice(7..-1)
pub_key = OpenSSL::PKey::RSA.new File.read 'public_key.pem'
payload = JWT.decode bearer, pub_key, true, { algorithm: 'RS256'}
# access your payload here
#app.call env
rescue JWT::ExpiredSignature
[403, { 'Content-Type' => 'text/plain' }, ['The token has expired.']]
rescue JWT::DecodeError
[401, { 'Content-Type' => 'text/plain' }, ['A token must be passed.']]
rescue JWT::InvalidIssuerError
[403, { 'Content-Type' => 'text/plain' }, ['The token does not have a valid issuer.']]
rescue JWT::InvalidIatError
[403, { 'Content-Type' => 'text/plain' }, ['The token does not have a valid "issued at" time.']]
end
To use RSA key in your .env instead of loading a file, you will need to use gem 'dotenv' and import the key as a single line variable with the use of newline '\n'. check this question on how to do it. example:
PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nmineminemineminemine\nmineminemineminemine\nmineminemine...\n-----END PUBLIC KEY-----\n"
as an .env PUBLIC_KEY variable, loading the key will change to this:
key = OpenSSL::PKey::RSA.new ENV['PUBLIC_KEY']
According to wikipedia, a secret key used in cryptography is basically just that, a key to open the lock. The key should be consistent and reliable, but not easy to duplicate, just like a key you would use on your home.
As stated in this answer, secret keys should be randomly-generated. However, you still want the key to be retained for use across the application. By using the password digest from bcrypt, you are actually using a hashed key that was derived from a base secret key (the password). Because the hash is random, this is not a reliable secret key to use, as you stated.
The previous answer using SecureRandom.hex(64) is a great way to create an initial base application key. However, in a production system, you should be taking this in as a configuration variable and storing it for consistent use across multiple runs of your application (for example following a server reboot, you should not invalidate all of your user's JWTs) or across multiple distributed servers. This article gives an example of pulling in the secret key from an environment variable for rails.
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 am playing around with DEX and openldap. When I get a token back in my browser and put it into JWT debugger with the public key i generated, it doesn't verify the signature. I am trying to step through the code of DEX, but the debugging tools are not really working on my computer. I have resorted to log statements. I can't really find where I can observe the signing of the token to see if the program is using the keys i provided or not. Which function actually signs the token and how can I observe what key it uses to sign?
The key can be read from the DEX "keys" endpoint which can be obtained from:
http://your.dex.com/.well-known/openid-configuration
Typically, it would be something like:
http://your.dex.co/keys
After that, the public keys can be extracted using the following program:
https://play.golang.org/p/wVusucNGDI
One of those keys will be able to validate the token:
from jose import jwt
key = '''-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArT9AtIlC8MxhLYhz8ODH
...
+QIDAQAB
-----END PUBLIC KEY-----'''
encoded = 'eyJh...ocw'
audience = ''
if audience == "":
opts = {"verify_aud": False}
else:
opts = {}
opts['verify_at_hash'] = False
decoded = jwt.decode(encoded, key, audience=audience, options=opts)
print(decoded)
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