Code Monkey home page Code Monkey logo

jwt_sessions's Introduction

jwt_sessions

Gem Version Maintainability Codacy Badge Build Status

XSS/CSRF safe JWT auth designed for SPA

Table of Contents

Synopsis

The primary goal of this gem is to provide configurable, manageable, and safe stateful sessions based on JSON Web Tokens.

The gem stores JWT based sessions on the backend (currently, Redis and memory stores are supported), making it possible to manage sessions, reset passwords and logout users in a reliable and secure way.

It is designed to be framework agnostic, yet easily integrable, and Rails integration is available out of the box.

The core concept behind jwt_sessions is that each session is represented by a pair of tokens: access and refresh. The session store is used to handle CSRF checks and prevent refresh token hijacking. Both tokens have configurable expiration times but in general the refresh token is supposed to have a longer lifespan than the access token. The access token is used to retrieve secure resources and the refresh token is used to renew the access token once it has expired. The default token store uses Redis.

All tokens are encoded and decoded by ruby-jwt gem. Its reserved claim names are supported and it can configure claim checks and cryptographic signing algorithms supported by it. jwt_sessions itself uses ext claim and HS256 signing by default.

Installation

Put this line in your Gemfile:

gem "jwt_sessions"

Then run:

bundle install

Getting Started

You should configure an algorithm and specify the signing key. By default the gem uses the HS256 signing algorithm.

JWTSessions.signing_key = "secret"

Authorization mixin provides helper methods which are used to retrieve the access and refresh tokens from incoming requests and verify the CSRF token if needed. It assumes that a token can be found either in a cookie or in a header (cookie and header names are configurable). It tries to retrieve the token from headers first and then from cookies (CSRF check included) if the header check fails.

Creating a session

Each token contains a payload with custom session info. The payload is a regular Ruby hash.
Usually, it contains a user ID or other data which help identify the current user but the payload can be an empty hash as well.

> payload = { user_id: user.id }
=> {:user_id=>1}

Generate the session with a custom payload. By default the same payload is sewn into the session's access and refresh tokens.

> session = JWTSessions::Session.new(payload: payload)
=> #<JWTSessions::Session:0x00007fbe2cce9ea0...>

Sometimes it makes sense to keep different data within the payloads of the access and refresh tokens.
The access token may contain rich data including user settings, etc., while the appropriate refresh token will include only the bare minimum which will be required to reconstruct a payload for the new access token during refresh.

session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)

Now we can call login method on the session to retrieve a set of tokens.

> session.login
=> {:csrf=>"BmhxDRW5NAEIx...",
    :access=>"eyJhbGciOiJIUzI1NiJ9...",
    :access_expires_at=>"..."
    :refresh=>"eyJhbGciOiJIUzI1NiJ9...",
    :refresh_expires_at=>"..."}

Access/refresh tokens automatically contain expiration time in their payload. Yet expiration times are also added to the output just in case.
The token's payload will be available in the controllers once the access (or refresh) token is authorized.

To perform the refresh do:

> session.refresh(refresh_token)
=> {:csrf=>"+pk2SQrXHRo1iV1x4O...",
    :access=>"eyJhbGciOiJIUzI1...",
    :access_expires_at=>"..."}

Available JWTSessions::Session.new options:

  • payload: a hash object with session data which will be included into an access token payload. Default is an empty hash.
  • refresh_payload: a hash object with session data which will be included into a refresh token payload. Default is the value of the access payload.
  • access_claims: a hash object with JWT claims which will be validated within the access token payload. For example, JWTSessions::Session.new(payload: { user_id: 1, aud: ['admin'], verify_aud: true }) means that the token can be used only by "admin" audience. Also, the endpoint can automatically validate claims instead. See token_claims method.
  • refresh_claims: a hash object with JWT claims which will be validated within the refresh token payload.
  • namespace: a string object which helps to group sessions by a custom criteria. For example, sessions can be grouped by user ID, making it possible to logout the user from all devices. More info Sessions Namespace.
  • refresh_by_access_allowed: a boolean value. Default is false. It links access and refresh tokens (adds refresh token ID to access payload), making it possible to perform a session refresh by the last expired access token. See Refresh with access token.
  • access_exp: an integer value. Contains an access token expiration time in seconds. The value overrides global settings. See Expiration time.
  • refresh_exp: an integer value. Contains a refresh token expiration time in seconds. The value overrides global settings. See Expiration time.

Helper methods within Authorization mixin:

  • authorize_access_request!: validates access token within the request.
  • authorize_refresh_request!: validates refresh token within the request.
  • found_token: a raw token found within the request.
  • payload: a decoded token's payload. Returns an empty hash in case the token is absent in the request headers/cookies.
  • claimless_payload: a decoded token's payload without claims validation (can be used for checking data of an expired token).
  • token_claims: the method should be defined by a developer and is expected to return a hash-like object with claims to be validated within a token's payload.

Rails integration

Include JWTSessions::RailsAuthorization in your controllers and add JWTSessions::Errors::Unauthorized exception handling if needed.

class ApplicationController < ActionController::API
  include JWTSessions::RailsAuthorization
  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized

  private

  def not_authorized
    render json: { error: "Not authorized" }, status: :unauthorized
  end
end

Specify a signing key for JSON Web Tokens in config/initializers/jwt_session.rb
It is advisable to store the key itself in a secure way, f.e. within app credentials.

JWTSessions.algorithm = "HS256"
JWTSessions.signing_key = Rails.application.credentials.secret_jwt_signing_key

Most of the algorithms require private and public keys to sign a token. However, HMAC requires only a single key and you can use the signing_key shortcut to sign the token. For other algorithms you must specify private and public keys separately.

JWTSessions.algorithm   = "RS256"
JWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)
JWTSessions.public_key  = JWTSessions.private_key.public_key

You can build a login controller to receive access, refresh and CSRF tokens in exchange for the user's login/password.
Refresh controller allows you to get a new access token using the refresh token after access is expired. \

Here is an example of a simple login controller, which returns a set of tokens as a plain JSON response.
It is also possible to set tokens as cookies in the response instead.

class LoginController < ApplicationController
  def create
    user = User.find_by!(email: params[:email])
    if user.authenticate(params[:password])
      payload = { user_id: user.id }
      session = JWTSessions::Session.new(payload: payload)
      render json: session.login
    else
      render json: "Invalid user", status: :unauthorized
    end
  end
end

Now you can build a refresh endpoint. To protect the endpoint use the before_action authorize_refresh_request!.
The endpoint itself should return a renewed access token.

class RefreshController < ApplicationController
  before_action :authorize_refresh_request!

  def create
    session = JWTSessions::Session.new(payload: access_payload)
    render json: session.refresh(found_token)
  end

  def access_payload
    # payload here stands for refresh token payload
    build_access_payload_based_on_refresh(payload)
  end
end

In the above example, found_token is a token fetched from request headers or cookies. In the context of RefreshController it is a refresh token.
The refresh request with headers must include X-Refresh-Token (header name is configurable) with the refresh token.

X-Refresh-Token: eyJhbGciOiJIUzI1NiJ9...
POST /refresh

When there are login and refresh endpoints, you can protect the rest of your secured controllers with before_action :authorize_access_request!.

class UsersController < ApplicationController
  before_action :authorize_access_request!

  def index
    ...
  end

  def show
    ...
  end
end

Headers must include Authorization: Bearer with access token.

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
GET /users

The payload method is available to fetch encoded data from the token.

def current_user
  @current_user ||= User.find(payload["user_id"])
end

Methods authorize_refresh_request! and authorize_access_request! will always try to fetch the tokens from the headers first and then from the cookies. For the cases when an endpoint must support only one specific token transport the following authorization methods can be used instead:

authorize_by_access_cookie!
authorize_by_access_header!
authorize_by_refresh_cookie!
authorize_by_refresh_header!

Non-Rails usage

You must include JWTSessions::Authorization module to your auth class and within it implement the following methods:

  1. request_headers
def request_headers
  # must return hash-like object with request headers
end
  1. request_cookies
def request_cookies
  # must return hash-like object with request cookies
end
  1. request_method
def request_method
  # must return current request verb as a string in upcase, f.e. 'GET', 'HEAD', 'POST', 'PATCH', etc
end

Example Sinatra app.
NOTE: Rack updates HTTP headers by using the HTTP_ prefix, upcasing and underscores for the sake of simplicity. JWTSessions token header names are converted to the rack-style in this example.

require "sinatra/base"

JWTSessions.access_header = "authorization"
JWTSessions.refresh_header = "x_refresh_token"
JWTSessions.csrf_header = "x_csrf_token"
JWTSessions.signing_key = "secret key"

class SimpleApp < Sinatra::Base
  include JWTSessions::Authorization

  def request_headers
    env.inject({}) { |acc, (k,v)| acc[$1.downcase] = v if k =~ /^http_(.*)/i; acc }
  end

  def request_cookies
    request.cookies
  end

  def request_method
    request.request_method
  end

  before do
    content_type "application/json"
  end

  post "/login" do
    access_payload = { key: "access value" }
    refresh_payload = { key: "refresh value" }
    session = JWTSessions::Session.new(payload: access_payload, refresh_payload: refresh_payload)
    session.login.to_json
  end

  # POST /refresh
  # x_refresh_token: ...
  post "/refresh" do
    authorize_refresh_request!
    access_payload = { key: "reloaded access value" }
    session = JWTSessions::Session.new(payload: access_payload, refresh_payload: payload)
    session.refresh(found_token).to_json
  end

  # GET /payload
  # authorization: Bearer ...
  get "/payload" do
    authorize_access_request!
    payload.to_json
  end

  # ...
end

Configuration

List of configurable settings with their default values.

Token store

In order to configure a token store you should set up a store adapter in a following way: JWTSessions.token_store = :redis, { redis_url: 'redis://127.0.0.1:6379/0' } (options can be omitted). Currently supported stores are :redis and :memory. Please note, that if you want to use Redis as a store then you should have redis-client gem listed in your Gemfile. If you do not configure the adapter explicitly, this gem will try to load redis-client and use it. Otherwise it will fall back to a memory adapter.

Memory store only accepts a prefix (used for Redis db keys). Here is a default configuration for Redis:

JWTSessions.token_store = :redis, {
  redis_host: "127.0.0.1",
  redis_port: "6379",
  redis_db_name: "0",
  token_prefix: "jwt_",
  pool_size: Integer(ENV.fetch("RAILS_MAX_THREADS", 5))
}

On default pool_size is set to 5. Override it with the value of max number of parallel redis connections in your app.

You can also provide a Redis URL instead:

JWTSessions.token_store = :redis, { redis_url: "redis://localhost:6397" }

NOTE: if REDIS_URL environment variable is set it is used automatically.

SSL, timeout, reconnect, etc. redis settings are supported:

JWTSessions.token_store = :redis, {
  read_timeout: 1.5,
  reconnect_attempts: 10,
  ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
}

If you already have a configured Redis client, you can pass it among the options to reduce opened connections to a Redis server:

JWTSessions.token_store = :redis, {redis_client: redis_pool}
JWT signature
JWTSessions.algorithm = "HS256"

You need to specify a secret to use for HMAC as this setting does not have a default value.

JWTSessions.signing_key = "secret"

If you are using another algorithm like RSA/ECDSA/EDDSA you should specify private and public keys.

JWTSessions.private_key = "abcd"
JWTSessions.public_key  = "efjh"

NOTE: ED25519 and HS512256 require rbnacl installation in order to make it work.

jwt_sessions only uses exp claim by default when it decodes tokens and you can specify which additional claims to use by setting jwt_options. You can also specify leeway to account for clock skew.

JWTSessions.jwt_options[:verify_iss] = true
JWTSessions.jwt_options[:verify_sub] = true
JWTSessions.jwt_options[:verify_iat] = true
JWTSessions.jwt_options[:verify_aud] = true
JWTSessions.jwt_options[:leeway]     = 30 # seconds

To pass options like sub, aud, iss, or leeways you should specify a method called token_claims in your controller.

class UsersController < ApplicationController
  before_action :authorize_access_request!

  def token_claims
    {
      aud: ["admin", "staff"],
      verify_aud: true, # can be used locally instead of a global setting
      exp_leeway: 15 # will be used instead of default leeway only for exp claim
    }
  end
end

Claims are also supported by JWTSessions::Session and you can pass access_claims and refresh_claims options in the initializer.

Request headers and cookies names

Default request headers/cookies names can be reconfigured.

JWTSessions.access_header  = "Authorization"
JWTSessions.access_cookie  = "jwt_access"
JWTSessions.refresh_header = "X-Refresh-Token"
JWTSessions.refresh_cookie = "jwt_refresh"
JWTSessions.csrf_header    = "X-CSRF-Token"
Expiration time

Access token must have a short life span, while refresh tokens can be stored for a longer time period.

JWTSessions.access_exp_time = 3600 # 1 hour in seconds
JWTSessions.refresh_exp_time = 604800 # 1 week in seconds

It is defined globally, but can be overridden on a session level. See JWTSessions::Session.new options for more info.

Exceptions

JWTSessions::Errors::Error - base class, all possible exceptions are inhereted from it.
JWTSessions::Errors::Malconfigured - some required gem settings are empty, or methods are not implemented.
JWTSessions::Errors::InvalidPayload - token's payload doesn't contain required keys or they are invalid.
JWTSessions::Errors::Unauthorized - token can't be decoded or JWT claims are invalid.
JWTSessions::Errors::ClaimsVerification - JWT claims are invalid (inherited from JWTSessions::Errors::Unauthorized).
JWTSessions::Errors::Expired - token is expired (inherited from JWTSessions::Errors::ClaimsVerification).

CSRF and cookies

When you use cookies as your tokens transport it becomes vulnerable to CSRF. That is why both the login and refresh methods of the Session class produce CSRF tokens for you. Authorization mixin expects that this token is sent with all requests except GET and HEAD in a header specified among this gem's settings (X-CSRF-Token by default). Verification will be done automatically and the Authorization exception will be raised in case of a mismatch between the token from the header and the one stored in the session.
Although you do not need to mitigate BREACH attacks it is still possible to generate a new masked token with the access token.

session = JWTSessions::Session.new
session.masked_csrf(access_token)
Refresh with access token

Sometimes it is not secure enough to store the refresh tokens in web / JS clients.
This is why you have the option to only use an access token and to not pass the refresh token to the client at all.
Session accepts refresh_by_access_allowed: true setting, which links the access token to the corresponding refresh token.

Example Rails login controller, which passes an access token token via cookies and renders CSRF:

class LoginController < ApplicationController
  def create
    user = User.find_by!(email: params[:email])
    if user.authenticate(params[:password])

      payload = { user_id: user.id }
      session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
      tokens = session.login
      response.set_cookie(JWTSessions.access_cookie,
                          value: tokens[:access],
                          httponly: true,
                          secure: Rails.env.production?)

      render json: { csrf: tokens[:csrf] }
    else
      render json: "Invalid email or password", status: :unauthorized
    end
  end
end

The gem provides the ability to refresh the session by access token.

session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
tokens  = session.refresh_by_access_payload

In case of token forgery and successful refresh performed by an attacker the original user will have to logout.
To protect the endpoint use the before_action authorize_refresh_by_access_request!.
Refresh should be performed once the access token is already expired and we need to use the claimless_payload method in order to skip JWT expiration validation (and other claims) in order to proceed.

Optionally refresh_by_access_payload accepts a block argument (the same way refresh method does). The block will be called if the refresh action is performed before the access token is expired. Thereby it's possible to prohibit users from making refresh calls while their access token is still active.

tokens = session.refresh_by_access_payload do
  # here goes malicious activity alert
  raise JWTSessions::Errors::Unauthorized, "Refresh action is performed before the expiration of the access token."
end

Example Rails refresh by access controller with cookies as token transport:

class RefreshController < ApplicationController
  before_action :authorize_refresh_by_access_request!

  def create
    session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
    tokens  = session.refresh_by_access_payload
    response.set_cookie(JWTSessions.access_cookie,
                        value: tokens[:access],
                        httponly: true,
                        secure: Rails.env.production?)

    render json: { csrf: tokens[:csrf] }
  end
end

For the cases when an endpoint must support only one specific token transport the following auth methods can be used instead:

authorize_refresh_by_access_cookie!
authorize_refresh_by_access_header!

Refresh token hijack protection

There is a security recommendation regarding the usage of refresh tokens: only perform refresh when an access token expires.
Sessions are always defined by a pair of tokens and there cannot be multiple access tokens for a single refresh token. Simultaneous usage of the refresh token by multiple users can be easily noticed as refresh will be performed before the expiration of the access token by one of the users. As a result, refresh method of the Session class supports an optional block as one of its arguments which will be executed only in case of refresh being performed before the expiration of the access token.

session = JwtSessions::Session.new(payload: payload)
session.refresh(refresh_token) { |refresh_token_uid, access_token_expiration| ... }

Flush Sessions

Flush a session by its refresh token. The method returns number of flushed sessions:

session = JWTSessions::Session.new
tokens = session.login
session.flush_by_token(tokens[:refresh]) # => 1

Flush a session by its access token:

session = JWTSessions::Session.new(refresh_by_access_allowed: true)
tokens = session.login
session.flush_by_access_payload
# or
session = JWTSessions::Session.new(refresh_by_access_allowed: true, payload: payload)
session.flush_by_access_payload

Or by refresh token UID:

session.flush_by_uid(uid) # => 1
Sessions namespace

It's possible to group sessions by custom namespaces:

session = JWTSessions::Session.new(namespace: "account-1")

Selectively flush sessions by namespace:

session = JWTSessions::Session.new(namespace: "ie-sessions")
session.flush_namespaced # will flush all sessions which belong to the same namespace

Selectively flush one single session inside a namespace by its access token:

session = JWTSessions::Session.new(namespace: "ie-sessions", payload: payload)
session.flush_by_access_payload # will flush a specific session which belongs to an existing namespace

Flush access tokens only:

session = JWTSessions::Session.new(namespace: "ie-sessions")
session.flush_namespaced_access_tokens # will flush all access tokens which belong to the same namespace, but will keep refresh tokens

Force flush of all app sessions:

JWTSessions::Session.flush_all
Logout

To logout you need to remove both access and refresh tokens from the store.
Flush sessions methods can be used to perform logout.
Refresh token or refresh token UID is required to flush a session.
To logout with an access token, refresh_by_access_allowed should be set to true on access token creation. If logout by access token is allowed it is recommended to ignore the expiration claim and to allow to logout with the expired access token.

Examples

Rails API
Sinatra API

You can use a mixed approach for the cases when you would like to store an access token in localStorage and refresh token in HTTP-only secure cookies.
Rails controllers setup example:

class LoginController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])

      payload = { user_id: user.id, role: user.role, permissions: user.permissions }
      refresh_payload = { user_id: user.id }
      session = JWTSessions::Session.new(payload: payload, refresh_payload: refresh_payload)
      tokens = session.login
      response.set_cookie(JWTSessions.refresh_cookie,
                          value: tokens[:refresh],
                          httponly: true,
                          secure: Rails.env.production?)

      render json: { access: tokens[:access], csrf: tokens[:csrf] }
    else
      render json: "Cannot login", status: :unauthorized
    end
  end
end

class RefreshController < ApplicationController
  before_action :authorize_refresh_request!

  def create
    tokens = JWTSessions::Session.new(payload: access_payload).refresh(found_token)
    render json: { access: tokens[:access], csrf: tokens[:csrf] }
  end

  def access_payload
    user = User.find_by!(email: payload["user_id"])
    { user_id: user.id, role: user.role, permissions: user.permissions }
  end
end

class ResourcesController < ApplicationController
  before_action :authorize_access_request!
  before_action :validate_role_and_permissions_from_payload

  # ...
end

Contributing

Fork & Pull Request.
RbNaCl and sodium cryptographic library are required for tests.

For MacOS see these instructions.
For example, with Homebrew:

brew install libsodium

License

MIT

jwt_sessions's People

Contributors

alanhala avatar alexktzk avatar bibendi avatar dmitrytsepelev avatar doutatsu avatar gabo-cs avatar holywalley avatar joelhoelting avatar palkan avatar petergoldstein avatar shettytejas avatar stanislove avatar starbeast avatar sumbach avatar suzumejakku avatar tuwukee avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

jwt_sessions's Issues

Unsuported algorithm when using ruby-jwt 2.2.3

after updating to ruby-jwt 2.2.3 and using the following configuration:

JWTSessions.algorithm   = "RS256"
JWTSessions.private_key = OpenSSL::PKey::RSA.generate(2048)
JWTSessions.public_key  = JWTSessions.private_key.public_key
JWTSessions.access_exp_time = 1.day

I'm getting the following error:

jwt_sessions-2.5.2/lib/jwt_sessions.rb:159:in `block in supported_algos': [JWT::Algos::Hmac, JWT::Algos::Ecdsa, JWT::Algos::Rsa, JWT::Algos::Eddsa, JWT::Algos::Ps, JWT::Algos::None, JWT::Algos::Unsupported] is not a class/module (TypeError)

which is weird because the the ruby-jwt was only a patch change from 2.2.1

Guest session

Are planned to implement guest sessions?
For example, how I can implement 2FA? At this moment a new session creating on each request.
Thanks.

Nil JSON web token

No matter what request comes from my frontend (VueJS), I always get JWTSessions::Errors::Unauthorized (Nil JSON web token): in Rails. I have made sure to follow everything to the tee from this Readme and tutorials.

Returns 401 when concurrent

When access expires, if the user clicks quickly on the web page and triggers two requests at the same time, in this case, the new access can be refresh normally, but when the new access is used to request resources, it returns 401

I wrote a demo project to solve this problem๏ผšhttps://github.com/activeliang/try_jwt_session

To simulate the above situation, I wrote the following code:

# first login
@tokens = JSON.parse RestClient.post 'http://localhost:3000/login', name: 'user1', password: '123456'

# method
def refresh_access_and_get_posts
  new_tokens = JSON.parse RestClient.post 'http://localhost:3000/refresh', {}, { 'X-Refresh-Token': @tokens['tokens']['refresh'] }
  posts = JSON.parse RestClient.get 'http://localhost:3000/posts', { Authorization: "Bearer #{new_tokens['access']}"}
end

# it works normal
10.times {
  refresh_access_and_get_posts
}

# 401 will be returned when get posts
10.times{
  Thread.new {
    refresh_access_and_get_posts
  }
}

Is it because I did something wrong?

concept working with headers

jwt_sessions can take tokens from the header (method token_from_headers).
In your concept, does the server set headers for the FE? Does the FE have to get tokens from cookies and add it to the header?

After logout user , I can also get the current_user in rails

I got current_user by this methods:

class ApplicationController < ActionController::API
...

private
  def current_user
    token = request_cookies[JWTSessions.cookie_by(:access)]
    if token.present?
      decode_options = { algorithm: JWTSessions.algorithm }
      payload = JWT.decode(token, JWTSessions.public_key, false, decode_options)
      @current_user = User.find_by_slug(payload[0]['slug'].to_s)
    else
      @current_user = nil
    end 
  end
end

Related issues: How to get current_user when skip authorize_access_request!

But now, I have a problem to logout current_user:

# logout method
class SigninController < ApplicationController
  before_action :authorize_access_request!, only: [:destroy]
...
 def destroy
    current_user&.update_attribute(:login_boolean, false)
    session = JWTSessions::Session.new(payload: payload)
    session.flush_by_access_payload
    delete_cookies
    render json: {status: 1, message: "Logout Success"}
  end
end

Use this method๏ผŒit seems current_user JWTSessions do not destroy, I can also get current_user in rails.

How should I to change the destroy method?

Thanks very much.

Storing sessions in DB instead of in Redis

Thank you for this gem! I'm interested in using this gem for authentication for my Rails app, but I'm hesitant to add another dependency (Redis) if not needed. To my knowledge, using the memory-backed adapter in production isn't recommended.

Is there any support for using a database like Postgres instead for storing session data? AFAIK this is what most web frameworks do. Any reason for not supporting this?

Thanks again for this gem! Happy to use Redis if I have no other option at all.

Add a changelog

With the latest gem bump, I realised there was no changelog. It's a bit tough to go through commits, to understand exactly what changed. Especially if there are any deprecations and such. Would be really useful if we could get a dedicated changelog

check authentication status?

what's the advised way to check if a user is authenticated? I'm trying to set up private routes for react router and thought maybe just adding an endpoint that checks if current_user is present would work but I end up getting a 401 because of the jwt_sessions module added to application controller

Also if I plan on having routes that don't require authentication, how would I bypass it? My only thought is to make a separate AuthController for when I need authentication

Managing multiple sessions

let's say that i have multiple client frontend (web & android), and i want to manage my session (when, what devices that logged in to this account) like picture below (picture from mega site)

image

is it possible to manage my sessions?
and probably called it using current_user.sessions or something like that

thanks.

build_access_payload_based_on_refresh method does not exist

In the README, there is an example for a refresh endpoint like this:

class RefreshController < ApplicationController
  before_action :authorize_refresh_request!

  def create
    session = JWTSessions::Session.new(payload: access_payload)
    render json: session.refresh(found_token)
  end

  def access_payload
    # payload here stands for refresh token payload
    build_access_payload_based_on_refresh(payload)
  end
end

build_access_payload_based_on_refresh does not exist in the codebase. https://github.com/tuwukee/jwt_sessions/search?q=build_access_payload_based_on_refresh&type=code

I can update, but should the refresh endpoint look something like this instead?

class RefreshController < ApplicationController
  before_action :authorize_refresh_request!

  def create
    session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
    render json: session.refresh(found_token)
  end
end

Work with Devise?

Amazing work @tuwukee!

Is possible to use jwt_session with devise for authentication? If not, do you think about create some modules like devise?

Thanks for all your hard work! ๐Ÿ’ช

Logout returning 401 Unauthorized (Rails 6.0.0)

Hello Yulia.
Thank you very much for this great work.
All is working fine (rails 5.2), but in rails 6.0.0 I have 401/Unauthorized error when trying to logout (destroy session in SigninController) and/or refresh token.
I tried to use auth headers instead of cookies, upgrade dummy_app of this awesome gem to rails 6.0.0 - all tests are green, but logout issue still exists. Could you please help me with it? Many thanks.

Here is my code:
`class SigninController < ApplicationController
before_action :authorize_access_request!, only: [:destroy]
def destroy
session = JWTSessions::Session.new(payload: payload)
session.flush_by_access_payload
render json: { notice: :ok }
end
end

