Skip to content

Instantly share code, notes, and snippets.

@pcorliss
Created October 2, 2020 16:59
Show Gist options
  • Save pcorliss/8cd8c24f25109bb3eb8ccb66d646d4b8 to your computer and use it in GitHub Desktop.
Save pcorliss/8cd8c24f25109bb3eb8ccb66d646d4b8 to your computer and use it in GitHub Desktop.
# Generate a random hex token with 256 bits, turn it into raw bytes
require 'jwt'
irb(main):013:0> token = rand(2**256).to_s(16)
irb(main):014:0> token
=> "625e3c0b7ed292952988ff403885d6a5718370a13625473984f152606a82e47e"
irb(main):015:0> [token].pack("H*")
=> "b^<\v~\xD2\x92\x95)\x88\xFF@8\x85\xD6\xA5q\x83p\xA16%G9\x84\xF1R`j\x82\xE4~"
irb(main):016:0> [token].pack("H*")
=> "b^<\v~\xD2\x92\x95)\x88\xFF@8\x85\xD6\xA5q\x83p\xA16%G9\x84\xF1R`j\x82\xE4~"
irb(main):017:0> [token].pack("H*").unpack("H*")
=> ["625e3c0b7ed292952988ff403885d6a5718370a13625473984f152606a82e47e"]
irb(main):018:0> [token].pack("H*").length
=> 32
irb(main):019:0> [token].pack("H*").length * 8
=> 256
irb(main):045:0> encoded = JWT.encode({:data => "test", :iss => Time.now}, bytes, "HS256")
irb(main):046:0> JWT.decode(encoded, bytes)
=> [{"data"=>"test", "iss"=>"2020-10-01 21:32:08 +0000"}, {"alg"=>"HS256"}]
iat == issued at (unix epoch seconds)
jti == unique id to prevent replays (not sure this is needed as we'd need to log and check that an ID isn't reused.
nbf == not before (Time.now.to_i - 2*60)
exp == expiration time (Time.now.to_i + 2*60)
irb(main):052:0> encoded = JWT.encode({:data => "test", :nbf => (Time.now.to_i - 2*60), :exp => (Time.now.to_i + 60)}, bytes, "HS256")
irb(main):053:0> JWT.decode(encoded, bytes)
=> [{"data"=>"test", "nbf"=>1601588435, "exp"=>1601588615}, {"alg"=>"HS256"}]
We'll need to include a key-id so we can pull the secret and decode the above.
Could theoretically just take the first 32-bits to 64-bits from the secret. But that would expose a component of the secret.
Probably better to just use the sha of the secret instead and include in the payload.
Client can gen it from the secret. Server has it in the payload.
Wait, we don't want the response to be signed with the client's secret. If we did that the client could write their own tokens without the licensing server.
Better to just use public key signing and include the pub key in the binary source code.
So Client -> Licensing is HS256 with 256-bit secret randomly generated, key-id (encoded or prepended into secret, could also be an email), and nbf and exp elements.
Licensing -> Client is RS256 and returns a JWT with a nbf, an exp, and that's it. The client verifies the JWT before running start and create commands.
To do this we'll need to create a licensing server with a few actions:
Enroll (create) - Takes an email, returns a key and formats it pretty in response.
Creates entries in a dynamo table: email, secret, expiration, created_at
Returns JSON wtih secret
Auth - Takes a JWT, signed by secret, including email (as key id).
Decodes jwt and extracts key id
Reads dynamo table for key id and pulls secret and expiration
Verifies expiration hasn't passed
Verifies JWT based on secret
Returns X days of auth not exceeding expiration. Signed with private key RS256
Future
May want to setup an audit table for these changes or make all changes immutable.
Extend (update) - Extends the expiration period, likely with a billing charge.
Quantity (update) - Add a quantity field to support site-licenses?
Change keys (update) - Generate a fresh secret
Change email (update) - Updates email, secret remains the same.
Stories
Create dynamo table, and enrollment lambda
Create auth lambda
Update client to read from config for key and key-id (email)
Update client to send auth request, receive response, and verify it.
Update client to support caching auth response
Update client to support a grace period
Release client 1.0.0
Release image 1.0.0
Look into CORS headers to allow easier website updates
Update website to require enrollment in 30-day trial, update docs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment