nsarno / knock Goto Github PK
View Code? Open in Web Editor NEWSeamless JWT authentication for Rails API
License: MIT License
Seamless JWT authentication for Rails API
License: MIT License
Knock looks like a great solution but I'm having issues. I know Knock is in there. The routes are registered and when I hit a secured page with no headers supplied, I get a 401. However, when I do supply the correct headers, I get this no method error. Any ideas what's up?
https://github.com/brandonpittman/knock_test/blob/master/app/controllers/users_controller.rb
Has anyone done a tutorial using Knock that you know of? I'm not having a lot of luck getting it up and running.
I updated to 1.4.0 and everything stopped working. I assume there are some changes I need to make but on the readme the setup is exactly the same. Any chance for explaining what has changed?
Using Knock I added token-based authentication to a rails app that already had session-based authentication. There was already a current_user
method that was included in ApplicationController which looked in the session for the logged in user. This method prevented Knock authentication working because Knock checks if it can get a result from current_user
to see if the user is authenticated, but it would call my current_user
method instead of it's own.
To work around this I had to change the name of my current_user
method to something else.
This sort of collision should be prevented using namespaces or something like that. At the very least it should be clearly documented that you can't have your own current_user
method in controllers using Knock. It would have saved me a lot of debugging time.
I'm new to JWT, but it looks better than just using the same API token forever.
I know the token expires after a day (by default), but does that mean the user will have to re-login again?
How does auth work between multiple devices? (esp in the case of a refresh token)
I think it would be nice to have an additional action alongside AuthToken#create
(maybe AuthToken#validate
) that simply returns success if the provided token is valid.
It can be useful when the client just wants to check if the token is valid and hasn't expired without having to decode it.
When a client tries to authenticate with bad credentials the server response is 404. The client then wont know if the credentials are bad or the sever is offline. Perhaps a 401 or 403 response would be better.
I need to know how to handle the logout operation, because the token will be valid up to the time mentioned in the expiry claim, but if a user log out from the application before the expiry time, the token should become invalid.
I was searching the way to revoke a jwt, i found some of the solutions here where someone said to use blacklisting mechanism and use of refresh token, i need to know whether these features are implemented in the knock or any other alternatives for invalidating a JWT.
I'm still wrapping my mind around how JWTs work and hoping for some guidance on how I might use Knock to help me in my situation.
I have an Authentication Server (Rails), a Client (Angular2), and a Data API (Rails). From the client I need to be able to authenticate with the auth server which would then give me permission to access the data api. I'm a little confused how to use the received JWT from the auth server with the data api. How does the data api decode the request that was signed by the auth server? Do I need to have the same 'secret' on both servers? I'm going for an SSO solution here so I'd like this to work with any *.mydomain.com. I assume Knock would be running on the Auth server since it has User accounts and I would need to implement something with straight JWT decoding on the Data API server.
I really appreciate any advice. Thanks!
Hello. In some cases AuthTokenController may need custom behaviour, but all the methods inside except create
action are private
. What about making them protected
? This will allow users add some custom logic. For example, adding expiration date or user identifier to response and etc
Hi,
executing :rails generate knock:install on my dev environment (Jruby 1.7.10 on Windows 7)
I got:
SyntaxError: C:/Ruby/jruby-1.7.10/lib/ruby/gems/shared/gems/knock-1.3.0/lib/knock.rb:12: syntax error,
unexpected tLPAREN_ARG self.current_user_from_token = -> (claims) { User.find claims['sub'] }
I'm opening a pull request for removing the spaces that cause problem.
Let me know if is useful.
I would like to use Knock for an API that allows the reuse of an email address against multiple different accounts. Just like Slack, the account would be determined by the subdomain that the user logs in from.
Is it possible to scope the current_user
to be found via an associated Account
object?
e.g. Users.where(account_id: $account_id).find_by! Knock.handle_attr => handle
I use grape. I hope knock will support grape.
Awesome gem.
I followed Adam's JWT tutorial in the past: http://adamalbrecht.com/2015/07/20/authentication-using-json-web-tokens-using-rails-and-react/ which builds a JWT from scratch without using gems.
Today, I tried this library, works like a gem (pun intended).
I have one issue though, hopefully it hasn't been asked before.
When I use cURL or Postman to make the request:
It doesn't matter whether I put in the HTTP Headers:
"Authorization: JWT <the_real_JWT_here>"
Or
"Authorization: Blah <the_real_JWT_here>"
The Rails server still lets it through....is this how it's suppose to work?
For example, this Github README uses Bearer <JWT_token>
. I would have thought a real JWT should enforce the Authorization header to have the word "JWT" instead of letting anything there.
Any ideas?
It would be nice if there was built in support for verifying that authentication is enforced to lock down an application much like verify_authorized
in Pundit.
Comments? Is this something you would like to see in a PR?
I'm thinking of something like:
module Knock::Authenticable
def current_user
@current_user ||= begin
token = params[:token] || request.headers['Authorization'].split.last
Knock::AuthToken.new(token: token).current_user
rescue
nil
end
end
def authenticate
@_authentication_performed = true
head :unauthorized unless current_user
end
def authentication_performed?
!!@_authentication_performed
end
def verify_authentication
raise Knock::AuthenticationNotPerformedError unless authentication_performed?
end
end
Usage:
class ApplicationController < ActionController::API
include Knock::Authenticable
after_action :verify_authentication, except: [:show, :index]
end
When posting to /knock/auth_token
with bad credentials it returns 404. Which is somewhat unexpected. 401 Not Authorized seems more appropriate. Comments?
Anyone try integrating this with Devise
yet? I want Devise
for it's user and password management, but need JWT
since I have an API.
lib/generators/templates/knock.rb:51
Believe you mean identify but wrote indentify
In authenticable.rb the @current_user
is retrieved like this
@current_user = User.find(payload['user_id'])
but since there is no User model in the lib it will force whoever who uses knock to have a model named User
and expect a claim named user_id
to be present in the JWT.
What I'd like to have is a way to define how the user is retrieved from my DB, e.g. in my app's config/knock.rb
I could have:
config.current_user_from_token = -> (claims) { User.find(claims['sub']) }
and then in authenticable.rb
@current_user = Knock.current_user_from_token.call(payload)
This will allow to fetch the user from any model using any claim in the JWT
Rails 5 has now been released and ActionController::TestCase is deprecated so the documentation on testing is outdated. As far as I can tell the request.env
object is no longer accessible. Instead, what I've been doing is something like this:
test_helper.rb
def authentication_token
Knock::AuthToken.new(payload: { sub: users(:one).id }).token
end
def authenticated_headers
{
'Authorization': "Bearer #{authentication_token}"
}
end
controller_test.rb
test "should respond successfully" do
get model_url(@model), headers: authenticated_headers
end
Maybe there's a better way to do this? Once a format has been decided on I can make a pull request, if you'd like.
Versions of rails and rails-api:
gem 'rails-api', '0.4.0'
gem 'rails', '~> 4.2.3'
Specs written with rspec get in some kind of conflict and throws the error that it cannot find the version of the engine version.
The path provided in the error is the same as the version file but somehow it cannot be loaded.
I changed my development machine. The same code that works fine on the old machine, now on the new machine gives this error:
uninitialized constant Knock
from this line in routes.rb
mount Knock::Engine => '/knock'
When I comment out that line I get
uninitialized constant ApplicationController::Knock
from this line in application_controller.rb
include Knock::Authenticable
The only difference I can think of from old machine to new is that this time I used rbenv instead of RVM, and also my ruby version is 2.3.0 instead of 2.2.3
Bundler could not find compatible versions for gem "rails":
In snapshot (Gemfile.lock):
rails (= 5.0.0.alpha)
In Gemfile:
knock (>= 0) ruby depends on
rails (~> 4.2) ruby
rails (>= 0) ruby
Any news about Rails 5 support?
Hi there.
I'm trying to implement Knock in a Rails 5 API, where I have everything namespaced (e.g. V1::User, V1::UsersController, etc..).
When mounting the knock authentication endpoint inside the namespace, like this, it works fine for exchanging username and password for a JWT:
constraints format: :json, defaults: { format: :json } do
namespace :v1 do
post :user_token, to: 'user_token#create'
resources :rooms
end
end
It doesn't work to go to the rooms endpoint afterwords, using the new JWT token. I get this exception: NameError: uninitialized constant User
. This is because I have this in my V1::BaseController:
before_action :authenticate_user
Here this is being constantized into the User model, so it's clear that a namespace is missing.
I then tried adding this to my controller instead:
before_action :authenticate_v1_user
But this gets transformed into a V1User
class, not as expected a V1::User
.
Is there a way that I'm not seeing for easily letting Knock know that I'm trying to get at a namespaced user?
Thanks in advance,
Emil
Would be nice be able to pass the token via query string. Something like
token = request.headers['Authorization'].split(' ').last || params[:token]
Interested?
Hi, how long the token is valid?
Is there a way to configure this time?
Thanks!!
Congratulations for the job with this gem!!!
I just download this example from Auth0 https://github.com/auth0/auth0-rubyonrailsapi-sample and for some reason if you pass verify=true it fail but there is not point if we dont berify
I traced it down to this line https://github.com/nsarno/knock/blob/master/app/model/knock/auth_token.rb#L9
I'm trying to write some request specs for my API but the following code results in a 401 status:
# spec/support/auth0.rb
def authenticate(user)
# user.auth0_id contains the actual user id from Auth0
Knock::AuthToken.new(payload: { sub: user.auth0_id }).token
end
# spec/requests/dashboard_spec.rb
require "rails_helper"
RSpec.describe 'GET /dashboard' do
let(:user) { create(:user) }
let(:auth_token) { authenticate(user) }
context 'when a user is logged in' do
it "returns a 200 response" do
get '/dashboard', headers: { 'Authorization' => "Bearer #{auth_token}" }
expect(response.status).to eq 200
end
end
end
Any idea on how to resolve this? By the way, i'm using the v2 branch with my updates found here
Is there a recommended method for generating permanent tokens?
I ask because I'm making both a mobile app and the Rails API it will use, and I only want the user to login once, after they install the app.
I noticed the default expiration period is config.token_lifetime = 1.day
with no obvious way to set this to infinity/nil. And after a cursory search I didn't see any mention of refresh tokens.
For now I've set it to an arbitrary long-term future date, 10.years
. And will store the token locally in the app, but is that a bad idea? Thanks
How would you provide authentication using email/password and facebook login?
This would give the implementer the freedom to implement authenticate
how they choose. I may want to pass additional information to authenticate
or provide a password-less authentication mechanism.
Granted, it looks like you're trying to adhere to the interface that has_secure_password
provides, but there are many use cases where this doesn't work. So making it a configurable option might make sense.
If you change default config.current_user_from_token
from
User.find(claims['sub'])
to some code not raising an exception like
User.where(id: claims['sub'])
or something more useful
User.find_or_create_by(auth_id: claims['sub'])
Then the following method used to check authenticity won't work anymore https://github.com/nsarno/knock/blob/master/lib/knock/authenticable.rb#L5-L9
Getting the following error (I followed the Auth0 manual)
LoadError (Unable to autoload constant Engine, expected /Users/mikhaildubov/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/knock-1.3.0/lib/knock/engine.rb to define it):
Any idea why this is happening? Appreciate any pointers.
Any recommendation on using Knock with the Rails 5 release candidates? Right now, the dependency is based on official releases (per PR #30). In the meantime, I've forked this repo and adjusted the dependency accordingly but wanted to know if you're planning an official release of Knock that supports the release candidates or not.
Hello,
I try to authenticate user in my tests. So far my spec looks like:
require 'rails_helper'
RSpec.describe 'GET /posts' do
subject { response }
let(:user) { create(:user) }
it 'authenticate user' do
authenticate
get '/posts'
expect(response.status).to eq 200
end
end
My User model has auth_id
column which stores auth0 user id. My code method is below.
def authenticate
token = Knock::AuthToken.new(payload: { sub: user.auth_id }).token
request.env['HTTP_AUTHORIZATION'] = "Bearer #{token}"
end
Every time I run spec I receive error about undefined method length
.
Failure/Error: authenticate
NoMethodError:
undefined method `length' for nil:NilClass
I will really appreciate it if someone can suggest me what's wrong with my code.
Thanks!
When I run bundle update I get:
In Gemfile:
rails (= 4.0.13)
knock was resolved to 1.0.0, which depends on
rails (~> 4.2)
I tried to force a lower version of knock, but this was "not found."
Going to the next version of Rails has many code-breaking changes within this app.
Is there a way to run knock on Rails 4.0.x ?
Commit 18b2cfb now requires prefixing a header-passed token with a Bearer
string. The previous release supported both forms, with and without the prefix. This breaks compatibility with all users who did not use prefixed tokens.
Please document this change in the CHANGELOG
and / or consider changing the regular expression to accommodate the old behaviour.
When testing the response is always unauthorized
with response.body equaling f
.
require 'rails_helper'
module V1
RSpec.describe DeliveriesController, type: :controller do
let(:auth_token) { authenticate }
context 'index' do
before do
@deliveries = [DateTime.now, (DateTime.now - 1), DateTime.now, (DateTime.now + 1), DateTime.now].each_with_object([]) do |time, stash|
stash << create(:delivery, eta: time)
end
@deliveries.each_with_index do |delivery, index|
delivery.locations << create(:location, name: "Location #{index}")
end
end
it 'give all deliveries that have an eta for the current day' do
get :index, headers: { 'Authorization' => "Bearer #{auth_token}" }
expected_count = 3
full_count = @deliveries.size
expect(json_data.size).to_not be(full_count)
expect(json_data.size).to be(expected_count)
end
context 'by location' do
it 'give all deliveries for a location for that day' do
location = @deliveries.last.locations.first
get :index, location_id: location.id, headers: { 'Authorization' => "Bearer #{auth_token}" }
expected_count = 1
full_count = @deliveries.size
puts response.body
expect(json_data.size).to_not be(full_count)
expect(json_data.size).to be(expected_count)
end
end
end
context 'CRUD' do
it 'show' do
delivery = create(:delivery)
get :show, id: delivery, headers: { 'Authorization' => "Bearer #{auth_token}" }
expect(response).to be_successful
expect(json_data).to include(:id, :type, :attributes)
end
it 'create' do
post :create, json_api_format { attributes_for(:delivery) }
expect(response).to be_successful
expect(json_data).to include(:id, :type, :attributes)
end
it 'update' do
delivery = create(:delivery)
old_cargo = delivery.cargo
expected_cargo = 'Updated Cargo'
delivery.cargo = expected_cargo
patch :update, json_api_format(delivery.id) { delivery.attributes }, headers: { 'Authorization' => "Bearer #{auth_token}" }
delivery.reload
expect(delivery.cargo).to_not eq(old_cargo)
expect(delivery.cargo).to eq(expected_cargo)
end
end
end
end
module V1
class DeliveriesController < ApplicationController
before_action :authenticate
def index
if location
render json: location.deliveries.today, status: :ok
else
render json: Delivery.today, include: '*', status: :ok
end
end
def show
delivery = Delivery.find delivery_id
render json: delivery, include: '*', status: :ok
end
def create
delivery = Delivery.create delivery_params
render json: delivery, include: '*', status: :created
end
def update
delivery = Delivery.find delivery_id
delivery.update delivery_params
render json: delivery, status: :ok
end
private
def location
@location ||= Location.find_by(id: location_params[:location_id])
end
def location_params
params.permit(:location_id)
end
def delivery_id
params[:id]
end
def delivery_params
ActiveModelSerializers::Deserialization
.jsonapi_parse(params, only: [:cargo, :cargo_quantity, :notes, :arrive_at, :escort,
:priority, :processing_time, :eta, :icon_url, :escort_badge_number])
end
end
end
def authenticate
token = Knock::AuthToken.new(payload: { sub: create(:driver).id }).token
request.env['HTTP_AUTHORIZATION'] = "Bearer #{token}"
end
config.current_user_from_handle = -> (handle) { Driver.find_by! Knock.handle_attr => handle }
config.current_user_from_token = -> (claims) { Driver.find claims['sub'] }
I use rails 5.0.0.beta3 and rbenv and used this guide
With rbenv i created a new rails api project rbenv exec rails _5.0.0.beta3_ new levented --api
After that i scaffold my user bin/rails g scaffold user name:string family_name:string nick_name:string email:string
routes.rb
Rails.application.routes.draw do
mount Knock::Engine => '/knock'
resources :users
end
application_controller.rb
class ApplicationController < ActionController::API
include Knock::Authenticable
end
users_controller.rb
class UsersController < ApplicationController
before_action :set_user, only: [:show, :update, :destroy]
# GET /users
def index
@users = User.all
render json: @users
end
end
A [GET] to /users prints the following error
ActionController::RoutingError (uninitialized constant ApplicationController::Knock):
app/controllers/application_controller.rb:2:in `<class:ApplicationController>'
app/controllers/application_controller.rb:1:in `<top (required)>'
app/controllers/users_controller.rb:1:in `<top (required)>'
Started GET "/users" for 127.0.0.1 at 2016-03-15 20:57:53 +0100
What did i wrong?
by defualt knock returns 'json' in response format heade. I would like to return "application/vnd.api+json
I tried to costumiz eit with an after_action:
response.content_type = "application/vnd.api+json"
but it doesn't work.
I creaed y own contrller for this, but will be great to add it as an option.
Thanks!
After several days of debugging and even nuking my repo and starting over, I still can't seem to get any authorization with a JWT token I'm passing from an iOS app authenticated with Auth0 Lock. After updating to Knock 1.5, I've narrowed it down to self.from_token_payload
not creating a new user. Any assistance would be greatly appreciated!
Here's my repo: https://github.com/brandonmanson/happening-api
Hi there, just wondering if you thoughts on adding additional claims to the token payload. I'd like to add some additional user information to it but it looks like I'd have to subclass the AuthTokenController (which will be easier once #47 is fixed).
Thanks and cheers
I found config.token_secret_signature_key was broken by ruby-jwt API updates ...
# config.token_secret_signature_key = -> { JWT.base64url_decode Rails.application.secrets.auth0_client_secret }
Latest ruby-jwt(v1.5.3) completely removes base64url_decode
method from JWT module(at this PR)
BTW, We may use these work around as a temporary solution :)
(we may have better solution for this problem...)
require 'base64'
# extracted from original [method](http://www.rubydoc.info/github/jwt/ruby-jwt/JWT.base64url_decode)
config.token_secret_signature_key = -> {
secret = Rails.application.secrets.auth0_client_secret
secret += '=' * (4 - secret.length.modulo(4))
Base64.decode64(secret.tr('-_', '+/'))
}
Hey, I see you specify for us to raise an ActiveRecord::RecordNotFound for the authentication process but I am having trouble doing so as I do not know where to do it since the controller for generating tokens based on user credentials is the one you provide with your gem.
What I would like to do is send back with the status a response with a custom message like "Wrong Credentials" for example when such exception occurs. I imagine I should use a rescue but I do not know how or where, thanks.
Hi,
I've noticed that this gem does not have warden as a dependency. I presume this is intentional? Is this gem reimplementing part of warden functionality?
I'm wondering what would it take to integrate knock as an authentication strategy into warden
If not, I'd like to add that functionality. I love how lightweight gem is, but it would be easier to deal with if I could just pull chunks of it into my project. I'll make a PR if this doesn't exist
Why this is on the readme and It doesn't exist in code?
rails generate knock:token_controller user
my terminal says that could not find generator... (Of course, knock installed)
I'm looking at providing an API that will have multiple token_audience/token_secret_signature_key values depending on the particular account that a user is associated with. Right now, it looks like the only way I could deal with this scenario is to set those config values manually for each request. However, I'm concerned that if I'm changing things at a global module level, I might run into an issue where multiple app requests running in parallel are overwriting the config values and stomping on each other (maybe only if I have a multi-threaded app server?). If I could, I would be happy to pass those values directly to the Knock::AuthToken model, but that's not how it works currently.
Anyway, just curious if you have an idea of how to go about this. Thanks!
Hello I'm trying skip athenticate method this way:
skip_before_filter :authenticate, only: :half_hourly
and
skip_before_filter :authenticate!, only: :half_hourly
but this is not working.
Can you help me?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.