base on Seamless JWT authentication for Rails API ## DISCLAIMER
This project is not being maintained and I don't recommend using it in its current form.
As an alternative, I recommend using the [jwt](https://github.com/jwt/ruby-jwt) gem directly.
# knock
[![Gem Version](https://badge.fury.io/rb/knock.svg)](http://badge.fury.io/rb/knock)
[![Build Status](https://travis-ci.org/nsarno/knock.svg)](https://travis-ci.org/nsarno/knock)
[![Code Climate](https://codeclimate.com/github/nsarno/knock/badges/gpa.svg)](https://codeclimate.com/github/nsarno/knock)
Seamless JWT authentication for Rails API
## Description
Knock is an authentication solution for Rails API-only application based on JSON Web Tokens.
## Getting Started
### Installation
Add this line to your application's Gemfile:
```ruby
gem 'knock'
```
Then execute:
$ bundle install
### Requirements
Knock makes one assumption about your user model:
It must have an `authenticate` method, similar to the one added by [has_secure_password](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password).
```ruby
class User < ActiveRecord::Base
has_secure_password
end
```
Using `has_secure_password` is recommended, but you don't have to as long as your user model implements an `authenticate` instance method with the same behavior.
### Usage
Include the `Knock::Authenticable` module in your `ApplicationController`
```ruby
class ApplicationController < ActionController::API
include Knock::Authenticable
end
```
You can now protect your resources by calling `authenticate_user` as a before_action
inside your controllers:
```ruby
class SecuredController < ApplicationController
before_action :authenticate_user
def index
# etc...
end
# etc...
end
```
You can access the current user in your controller with `current_user`.
If no valid token is passed with the request, Knock will respond with:
```
head :unauthorized
```
You can modify this behaviour by overriding `unauthorized_entity` in your controller.
You also have access directly to `current_user` which will try to authenticate or return `nil`:
```ruby
def index
if current_user
# do something
else
# do something else
end
end
```
_Note: the `authenticate_user` method uses the `current_user` method. Overwriting `current_user` may cause unexpected behaviour._
You can do the exact same thing for any entity. E.g. for `Admin`, use `authenticate_admin` and `current_admin` instead.
If you're using a namespaced model, Knock won't be able to infer it automatically from the method name. Instead you can use `authenticate_for` directly like this:
```ruby
class ApplicationController < ActionController::Base
include Knock::Authenticable
private
def authenticate_v1_user
authenticate_for V1::User
end
end
```
```ruby
class SecuredController < ApplicationController
before_action :authenticate_v1_user
end
```
Then you get the current user by calling `current_v1_user` instead of `current_user`.
### Configuration
#### In the entity model
The entity model (e.g. `User`) can implement specific methods to provide
customization over different parts of the authentication process.
- **Find the entity when creating the token (when signing in)**
By default, Knock tries to find the entity by email. If you want to modify this
behaviour, implement within your entity model a class method `from_token_request`
that takes the request in argument.
E.g.
```ruby
class User < ActiveRecord::Base
def self.from_token_request request
# Returns a valid user, `nil` or raise `Knock.not_found_exception_class_name`
# e.g.
# email = request.params["auth"] && request.params["auth"]["email"]
# self.find_by email: email
end
end
```
- **Find the authenticated entity from the token payload (when authenticating a request)**
By default, Knock assumes the payload as a subject (`sub`) claim containing the entity's id
and calls `find` on the model. If you want to modify this behaviour, implement within
your entity model a class method `from_token_payload` that takes the
payload in argument.
E.g.
```ruby
class User < ActiveRecord::Base
def self.from_token_payload payload
# Returns a valid user, `nil` or raise
# e.g.
# self.find payload["sub"]
end
end
```
- **Modify the token payload**
By default the token payload contains the entity's id inside the subject (`sub`) claim.
If you want to modify this behaviour, implement within your entity model an instance method
`to_token_payload` that returns a hash representing the payload.
E.g.
```ruby
class User < ActiveRecord::Base
def to_token_payload
# Returns the payload as a hash
end
end
```
- **Token Lifetime**
By default the generated tokens will be valid, after generated, for 1 day.
You can change it in the Knock configuration file (config/knock.rb),
setting the desired lifetime:
E.g.
```ruby
Knock.token_lifetime = 3.hours
```
If you are generating tokens for more than one entity, you can pass
each lifetime in a hash, using the entities class names as keys, like:
E.g.
```ruby
# How long before a token is expired. If nil is provided,
# token will last forever.
Knock.token_lifetime = {
user: 1.day
admin: 30.minutes
}
```
#### In the initializer
Read [lib/knock.rb](https://github.com/nsarno/knock/blob/master/lib/knock.rb) to learn about all the possible configuration options and their default values.
You can create an initializer like in the example below:
Inside `config/initializers/knock.rb`
```ruby
Knock.setup do |config|
config.token_lifetime = 1.hour
# For Auth0
config.token_audience = -> { Rails.application.secrets.auth0_client_id }
config.token_secret_signature_key = -> { JWT.base64url_decode Rails.application.secrets.auth0_client_secret }
end
```
### Authenticating from a web or mobile application
Example request to get a token from your API:
```
POST /user_token
{"auth": {"email": "
[email protected]", "password": "secret"}}
```
Example response from the API:
```
201 Created
{"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"}
```
To make an authenticated request to your API, you need to pass the token via the request header:
```
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
GET /my_resources
```
Knock responds with a `404 Not Found` when the user cannot be found or the password is invalid. This is a security best practice to avoid giving away information about the existence or not of a particular user.
**NB:** HTTPS should always be enabled when sending a password or token in your request.
### Authenticated tests
To authenticate within your tests:
1. Create a valid token
2. Pass it in your request
e.g.
```ruby
class SecuredResourcesControllerTest < ActionDispatch::IntegrationTest
def authenticated_header
token = Knock::AuthToken.new(payload: { sub: users(:one).id }).token
{
'Authorization': "Bearer #{token}"
}
end
it 'responds successfully' do
get secured_resources_url, headers: authenticated_header
assert_response :success
end
end
```
#### Without ActiveRecord
If no ActiveRecord is used, then you will need to specify what Exception will be used when the user is not found with the given credentials.
```ruby
Knock.setup do |config|
# Exception Class
# ---------------
#
# Configure the Exception to be used (raised and rescued) for User Not Found.
# note: change this if ActiveRecord is not being used.
#
# Default:
config.not_found_exception_class_name = 'MyCustomException'
end
```
### Algorithms
The JWT spec supports different kind of cryptographic signing algorithms.
You can set `token_signature_algorithm` to use the one you want in the
initializer or do nothing and use the default one (HS256).
You can specify any of the algorithms supported by the
[jwt](https://github.com/jwt/ruby-jwt) gem.
If the algorithm you use requires a public key, you also need to set
`token_public_key` in the initializer.
## CORS
To enable cross-origin resource sharing, check out the [rack-cors](https://github.com/cyu/rack-cors) gem.
## Related links
- [10 things you should know about tokens](https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/)
## Contributing
1. Fork it ( https://github.com/nsarno/knock/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
## License
MIT
", Assign "at most 3 tags" to the expected json: {"id":"8246","tags":[]} "only from the tags list I provide: [{"id":77,"name":"3d"},{"id":89,"name":"agent"},{"id":17,"name":"ai"},{"id":54,"name":"algorithm"},{"id":24,"name":"api"},{"id":44,"name":"authentication"},{"id":3,"name":"aws"},{"id":27,"name":"backend"},{"id":60,"name":"benchmark"},{"id":72,"name":"best-practices"},{"id":39,"name":"bitcoin"},{"id":37,"name":"blockchain"},{"id":1,"name":"blog"},{"id":45,"name":"bundler"},{"id":58,"name":"cache"},{"id":21,"name":"chat"},{"id":49,"name":"cicd"},{"id":4,"name":"cli"},{"id":64,"name":"cloud-native"},{"id":48,"name":"cms"},{"id":61,"name":"compiler"},{"id":68,"name":"containerization"},{"id":92,"name":"crm"},{"id":34,"name":"data"},{"id":47,"name":"database"},{"id":8,"name":"declarative-gui "},{"id":9,"name":"deploy-tool"},{"id":53,"name":"desktop-app"},{"id":6,"name":"dev-exp-lib"},{"id":59,"name":"dev-tool"},{"id":13,"name":"ecommerce"},{"id":26,"name":"editor"},{"id":66,"name":"emulator"},{"id":62,"name":"filesystem"},{"id":80,"name":"finance"},{"id":15,"name":"firmware"},{"id":73,"name":"for-fun"},{"id":2,"name":"framework"},{"id":11,"name":"frontend"},{"id":22,"name":"game"},{"id":81,"name":"game-engine "},{"id":23,"name":"graphql"},{"id":84,"name":"gui"},{"id":91,"name":"http"},{"id":5,"name":"http-client"},{"id":51,"name":"iac"},{"id":30,"name":"ide"},{"id":78,"name":"iot"},{"id":40,"name":"json"},{"id":83,"name":"julian"},{"id":38,"name":"k8s"},{"id":31,"name":"language"},{"id":10,"name":"learning-resource"},{"id":33,"name":"lib"},{"id":41,"name":"linter"},{"id":28,"name":"lms"},{"id":16,"name":"logging"},{"id":76,"name":"low-code"},{"id":90,"name":"message-queue"},{"id":42,"name":"mobile-app"},{"id":18,"name":"monitoring"},{"id":36,"name":"networking"},{"id":7,"name":"node-version"},{"id":55,"name":"nosql"},{"id":57,"name":"observability"},{"id":46,"name":"orm"},{"id":52,"name":"os"},{"id":14,"name":"parser"},{"id":74,"name":"react"},{"id":82,"name":"real-time"},{"id":56,"name":"robot"},{"id":65,"name":"runtime"},{"id":32,"name":"sdk"},{"id":71,"name":"search"},{"id":63,"name":"secrets"},{"id":25,"name":"security"},{"id":85,"name":"server"},{"id":86,"name":"serverless"},{"id":70,"name":"storage"},{"id":75,"name":"system-design"},{"id":79,"name":"terminal"},{"id":29,"name":"testing"},{"id":12,"name":"ui"},{"id":50,"name":"ux"},{"id":88,"name":"video"},{"id":20,"name":"web-app"},{"id":35,"name":"web-server"},{"id":43,"name":"webassembly"},{"id":69,"name":"workflow"},{"id":87,"name":"yaml"}]" returns me the "expected json"