Last active
January 22, 2021 21:36
-
-
Save dillonhafer/a921de98585cfa36f8fb8a0beb18ce0b to your computer and use it in GitHub Desktop.
Ruby JWT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jwt_ec_private_key: | | |
-----BEGIN EC PRIVATE KEY----- | |
MHcCAQEEIAnkqPr4ofWKuLAffpjHZS4RkC5hfUvh49OyP4Jn/lgCoAoGCCqGSM49 | |
AwEHoUQDQgAEKTQeDjhs1MkjhxYiCe/GEEay47D2O7vFE07HUmabPv5J4InMH7Zs | |
MfxPvAX2XuL719PMUKm25brlNMj7Scptrg== | |
-----END EC PRIVATE KEY----- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
openssl ecparam -genkey -name prime256v1 -noout -out ec256-key-pair.pem |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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