-
Notifications
You must be signed in to change notification settings - Fork 231
JWT authentication ( after the commit b06aa7a ) #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9e33d10
5b3fe7c
270ea1c
ef86e9c
9bb9443
a70144a
edd5483
7f12bd4
3366df1
31fcc44
9f84e58
e6a3d4e
87de4bf
b06aa7a
4dde3c1
5a5e39a
9e02135
d66f47a
373a5f3
5488cbe
2c84e31
3d012de
25ab0a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
module Sorcery | ||
module Controller | ||
module Submodules | ||
module JwtAuth | ||
def self.included(base) | ||
base.send(:include, InstanceMethods) | ||
end | ||
|
||
module InstanceMethods | ||
# This method return generated token if user can be authenticated | ||
def jwt_auth(*credentials) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think slightly more appropriate name for the method would be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Method should also allow users to get login failure reason, similar to what |
||
user = user_class.authenticate(*credentials) | ||
if user | ||
now = Time.current | ||
default_payload = { | ||
sub: user.id, | ||
exp: (now + 3.days).to_i, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Token duration should be configurable. |
||
iat: now.to_i | ||
} | ||
|
||
payload = default_payload.merge Config.jwt_payload | ||
|
||
{ Config.jwt_user_data_key => default_payload, | ||
Config.jwt_auth_token_key => jwt_encode(payload) } | ||
end | ||
end | ||
|
||
# To be used as a before_action. | ||
def jwt_require_auth | ||
binding.pry | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess this is here by accident? :) |
||
@current_user = Config.jwt_set_user ? User.find(jwt_user_id) : jwt_user_data | ||
rescue JWT::DecodeError => e | ||
jwt_not_authenticated(message: e.message) && return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is trailing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
end | ||
|
||
# This method creating JWT token by payload | ||
def jwt_encode(payload) | ||
JWT.encode(payload, Config.jwt_secret_key, Config.jwt_algorithm) | ||
end | ||
|
||
# This method decoding JWT token | ||
# Return nil if token incorrect | ||
def jwt_decode(token) | ||
HashWithIndifferentAccess.new( | ||
JWT.decode(token, Config.jwt_secret_key)[0] | ||
) | ||
end | ||
|
||
# Take token from header, by key defined in config | ||
# With memoization | ||
def jwt_from_header | ||
@jwt_header_token ||= request.headers[Config.jwt_headers_key] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that more sensible default here would be to expect header value in one of the following formats: Further more, does it makes sense to allow users to define how should token be extracted from request? |
||
end | ||
|
||
# Return user data which decoded from token | ||
# With memoization | ||
def jwt_user_data(token = jwt_from_header) | ||
@jwt_user_data ||= jwt_decode(token) | ||
end | ||
|
||
# Return user id from user data if id present. | ||
# Else return nil | ||
def jwt_user_id | ||
jwt_user_data[:sub] | ||
end | ||
|
||
# This method called if user not authenticated | ||
def jwt_not_authenticated(message:) | ||
respond_to do |format| | ||
format.html { not_authenticated } | ||
format.json { | ||
render json: { | ||
"error": { | ||
"message": message, | ||
} | ||
}, | ||
status: :unauthorized | ||
} | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
require 'spec_helper' | ||
|
||
describe SorceryController, type: :controller do | ||
let!(:user) { double('user', id: 42) } | ||
before(:each) do | ||
request.env['HTTP_ACCEPT'] = "application/json" if ::Rails.version < '5.0.0' | ||
Timecop.freeze(Time.new(2019, 01, 14, 19, 00, 00)) | ||
end | ||
|
||
describe 'with jwt auth features' do | ||
let(:user_email) { '[email protected]' } | ||
let(:user_password) { 'testpass' } | ||
let(:auth_token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQyLCJleHAiOjE1NDc3MTkyMDAsImlhdCI6MTU0NzQ2MDAwMH0.QM5mTkYiDwI-10cEOq4b_bfrwe99BRuef6pnIB-jqIk' } | ||
let(:response_data) do | ||
{ | ||
user_data: { | ||
sub: user.id, | ||
exp: 1547719200, | ||
iat: 1547460000 | ||
}, | ||
auth_token: auth_token | ||
} | ||
end | ||
|
||
before(:all) do | ||
sorcery_reload!([:jwt_auth]) | ||
end | ||
|
||
describe '#jwt_auth' do | ||
context 'when success' do | ||
before do | ||
allow(User).to receive(:authenticate).with(user_email, user_password).and_return(user) | ||
|
||
post :test_jwt_auth, params: { email: user_email, password: user_password } | ||
end | ||
|
||
it 'assigns user to @token variable' do | ||
expect(assigns[:token]).to eq response_data | ||
end | ||
end | ||
|
||
context 'when fails' do | ||
before do | ||
allow(User).to receive(:authenticate).with(user_email, user_password).and_return(nil) | ||
|
||
post :test_jwt_auth, params: { email: user_email, password: user_password } | ||
end | ||
|
||
it 'assigns user to @token variable' do | ||
expect(assigns[:token]).to eq nil | ||
end | ||
end | ||
end | ||
|
||
describe '#jwt_require_auth' do | ||
context 'when success' do | ||
before do | ||
allow(User).to receive(:find).with(user.id).and_return(user) | ||
allow(user).to receive(:set_last_activity_at) | ||
end | ||
|
||
it 'does return 200' do | ||
request.headers.merge! Authorization: auth_token | ||
|
||
get :some_action_jwt, format: :json | ||
|
||
expect(response.status).to eq(200) | ||
end | ||
end | ||
|
||
context 'when fails' do | ||
let(:user_email) { '[email protected]' } | ||
let(:user_password) { 'testpass' } | ||
|
||
context 'without auth header' do | ||
it 'does return 401' do | ||
get :some_action_jwt, format: :json | ||
|
||
expect(response.status).to eq(401) | ||
expect(JSON.parse(response.body)["error"]["message"]).not_to be nil | ||
end | ||
end | ||
|
||
context 'with incorrect auth header' do | ||
let(:incorrect_header) { '123.123.123' } | ||
|
||
it 'does return 401' do | ||
request.headers.merge! Authorization: incorrect_header | ||
|
||
get :some_action_jwt, format: :json | ||
|
||
expect(response.status).to eq(401) | ||
expect(JSON.parse(response.body)["error"]["message"]).not_to be nil | ||
end | ||
end | ||
|
||
context "token is expired" do | ||
before do | ||
Timecop.freeze(Time.new(2099, 01, 14, 19, 00, 00)) | ||
request.headers.merge! Authorization: auth_token | ||
end | ||
|
||
it "does return 401" do | ||
get :some_action_jwt, format: :json | ||
|
||
expect(response.status).to eq(401) | ||
expect(JSON.parse(response.body)["error"]["message"]).not_to be nil | ||
end | ||
|
||
after do | ||
Timecop.return | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
after do | ||
Timecop.return | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this flag? Is there ever a case when
current_user
should not be an instance of a user model?