class RefreshController < ApplicationController
before_action :authorize_refresh_by_access_request!

def create
session = JWTSessions::Session.new(payload: claimless_payload,
refresh_by_access_allowed: true,
namespace: "user_#{claimless_payload['user_id']}")
tokens = session.refresh_by_access_payload do
raise JWTSessions::Errors::Unauthorized, 'Malicious activity detected'
end
response.set_cookie(JWTSessions.access_cookie,
value: tokens[:access],
httponly: true,
secure: Rails.env.production?)

render json: { csrf: tokens[:csrf] }

end
end`

Session .flush_all method appears to flush one at a time

Rails 6.0.0.rc1, Ruby 2.6.3

Initializer

JWTSessions.encryption_key = '12345'
JWTSessions.token_store = :redis, {
  redis_host: "127.0.0.1",
  redis_port: "6379",
  redis_db_name: "xyz_sessions",
  token_prefix: "jwt_"
}

I created a bunch of sessions for one "user" and tried to flush them all with the rails console - got this output:

[18] pry(main)> JWTSessions::Session.flush_all
=> 1
[19] pry(main)> JWTSessions::Session.flush_all
=> 1
[20] pry(main)> JWTSessions::Session.flush_all
=> 1
[21] pry(main)> JWTSessions::Session.flush_all
=> 1
[22] pry(main)> JWTSessions::Session.flush_all
=> 1
[23] pry(main)> JWTSessions::Session.flush_all
=> 1
[24] pry(main)> JWTSessions::Session.flush_all
=> 1
[25] pry(main)> JWTSessions::Session.flush_all
=> 1
[26] pry(main)> JWTSessions::Session.flush_all
=> 1
[27] pry(main)> JWTSessions::Session.flush_all
=> 1
[28] pry(main)> JWTSessions::Session.flush_all
=> 1
[29] pry(main)> JWTSessions::Session.flush_all
=> 0

Looks like there may be an issue with flushing multiple tokens?

Allow to set expiration on Session level

It would be more convenient to have ability to pass expiration times to JWTSessions::Session.new to override global settings, for cases like "Remember me" functionality, etc.

Double exp in token payload

Hi, first of all, thanks for your work!

I found a small issue, it's not significant, however, I decided to report it.

I found that in decoded version of access and refresh tokens exp key is doubled.

Base64.decode64("eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODcyOTYyOTAsImxlYXJuZXJfaWQiOjEsInVpZCI6ImE5NzJjYzIwLTNkOGItNGMwNS05ODNmLTM3ZWYyMjg0ZTBlMSIsImV4cCI6MTU4NzI5NjI5MH0
.QleaH_SUElIJn8oLxPYMm8eAT6o18LEFQAlCax5DOMo".split('.')[1])

# => "{\"exp\":1587296290,\"learner_id\":1,\"uid\":\"a972cc20-3d8b-4c05-983f-37ef2284e0e1\",\"exp\":1587296290}"

The reason is quite predictable, string vs symbol keys.

Here
https://github.com/tuwukee/jwt_sessions/blob/master/lib/jwt_sessions/token.rb#L34

meta is

{ exp:  JWTSessions.access_expiration }

but exp has been already added to payload here:
https://github.com/tuwukee/jwt_sessions/blob/master/lib/jwt_sessions/access_token.rb#L11 (for access token)

and here
https://github.com/tuwukee/jwt_sessions/blob/master/lib/jwt_sessions/refresh_token.rb#L19 (for refresh one)

in a form of string keys.

This way exp_payload at the end looks like:

{"exp"=>1587296290, "learner_id"=>1, "uid"=>"a972cc20-3d8b-4c05-983f-37ef2284e0e1", :exp=>1587296290}

Probably I'm missing something.

I'm glad to open a PR if you think this should be fixed.

Independent configuration for different environments

Hi, Yulia! Thank you so much for your awesome work ๐Ÿ‘๐Ÿ‘๐Ÿ‘ Is there a way to use independent jwt_sessions configuration for different environments. It would be nice to use for development/production env:

JWTSessions.token_store = :redis

and for test env:

JWTSessions.token_store = :memory

Cheers!

Refresh token with namespace

ruby 2.6.3p62
jwt_sessions (2.4.2)
rails (6.0.0)

Hi, i'm facing JWTSessions::Errors::Unauthorized exception when trying to refresh namespaced session. I use namespace to be able to destroy all user's session, so my namespace looks like user-{user_id}.
Here's my code for session creation:

      session = JWTSessions::Session.new(
        payload: payload,
        refresh_payload: payload,
        namespace: namespace, # 'user-42' for example
        refresh_exp: refresh_exp
      )

Code in controller:

class RefreshesController < ApiController
  before_action :authorize_refresh_request!

  def create
    ...
  end
end

The exception comes from authorize_refresh_request! method. I traced it back to it's origin and found that it doesn't use namespace to retrieve token from store. Well it does, but there is no way to pass it down, and would be useless anyway taking into account that my namespace has data stored inside of token (id) in it.

So when session existance is checked at /lib/jwt_sessions/authorization.rb:134

  invalid_authorization unless session_exists?(token_type)

We have session instantiated and called session_exists? at lib/jwt_sessions/authorization.rb:86 without any namespace

def session_exists?(token_type)
  JWTSessions::Session.new.session_exists?(found_token, token_type)
end

Then it calls refresh_token_data, that calls retrieve_refresh_token so we end up at lib/jwt_sessions/session.rb:181 where namespace passed down to RefreshToken.find is nil.

def retrieve_refresh_token(uid)
  @_refresh = RefreshToken.find(uid, store, namespace)
end

And my key that looks like jwt__user-42_refresh_dd45acf2-1f8d-47b6-9711-bac3749ae130 is never found. Access token is not namespaced so it never has such an issue.

My suggestion is to include namespace into the token's payload and read it from there when checking for token existence/instantiating session.

How to set refresh access payload based on expired access payload

Hello, first thanks for making the great library @tuwukee!

I am using the library with Rails but struggling with the refresh implementation.

My problem is that my original authentication payload includes a user_id (like the example in the README), however, in my refresh method I can't figure out how to access the user_id, so I can set it again in the new payload.

Based on the example in the README:

  1. found_token is the refresh token?
  2. How would I create the method build_access_payload_based_on_refresh to include the user_id from the former access token into the new payload?
  3. Do I need to be using refresh_payload and include the user_id in there as well?
class RefreshController < ApplicationController
  before_action :authorize_refresh_request!

  def create
    session = JWTSessions::Session.new(payload: access_payload)
    render json: session.refresh(found_token)
  end

  def access_payload
    # payload here stands for refresh token payload
    build_access_payload_based_on_refresh(payload)
  end
end

Front end consumer questions / Front-End Example

Hi there!

I'm trying to implement this gem in a rails API with a Next.js driven front end. I believe I have a backend implementation working, with endpoints to login, refresh, and flush.

But I have some questions about the expected behavior on the front end. I'm trying to make a robust session that can share tabs, and persist after all tabs / browser is closed.

The Problem
The wisdom I seem to see around the web is that localStorage and sessionStorage are insecure places to store JWTs, as are non-httpOnly cookies. Which leaves httpOnly cookies and browser memory as the two main places to keep them.

Is this what you would recommend? This seems to complicate several front end details, like how to know when to refresh (That data can't be read by JS at all if in an httpOnly cookie, and if in memory, it both won't be shared by tabs and will be lost after closing all tabs), or how to resume (same as before: that data isn't necessarily available).

Are there any examples of a front-end consuming tan jwt_sessions powered API that you can provide? Am I missing something here?

Thanks in advance!

error in logout - "access token payload does not contain refresh uid"

I'm trying to follow the guidance re: logout. My logout method consists of:

  def destroy
    puts payload
    session = JWTSessions::Session.new(payload: payload)
    session.flush_by_access_payload
    render json: :ok
  end

When I attempt to logout I get the following error:

JWTSessions::Errors::InvalidPayload: Access token payload does not contain refresh uid

From this, I take it that I'm supposed to put the refresh token in my payload when the user signs in but I can't figure out how to get that in there, as the payload gets generated before I call Session.new and inject the payload.

I'm passing the JWT in the Authorization header. Is there anything I'm supposed to send in the body?

Setting a cookie for a specific domain

Can I provide a jwt token for a specific domain?
I would like to set a cookie after I was authenticated with a third party provider like google. Coming back from google I am not able to set the cookie correctly.

Malconfigured error

When trying to set up a new rails app with this JWTSessions, I get an unexpected JWTSessions::Errors::Malconfigured error:
JWTSessions::Errors::Malconfigured (private_key is not specified)

This is raised at session.login in my controller:

def create
      user = User.find_by!(email: params[:email])
      if user.valid_password?(params[:password])
        payload = {
          user_id: user.id,
        }
        session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
        tokens = session.login
        response.set_cookie(
          JWTSessions.access_cookie,
          value: tokens[:access],
          httponly: true,
          secure: Rails.env.production?
        )
...

I first thought that maybe some algorithm like rsa was specified, but I want to use SHA256. Checking my config/intializers/jwt_sessions.rb:

JWTSessions.algorithm = "HS256"
JWTSessions.encryption_key = Rails.application.secrets.secret_key_base

This should also be correct. Any help is appreciated!

[Ruby 2.7] New version ruby` warnings appeared

I have newest ruby version and jwt_sessions 2.5.1

This is the console output related to gem appearing every time I execute my specs:

/bundle/ruby/2.7.0/gems/jwt_sessions-2.5.1/lib/jwt_sessions/store_adapters.rb:12: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call8 
/bundle/ruby/2.7.0/gems/jwt_sessions-2.5.1/lib/jwt_sessions/store_adapters/redis_store_adapter.rb:10: warning: The called method `initialize' is defined here
/bundle/ruby/2.7.0/gems/jwt_sessions-2.5.1/lib/jwt_sessions/store_adapters/redis_store_adapter.rb:15: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/bundle/ruby/2.7.0/gems/jwt_sessions-2.5.1/lib/jwt_sessions/store_adapters/redis_store_adapter.rb:83: warning: The called method `configure_redis_client' is defined here

README update

Add access/refresh tokens concept description to the README.

Exception: undefined method `[]' for true:TrueClass

I've follow your guide in RefreshController using cookie below:

class RefreshController < ApplicationController
  before_action :authorize_refresh_by_access_request!

  def create
    session = JWTSessions::Session.new(payload: claimless_payload, refresh_by_access_allowed: true)
    tokens = session.refresh_by_access_allowed do
      raise JWTSession::Errors::Unauthorized, "Malicious Activity detected."
    end

    byebug
    
    response.set_cookie(JWTSessions.access_cookie,
                        value: tokens[:access],
                        httponly: true,
                        secure: Rails.env.production?)

    render json: { csrf: tokens[:csrf] }
  end
end

but after i try debugging it the result is error:
*** NoMethodError Exception: undefined method []' for true:TrueClass`

which is referenced to tokens[:access] in response.set_cookie(...

authorize_refresh_request! doesn't populate user_id key in payload

Hey
I've my refresh code as follows,

def refresh
    authorize_refresh_request!
    access_payload = { user_id: payload["user_id"] } # Here payload["user_id"] is null for some reason
    session = JWTSessions::Session.new(payload: access_payload, refresh_payload: payload)
    refresh = session.refresh(found_token)
    json_response(jwt: refresh[:access])
end

I'm passing a valid value X-Refresh-Token (eg: X-Refresh-Token: "euidf..") in header and still I see payload as null always. To me looks like an issue or I'm screwing up something.

p.s. I'm calling refresh before access token expiration. But I don't think, it should cause any issue.

Rack Authentication

How I can check user in middleware? As an example, ActionCable wants to have authentication on the middleware level. Maybe it makes sense to move auth logic to middleware. Maybe use warden for it.
@tuwukee, what flow do you see for this issue?

Documentation difficult to understand

Hi @tuwukee!

I like you work on this gem, it's really useful. I have been spending a lot time learning about jwt_sessions gem and all concepts wrapped in. As an experienced rookie with Rails and web development, I feel a bit frustrated reading the docs. I'd be very gratefull if you include more examples in order to explain the functioning of some callbacks and functions. I would like feel guided and no blocked reading and reading again the same paragraph.

Thank so much for your attention ๐Ÿ˜„ !

PD: Sorry for my english.

How to get current_user when skip authorize_access_request!

Hi, I used Rails 6.0.

Now I want to check is current_user present with skip authorize_access_request!, but I always get the error 401 Unauthorized.

class ApplicationController < ActionController::API
  include JWTSessions::RailsAuthorization
  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized

  private

  def current_user
    @current_user ||= User.find(payload['user_id'])
  end

  def not_authorized
    render json: { error: 'NO Data' }, status: :unauthorized
  end
end
class ClassifiesController < ApplicationController
  before_action :authorize_access_request!, except: [:index,:classify_tags]

  def  index 
     ...
  end

  def classify_tags
    ...
    @user_permission = current_user && (current_user.admin? || @tag.user_id == current_user.id)
    ...
  end
end

Thanks so much!

cookieless_auth("Authorization") returns "undefined"

Hey @tuwukee thanks for the amazing jwt gem :)

I have opted for the cookie based token approach, I get the following error when the authorize_access_request! method is executed, JWTSessions::Errors::Unauthorized (Nil JSON web token).

The request headers were the following

Accept: application/json
Authorization: undefined
Origin: https://localhost:8080
Referer: https://localhost:8080/login

I compared the access tokens in my console and they matched, digging further into the gem I noticed the cookieless_auth method did not raise an exception when executing the authorize_access_request! method - meaning the cookie_based_auth method is never called.

The cookieless_auth("Authorization") returns "undefined"

Would be amazing if we could get a fix

Sidekiq and Flipper authentication under jwt_sessions

Hey @tuwukee

I am trying to make sidekiq and flipper to authenticate with jwt_sessions. I've built a middleware, that works nicely with GET requests, that do not check CSRF token.

But I have no idea how to proceed regarding CSRF token for POST, DELETE, etc http methods that check CSRF.

Is there a simple way to solve that? Or I would have to read CSRF token from the store and inject it in the request. And if so, how can I do that?

# routes.rb
Rails.application.routes.draw do
  require 'sidekiq/web'
  Sidekiq::Web.use AuthMiddleware
  mount Sidekiq::Web => '/kiq'

  flipper_auth = Flipper::UI.app(Flipper.instance) do |builder|
    builder.use AuthMiddleware
  end
  mount flipper_auth, at: '/flipper'

  ...
end
# auth_middleware.rb
class AuthMiddleware
  include JWTSessions::RailsAuthorization

  def initialize(app)
    @app = app
  end

  def call(env)
    @env = env

    begin
      authorize_by_access_cookie!
      user = User.find_by(id: payload['user_id'])
      return unauthorized if user.blank? || user.permission_level != 9
      @app.call(env)
    rescue JWTSessions::Errors::Unauthorized => e
      unauthorized
    end
  end

  def unauthorized
    location = 'http://localhost:3004/'
    [301, { 'Content-Type' => 'text/html', 'Location' => location }, []]
  end

  def request
    @request ||= ActionDispatch::Request.new(@env)
  end
end

Redis store ist not triggered

I am trying to get an example for this gem and during the creation of the user I do not see any activity in the redis logs on my local machine.
The goal ist to make sure that the request from the front end with the access token is valid. But I always get an JWTSessions::Errors::Unauthorized (Nil JSON web token). Now I am not sure if this caused because I am doing something wrong on the frontend or because no "session" is stored.

My Controller

class SessionsController < ApplicationController
  skip_before_action :verify_authenticity_token, only: %i(create)

  def create
    @user = User.find_or_create_by(auth_hash)
    payload = { user_id: @user.id }
    my_session = JWTSessions::Session.new( payload: payload)
    tokens = my_session.login
   
    redirect_to "http://localhost:4200?token=#{tokens.to_json}"
  end

  def verify_login
    render json: { user: current_user }
  end
end

My Initiliazier

JWTSessions.encryption_key = 'secret'
JWTSessions.token_store = :redis, { redis_url: 'redis://127.0.0.1:6379/0' }

cookie issue with multiple logins?

Hi, I'm seeing a situation where I login a user in one browser window, and then login a 2nd user in a different browser window, the cookie token check fails. I think this is because there's just one cookie, and they end up sharing it, and thus when I try to do something with the 1st user after login in the 2nd user, the 1st user ends up with the 2nd user's cookie and things blow up.

Is this behavior just not supported? Do I need to rely only on headers?

Thx.

Session Namespaces & Refreshing

I noticed that when I added a namespace to the sessions I was creating a few things broke in my app regarding session refreshing and session deleting/flushing.

E.g. creation of a session changed to include the namespace attribute:

session = JWTSessions::Session.new(
    payload: { user_id: @user.id },
    refresh_payload: { user_id: @user.id },
    refresh_by_access_allowed: @client == :web,
    namespace: "user_#{@user.id}_sessions"
)

And After this change to include the namespace I started getting unauthorized request errors when refreshing and deleting sessions in my tests.

I tried adding the namespace onto the refresh session objects, but that didn't appear to solve the problem.

What is the correct way to implement namespaces to support flushing by namespace (for password resets for example). Or if I shouldn't be using namespaces for this, how would you suggest flushing all of the sessions attached to a specific user?

Integration problems

Hi, I try to implement an authentication method to an api for mobile app, but I receive two errors:

  1. For simple operations, I receive 500 error even if the result is correct. For instance:
class HelloController < ApplicationController
    def index
        render json: { message: "hello, world!" }, status: :succes
      end
end

I receive

< HTTP/1.1 500 Internal Server Error
< Content-Type: application/json; charset=utf-8
< Cache-Control: no-cache
< X-Request-Id: a464987f-6b78-4d84-bce2-d25b46a4d957
< X-Runtime: 0.001672
< Vary: Origin
< Transfer-Encoding: chunked
<
* Connection #0 to host api.api.test left intact
{"message":"hello, world!"} 

And logs from rails:

Started GET "/hello" for 127.0.0.1 at 2019-07-02 02:59:09 +0300
Processing by Api::V1::HelloController#index as JSON
  Parameters: {"subdomain"=>"api", "hello"=>{}}
Completed 500 Internal Server Error in 1ms (Views: 0.1ms | ActiveRecord: 0.0ms)
  1. When I try to log out (flush current session), the payload is nil:
def destroy
        byebug
        session = JWTSessions::Session.new(payload: payload)
        session.flush_by_access_payload
        render json: :ok
end

Receive:

   16:       def destroy
   17:         byebug
=> 18:         session = JWTSessions::Session.new(payload: payload)
   19:         session.flush_by_access_payload
   20:         render json: :ok
   21:       end
   22:     end
(byebug) payload
*** JWTSessions::Errors::Unauthorized Exception: Nil JSON web token nil

My ApplicationController:

class ApplicationController < ActionController::API
  include JWTSessions::RailsAuthorization

  before_action :authorize_access_request!
  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized

  private

  def current_user
    @current_user ||= User.find(payload['user_id'])
  end

  def not_authorized
    render json: { error: 'Not authorized' }, status: :unauthorized
  end
end

Sharing CSRF token between 2 different sub-domains

I am building own ERP system, where I have 3 independent Rails applications with own databases

Core (core.mydomain.com) - Rails + Admin app (based on RailsAdmin)+ Auth with JWT Sessions + React.js (list of applications, Sign In, Reset Password, User Profile)
HR (hr.mydomain.com)- Rails + React.js (list of Resumes)
IT (it.mydomain.com) - Rails + React.js (list of workstations)
I have integrated jwt_sessions to core and it perfectly works with react.js using cookies to store access and refresh tokens. User can open another application, for example HR and cookies will be automatically reused from Core to prevent re-login. HR application receives cookies and send them to receive current user. But as soon as it will be required to do any POST/PUT/PATCH or DELETE, for example Logout with calling Revoke taken or create new Resume, the system will return 401, cause HR does not have CSRF token. It is really very difficult to share the token from one React.js app to another.

I would like to know how to share a CSRF token between different react.js applications hosted on sub-domains? Of course I could implement some endpoint to fetch CSRF token based on Access token, but it would be a security hole. Probably I am using jwt_sessions incorrectly for such case. Looks like conceptually each sub-domain has to have own csrf token.

Heroku Redis TLS connection

I've tried to upgrade my Redis instance on Heroku to Premium, which only uses secured URL, resulting in JWT Sessions not working anymore. In their documentation, they suggest removing the SSL check to make it work, like so:

$redis = Redis.new(
  url: ENV["REDIS_URL"],
  ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
)

How can I do the same for JWT sessions? My configuration is like this:

JWTSessions.encryption_key = "secret"
JWTSessions.token_store = :redis

Perhaps something like this?

JWTSessions.token_store = :redis, {
  redis_url: ENV["REDIS_URL"],
  ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.