Skip to content

Instantly share code, notes, and snippets.

@dillonhafer
Last active January 22, 2021 21:36
Show Gist options
  • Save dillonhafer/a921de98585cfa36f8fb8a0beb18ce0b to your computer and use it in GitHub Desktop.
Save dillonhafer/a921de98585cfa36f8fb8a0beb18ce0b to your computer and use it in GitHub Desktop.
Ruby JWT
class AuthenticatedController < ApplicationController
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_user
private
def authenticate_user
head(:unauthorized) if current_user.blank?
end
def current_user
@current_user ||= find_user_from_token
end
def find_user_from_token
authenticate_with_http_token do |jwt_token|
email = JwtAuthenticator.decode(jwt_token)&.first&.dig("sub")
User.find_by(email: email)
end
end
end
jwt_ec_private_key: |
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIAnkqPr4ofWKuLAffpjHZS4RkC5hfUvh49OyP4Jn/lgCoAoGCCqGSM49
AwEHoUQDQgAEKTQeDjhs1MkjhxYiCe/GEEay47D2O7vFE07HUmabPv5J4InMH7Zs
MfxPvAX2XuL719PMUKm25brlNMj7Scptrg==
-----END EC PRIVATE KEY-----
openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-pair.pem
class JwtAuthenticator
ALGORITHM = "ES256"
ISSUER = "myorganizationname"
def self.encode(user)
new.encode(user)
end
def self.decode(token)
new.decode(token)
end
def decode(token)
JWT.decode(token, secret, true, {
iss: ISSUER,
verify_iss: true,
verify_iat: true,
algorithm: ALGORITHM
})
rescue JWT::InvalidIatError, JWT::ExpiredSignature, JWT::InvalidIssuerError, JWT::DecodeError
nil
end
def encode(user)
payload = generate_payload(user)
sign(payload)
end
private
def sign(payload)
headers = {
alg: ALGORITHM,
typ: "JWT"
}
JWT.encode(payload, secret, ALGORITHM, headers)
end
# Sign in for 24 hours
def generate_payload(user)
iat = Time.now.to_i
exp = 1.day.from_now.to_i
{
iss: ISSUER,
sub: user.email,
iat: iat,
exp: exp,
}
end
def secret
ec_pk = Rails.application.credentials.dig(:jwt_ec_private_key)
OpenSSL::PKey.read(ec_pk)
end
end
require "test_helper"
class JwtAuthenticatorTest < ActiveSupport::TestCase
def build_token(
user: build(:user),
exp: 5.minutes.from_now.to_i,
iat: 5.minutes.ago.to_i,
iss: JwtAuthenticator::ISSUER
)
payload = {
iss: iss,
sub: user.email,
iat: iat,
exp: exp,
roles: roles
}
JwtAuthenticator.new.send(:sign, payload)
end
test ".encode sets the email in the payload" do
user = build(:user)
token = JwtAuthenticator.encode(user)
payload = JwtAuthenticator.decode(token).first
assert payload["sub"] == user.email
end
test ".encode sets the issuer in the payload" do
user = build(:user)
token = JwtAuthenticator.encode(user)
payload = JwtAuthenticator.decode(token).first
assert payload["iss"] == "myorganizationname"
end
test ".encode sets the expiration to 1 day" do
user = build(:user)
token = JwtAuthenticator.encode(user)
payload = JwtAuthenticator.decode(token).first
assert payload["exp"] == 1.day.from_now.to_i
end
test "we build valid tokens" do
verified = JwtAuthenticator.decode(build_token)
assert verified.present?
end
test ".decode returns nil if issuer is incorrect" do
token = build_token(iss: "not issuer")
verified = JwtAuthenticator.decode(token)
assert verified.blank?
end
test ".decode returns nil if issued at is in the future" do
token = build_token(iat: 5.minutes.from_now.to_i)
verified = JwtAuthenticator.decode(token)
assert verified.blank?
end
test ".decode returns nil if expired at is in the past" do
token = build_token(exp: 5.minutes.ago.to_i)
verified = JwtAuthenticator.decode(token)
assert verified.blank?
end
end
class SessionsController < ApplicationController
def create
if (user = User.find_for_authentication(email: params[:email], password: params[:password]))
jwt = JwtAuthenticator.encode(user)
render json: {jwt: jwt}, status: 201
else
head :401
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment