Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

JWT token authentication with devise and rails

License

NotificationsYou must be signed in to change notification settings

waiting-for-dev/devise-jwt

Repository files navigation

Gem VersionBuild StatusCode ClimateTest Coverage

devise-jwt is aDevise extension which usesJWT tokens for user authentication. It followssecure by default principle.

This gem is just a replacement for cookies when these can't be used. As withcookies, adevise-jwt token will mandatorily have an expirationtime. If you need that your users never sign out, you will be better off with asolution using refresh tokens, like some implementation of OAuth2.

You can read about which security concerns this library takes into account and about JWT generic secure usage in the following series of posts:

devise-jwt is just a thin layer on top ofwarden-jwt_auth that configures it to be used out of the box with Devise and Rails.

Upgrade notes

v0.7.0

Since version v0.7.0Blacklist revocation strategy has been renamed toDenylist whileWhitelist has been renamed toAllowlist.

ForDenylist, you only need to update theinclude line you're using in your revocation strategy model:

# include Devise::JWT::RevocationStrategies::Blacklist # beforeincludeDevise::JWT::RevocationStrategies::Denylist

ForAllowlist, you need to update theinclude line you're using in your user model:

# include Devise::JWT::RevocationStrategies::Whitelist # beforeincludeDevise::JWT::RevocationStrategies::Allowlist

You also have to rename yourWhitelistedJwt model toAllowlistedJwt, renamemodel/whitelisted_jwt.rb tomodel/allowlisted_jwt.rb and change the underlying database table toallowlisted_jwts (or configure the model to keep using the old name).

Installation

Add this line to your application's Gemfile:

gem'devise-jwt'

And then execute:

$ bundle

Or install it yourself as:

$ gem install devise-jwt

Usage

First, you need to configure Devise to work in an API application. You can follow the instructions in this project wiki pageConfiguring Devise for APIs (you are more than welcome to improve them).

Secret key configuration

You have to configure the secret key that will be used to sign generated tokens. You can do it in the Devise initializer:

Devise.setupdo |config|# ...config.jwtdo |jwt|jwt.secret=ENV['DEVISE_JWT_SECRET_KEY']endend

If you are using Encrypted Credentials (Rails 5.2+), you can store the secret key inconfig/credentials.yml.enc.

Open your credentials editor usingbin/rails credentials:edit and adddevise_jwt_secret_key.

Note you may need to set$EDITOR depending on your specific environment.

# Other secrets...# Used as the base secret for Devise JWTdevise_jwt_secret_key:abc...xyz

Add the following to the Devise initializer.

Devise.setupdo |config|# ...config.jwtdo |jwt|jwt.secret=Rails.application.credentials.devise_jwt_secret_key!endend

Important: You are encouraged to use a secret different than your applicationsecret_key_base. It is quite possible that some other component of your system is already using it. If several components share the same secret key, chances that a vulnerability in one of them has a wider impact increase. In rails, generating new secrets is as easy asrails secret. Also, never share your secrets pushing it to a remote repository, you are better off using an environment variable like in the example.

Currently, HS256 algorithm is the one in use. You may configure a matching secret and algorithm name to use a different one (seeruby-jwt to see which are supported):

Devise.setupdo |config|# ...config.jwtdo |jwt|jwt.secret=OpenSSL::PKey::RSA.new(Rails.application.credentials.devise_jwt_secret_key!)jwt.algorithm=Rails.application.credentials.devise_jwt_algorithm!endend

If the algorithm is asymmetric (e.g. RS256) which necessitates a different decoding secret, configure thedecoding_secret setting as well:

Devise.setupdo |config|# ...config.jwtdo |jwt|jwt.secret=OpenSSL::PKey::RSA.new(Rails.application.credentials.devise_jwt_private_key!)jwt.decoding_secret=OpenSSL::PKey::RSA.new(Rails.application.credentials.devise_jwt_public_key!)jwt.algorithm='RS256'# or some other asymmetric algorithmendend

Model configuration

You have to tell which user models you want to be able to authenticate with JWT tokens. For them, the authentication process will be like this:

  • A user authenticates through Devise create session request (for example, using the standard:database_authenticatable module).
  • If the authentication succeeds, a JWT token is dispatched to the client in theAuthorization response header, with formatBearer #{token} (tokens are also dispatched on a successful sign up).
  • The client can use this token to authenticate following requests for the same user, providing it in theAuthorization request header, also with formatBearer #{token}
  • When the client visits Devise destroy session request, the token is revoked.

Seerequest_formats configuration option if you are using paths with a format segment (like.json) in order to use it properly.

As you see, unlike other JWT authentication libraries, it is expected that tokens will be revoked by the server. I wrote aboutwhy I think JWT revocation is needed and useful.

An example configuration:

classUser <ApplicationRecorddevise:database_authenticatable,:jwt_authenticatable,jwt_revocation_strategy:Denylistend

If you need to add something to the JWT payload, you can do it by defining ajwt_payload method in the user model. It must return aHash. For instance:

defjwt_payload{'foo'=>'bar'}end

You can add a hook methodon_jwt_dispatch on the user model. It is executed when a token dispatched for that user instance, and it takestoken andpayload as parameters.

defon_jwt_dispatch(token,payload)do_something(token,payload)end

Note: if you are making cross-domain requests, make sure that you addAuthorization header to the list of allowed request headers and exposed response headers. You can use something likerack-cors for that, for example:

config.middleware.insert_before0,Rack::Corsdoallowdoorigins'http://your.frontend.domain.com'resource'/api/*',headers:%w(Authorization),methods::any,expose:%w(Authorization),max_age:600endend

Session storage caveat

If you are working with a Rails application that has session storage enabledand a default Devise setup, chances are the same origin requests will beauthenticated from the session regardless of a token being present in theheaders or not.

This is so because of the following default Devise workflow:

  • When a user signs in with:database_authenticatable strategy, the user isstored in the session unless one of the following conditions is met:
    • Session is disabled.
    • Deviseconfig.skip_session_storage includes:params_auth.
    • Rails Request forgeryprotectionhandles an unverified request (but this is usually deactivated for APIrequests).
  • Warden (the engine below Devise), authenticates any request that the user hasin the session without requiring a strategy (:jwt_authenticatablein our case).

So, if you want to avoid this caveat you have five options:

  • Disable the session. If you are developing an API, you probably don't needit. In order to disable it, changeconfig/initializers/session_store.rb to:

    Rails.application.config.session_store:disabled

    Notice that if you created the application with the--api flag you alreadyhave the session disabled.

  • If you still need the session for any other purpose, disable:database_authenticatable user storage. Inconfig/initializers/devise.rb:

    config.skip_session_storage=[:http_auth,:params_auth]
  • If you are using Devise for another model (e.g.AdminUser) and doesn't wantto disable session storage for Devise entirely, you can disable it on aper-model basis:

    classUser <ApplicationRecorddevise:database_authenticatable#, your other enabled modules...self.skip_session_storage=[:http_auth,:params_auth]end
  • If you need the session for some of the controllers, you are able to disable it atthe controller level for those controllers which don't need it:

    classAdminsController <ApplicationControllerbefore_action:drop_session_cookieprivatedefdrop_session_cookierequest.session_options[:skip]=trueend
  • As the last option you can tell Devise to not store the user in the Warden sessionif you override default DeviseSessionsController with your own one, and passstore: false attribute to thesign_in,sign_in_and_redirect,bypass_sign_inmethods:

    sign_inuser,store:false

Revocation strategies

devise-jwt comes with three revocation strategies out of the box. Some of them are implementations of what is discussed in the blog postJWT Revocation Strategies, where I also talk about their pros and cons.

JTIMatcher

Here, the model class acts as the revocation strategy. It needs a new string column namedjti to be added to the user.jti stands for JWT ID, and it is a standard claim meant to uniquely identify a token.

It works like the following:

  • When a token is dispatched for a user, thejti claim is taken from thejti column in the model (which has been initialized when the record has been created).
  • At every authenticated action, the incoming tokenjti claim is matched against thejti column for that user. The authentication only succeeds if they are the same.
  • When the user requests to sign out itsjti column changes, so that provided token won't be valid anymore.

In order to use it, you need to add thejti column to the user model. So, you have to set something like the following in a migration:

defchangeadd_column:users,:jti,:string,null:falseadd_index:users,:jti,unique:true# If you already have user records, you will need to initialize its `jti` column before setting it to not nullable. Your migration will look this way:# add_column :users, :jti, :string# User.all.each { |user| user.update_column(:jti, SecureRandom.uuid) }# change_column_null :users, :jti, false# add_index :users, :jti, unique: trueend

Important: You are encouraged to set a unique index in thejti column. This way we can be sure at the database level that there aren't two valid tokens with samejti at the same time.

Then, you have to add the strategy to the model class and configure it accordingly:

classUser <ApplicationRecordincludeDevise::JWT::RevocationStrategies::JTIMatcherdevise:database_authenticatable,:jwt_authenticatable,jwt_revocation_strategy:selfend

Be aware that this strategy makes uses ofjwt_payload method in the user model, so if you need to use it don't forget to callsuper:

defjwt_payloadsuper.merge('foo'=>'bar')end

Denylist

In this strategy, a database table is used as a list of revoked JWT tokens. Thejti claim, which uniquely identifies a token, is persisted. Theexp claim is also stored to allow the clean-up of stale tokens.

In order to use it, you need to create the denylist table in a migration:

defchangecreate_table:jwt_denylistdo |t|t.string:jti,null:falset.datetime:exp,null:falseendadd_index:jwt_denylist,:jtiend

For performance reasons, it is better if thejti column is an index.

Note: if you used the denylist strategy before version 0.4.0 you may not have the fieldexp. If not, run the following migration:

classAddExpirationTimeToJWTDenylist <ActiveRecord::Migrationdefchangeadd_column:jwt_denylist,:exp,:datetime,null:falseendend

Then, you need to create the corresponding model and include the strategy:

classJwtDenylist <ApplicationRecordincludeDevise::JWT::RevocationStrategies::Denylistself.table_name='jwt_denylist'end

Last, configure the user model to use it:

classUser <ApplicationRecorddevise:database_authenticatable,:jwt_authenticatable,jwt_revocation_strategy:JwtDenylistend

Allowlist

Here, the model itself also acts as a revocation strategy, but it needs to havea one-to-many association with another table which stores the tokens (in facttheirjti claim, which uniquely identifies them) that are valid for each user record.

The workflow is as the following:

  • Once a token is dispatched for a user, itsjti claim is stored in theassociated table.
  • At every authentication, the incoming tokenjti is matched against all thejti associated to that user. The authentication only succeeds if one ofthem matches.
  • On a sign out, the tokenjti is deleted from the associated table.

In fact, besides thejti claim, theaud claim is also stored and matched atevery authentication. This, together with theaud_headerconfiguration parameter, can be used to differentiate between clients ordevices for the same user.

Theexp claim is also stored to allow the clean-up of staled tokens.

In order to use it, you have to create the associated table and model.The association table must be calledallowlisted_jwts:

defchangecreate_table:allowlisted_jwtsdo |t|t.string:jti,null:falset.string:aud# If you want to leverage the `aud` claim, add to it a `NOT NULL` constraint:# t.string :aud, null: falset.datetime:exp,null:falset.references:your_user_table,foreign_key:{on_delete::cascade},null:falseendadd_index:allowlisted_jwts,:jti,unique:trueend

Important: You are encouraged to set a unique index in thejti column. This way we can be sure at the database level that there aren't two valid tokens with the samejti at the same time. Definingforeign_key: { on_delete: :cascade }, null: false ont.references :your_user_table helps to keep referential integrity of your database.

And then, the model:

classAllowlistedJwt <ApplicationRecordend

Finally, include the strategy in the model and configure it:

classUser <ApplicationRecordincludeDevise::JWT::RevocationStrategies::Allowlistdevise:database_authenticatable,:jwt_authenticatable,jwt_revocation_strategy:selfend

Be aware that this strategy makes uses ofon_jwt_dispatch method in the user model, so if you need to use it don't forget to callsuper:

defon_jwt_dispatch(token,payload)superdo_something(token,payload)end

Null strategy

Anull object pattern strategy, which does not revoke tokens, is provided out of the box just in case you are absolutely sure you don't need token revocation. It is recommendednot to use it.

classUser <ApplicationRecorddevise:database_authenticatable,:jwt_authenticatable,jwt_revocation_strategy:Devise::JWT::RevocationStrategies::Nullend

Custom strategies

You can also implement your own strategies. They just need to implement two methods:jwt_revoked? andrevoke_jwt, both of them accept the JWT payload and the user record as parameters, in this order.

For instance:

moduleMyCustomStrategydefself.jwt_revoked?(payload,user)# Does something to check whether the JWT token is revoked for given userenddefself.revoke_jwt(payload,user)# Does something to revoke the JWT token for given userendendclassUser <ApplicationRecorddevise:database_authenticatable,:jwt_authenticatable,jwt_revocation_strategy:MyCustomStrategyend

Testing

Models configured with:jwt_authenticatable usually won't be retrieved fromthe session. For this reason,sign_in Devise testing helper methods won'twork as expected.

What you need to do to authenticate test environment requests is thesame that you will do in production: to provide a valid token in theAuthorization header (in the form ofBearer #{token}) at every request.

There are two ways you can get a valid token:

  • Inspecting theAuthorization response header after a valid sign in request.
  • Manually creating it.

The first option tests the real workflow of your application, but it can slowthings if you perform it at every test.

For the second option, a test helper is provided in order to add theAuthorization name/value pair to given request headers. You can use it as inthe following example:

# First, require the helper modulerequire'devise/jwt/test_helpers'# ...it'tests something'douser=fetch_my_user()headers={'Accept'=>'application/json','Content-Type'=>'application/json'}# This will add a valid token for `user` in the `Authorization` headerauth_headers=Devise::JWT::TestHelpers.auth_headers(headers,user)get'/my/end_point',headers:auth_headersexpect_something()end

Usually you will wrap this in your own test helper.

Configuration reference

This library can be configured callingjwt on Devise config object:

Devise.setupdo |config|config.jwtdo |jwt|# ...endend

secret

Secret key is used to sign generated JWT tokens. You must set it.

rotation_secret

Allow rotating secrets. Set a new value tosecret and copy the old secret torotation_secret.

expiration_time

Number of seconds while a JWT is valid after its generation. After that, it won't be valid anymore, even if it hasn't been revoked.

Defaults to 3600 seconds (1 hour).

dispatch_requests

Besides the create session one, there are additional requests where JWT tokens should be dispatched.

It must be a bidimensional array, each item being an array of two elements: the request method and a regular expression that must match the request path.

For example:

jwt.dispatch_requests=[['POST',%r{^/dispatch_path_1$}],['GET',%r{^/dispatch_path_2$}],]

Important: You are encouraged to delimit your regular expression with^ and$ to avoid unintentional matches.

revocation_requests

Besides the destroy session one, there are additional requests where JWT tokens should be revoked.

It must be a bidimensional array, each item being an array of two elements: the request method and a regular expression that must match the request path.

For example:

jwt.revocation_requests=[['DELETE',%r{^/revocation_path_1$}],['GET',%r{^/revocation_path_2$}],]

Important: You are encouraged to delimit your regular expression with^ and$ to avoid unintentional matches.

request_formats

Request formats that must be processed (in order to dispatch or revoke tokens).

It must be a hash of Devise scopes as keys and an array of request formats asvalues. When a scope is not present or if it has a nil item, requests withoutformat will be taken into account.

For example, with following configuration,user scope would dispatch andrevoke tokens injson requests (as in/users/sign_in.json), whileadmin_user would do it inxml and with no format (as in/admin_user/sign_in.xml and/admin_user/sign_in).

jwt.request_formats={user:[:json],admin_user:[nil,:xml]}

By default, only requests without format are processed.

token_header

Request/response header which will transmit the JWT token.

Defaults to 'Authorization'

issuer

Expected issuer claim. If present, it will be checked against the incomingtoken issuer claim and authorization will be skipped if they don't match.

Defaults to nil.

aud_header

Request header which content will be stored to theaud claim in the payload.

It is used to validate whether an incoming token was originally issued to thesame client, checking ifaud and theaud_header header value match. If youdon't want to differentiate between clients, you don't need to provide thatheader.

Important: Be aware that this workflow is not bullet proof. In somescenarios a user can handcraft the request headers, therefore being able toimpersonate any client. In such cases you could need something more robust,like an OAuth workflow with client id and client secret.

Defaults toJWT_AUD.

token_header

Request header containing the token in the format ofBearer #{token}.

Defaults toAuthorization.

issuer

Theissuer claim in the token.

If present, it will be checked against the incoming token issuer claim andauthorization will be skipped if they don't match.

Defaults tonil.

jwt.issuer='http://myapp.com'

Development

There are docker and docker-compose files configured to create a development environment for this gem. So, if you use Docker you only need to run:

docker-compose up -d

An then, for example:

docker-compose exec app rspec

Contributing

Bug reports and pull requests are welcome on GitHub athttps://github.com/waiting-for-dev/devise-jwt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to theContributor Covenant code of conduct.

Release Policy

devise-jwt follows the principles ofsemantic versioning.

License

The gem is available as open source under the terms of theMIT License.

About

JWT token authentication with devise and rails

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors35


[8]ページ先頭

©2009-2025 Movatter.jp