Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWT authentication #239

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@ User.load_from_activation_token(token)
@user.activate!
```

### JWT Authentication

```ruby
require_jwt_authentication # This is a before action
jwt_authenticate(email, password) # => {token: 'token', payload: {..}}
jwt_from_header # Token extracted from header
jwt_decoded_payload # Payload extracted from token
```

Please see the tutorials in the github wiki for detailed usage information.

## Installation
Expand Down Expand Up @@ -214,6 +223,12 @@ Inside the initializer, the comments will tell you what each setting does.
- Configurable database column names
- Authentications table

**JWT** (see [lib/sorcery/controller/submodules/jwt.rb](https://github.com/Sorcery/sorcery/blob/master/lib/sorcery/controller/submodules/jwt.rb)):

- Token generation (various algorithms are supported)
- Before action for authenticating via header
- Configurable payload

## Planned Features

- Passing a block to encrypt, allowing the developer to define his own mix of salting and encrypting
Expand Down
41 changes: 40 additions & 1 deletion lib/generators/sorcery/templates/initializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# Available submodules are: :user_activation, :http_basic_auth, :remember_me,
# :reset_password, :session_timeout, :brute_force_protection, :activity_logging,
# :magic_login, :external
# :magic_login, :external, :jwt
Rails.application.config.sorcery.submodules = []

# Here you can configure each submodule's features.
Expand Down Expand Up @@ -229,6 +229,45 @@
# config.discord.secret = "xxxxxx"
# config.discord.callback_url = "http://localhost:3000/oauth/callback?provider=discord"
# config.discord.scope = "email guilds"

# -- jwt --
# REQUIRED:
# Algorithm and keys for token management.
# Default: `nil`
# Depending on algorithm keys can be equal strings - 'secret_key', 'secret_key'.
# Or a key objects - OpenSSL::PKey.read(File.read('private.key')), OpenSSL::PKey.read(File.read('public.key')).
# Check out https://github.com/jwt/ruby-jwt for more details.
#
# config.jwt_algorithm =
# config.jwt_encode_key =
# config.jwt_decode_key =

# How long generated token is valid (in seconds).
# Default: `3600`
#
# config.jwt_lifetime =

# Additional time (in seconds) to account for clock skew.
# Default: `30`
#
# config.jwt_lifetime_leeway =

# Header which will be used as token source.
# Default: `'Authorization'`
#
# config.jwt_header =

# User action to be called and merged with default payload.
# Default: `nil`
#
# config.jwt_additional_user_payload_action =

# What controller action to call when token is invalid.
# You can also override the 'jwt_not_authenticated' method of course.
# Default: `:jwt_not_authenticated`
#
# config.jwt_not_authenticated_action =

# --- user config ---
config.user_config do |user|
# -- core --
Expand Down
1 change: 1 addition & 0 deletions lib/sorcery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ module Submodules
require 'sorcery/controller/submodules/http_basic_auth'
require 'sorcery/controller/submodules/activity_logging'
require 'sorcery/controller/submodules/external'
require 'sorcery/controller/submodules/jwt'
end
end

Expand Down
139 changes: 139 additions & 0 deletions lib/sorcery/controller/submodules/jwt.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
module Sorcery
module Controller
module Submodules
# This submodule adds support for authentication via JSON Web Tokens.
# https://jwt.io/
module Jwt
TOKEN_REGEX = /^(Token|Bearer)\s+/.freeze

def self.included(base)
base.send(:include, InstanceMethods)

Config.module_eval do
class << self
attr_accessor :jwt_algorithm
attr_accessor :jwt_encode_key
attr_accessor :jwt_decode_key
attr_accessor :jwt_lifetime
attr_accessor :jwt_lifetime_leeway
attr_accessor :jwt_header
attr_accessor :jwt_additional_user_payload_action
attr_accessor :jwt_not_authenticated_action

def merge_jwt_defaults!
@defaults.merge!(:@jwt_algorithm => nil,
:@jwt_encode_key => nil,
:@jwt_decode_key => nil,
:@jwt_lifetime => 3600,
:@jwt_lifetime_leeway => 30,
:@jwt_header => 'Authorization',
:@jwt_additional_user_payload_action => nil,
:@jwt_not_authenticated_action => :jwt_not_authenticated)
end
end

merge_jwt_defaults!
end

Config.login_sources << :login_from_jwt_header
end

module InstanceMethods
# To be used as before_action.
# Will trigger auto-login attempts via the call to logged_in?
# If all attempts to auto-login fail, the failure callback will be called.
def require_jwt_authentication
return if logged_in?

send(Config.jwt_not_authenticated_action)
end

# Takes credentials and returns generated token on successful authentication.
# Runs hooks after login or failed login.
def jwt_authenticate(*credentials)
validate_jwt_configuration

@current_user = nil

user_class.authenticate(*credentials) do |user, failure_reason|
if failure_reason
after_failed_login!(credentials)

yield(user, failure_reason) if block_given?

break
end

# Identical to auto_login but doesn't touch session
@current_user = user

after_login!(user, credentials)

yield(user, failure_reason) if block_given?

# Return our own value, not the return_value from authentication_response
break generate_jwt(user)
end
end

# Generate token and payload hash based on provided user
def generate_jwt(user)
now = Time.current.to_i

payload = { sub: user.id,
iat: now,
exp: now + Config.jwt_lifetime }

payload.merge!(user.public_send(Config.jwt_additional_user_payload_action)) if Config.jwt_additional_user_payload_action

{ token: jwt_encode(payload),
payload: payload }
end

# Checks header for a token and tries to log in user if token is valid.
# Runs as a login source callback. Check current_user method for more details.
def login_from_jwt_header
@current_user = if jwt_decoded_payload['sub']
user_class.sorcery_adapter.find_by_id(jwt_decoded_payload['sub'])
end
end

# The default action for denying non-authenticated users.
# You can override this method in your controllers,
# or provide a different method in the configuration.
def jwt_not_authenticated
head :unauthorized
end

def validate_jwt_configuration
raise ArgumentError, "To use jwt submodule, you must define an algorithm (config.jwt_algorithm = 'algorithm')." if Config.jwt_algorithm.nil? || Config.jwt_algorithm == ''
raise ArgumentError, "To use jwt submodule, you must define an encode key (config.jwt_encode_key = 'your_key')." if Config.jwt_encode_key.nil? || Config.jwt_encode_key == ''
raise ArgumentError, "To use jwt submodule, you must define a decode key (config.jwt_decode_key = 'your_key')." if Config.jwt_decode_key.nil? || Config.jwt_decode_key == ''
end

# Token (without type) extracted from header
def jwt_from_header
@jwt_from_header ||= request.headers[Config.jwt_header].to_s.sub(TOKEN_REGEX, '')
end

# Payload decoded from token
def jwt_decoded_payload
@jwt_decoded_payload ||= jwt_decode(jwt_from_header)
end

# Create JWT from payload
def jwt_encode(payload)
JWT.encode(payload, Config.jwt_encode_key, Config.jwt_algorithm)
end

# Decode payload from JWT
def jwt_decode(token)
JWT.decode(token, Config.jwt_decode_key, true, { algorithm: Config.jwt_algorithm, exp_leeway: Config.jwt_lifetime_leeway })[0]
rescue JWT::DecodeError
{}
end
end
end
end
end
end
1 change: 1 addition & 0 deletions sorcery.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Gem::Specification.new do |s|
s.add_dependency 'bcrypt', '~> 3.1'
s.add_dependency 'oauth', '~> 0.4', '>= 0.4.4'
s.add_dependency 'oauth2', '~> 1.0', '>= 0.8.0'
s.add_dependency 'jwt', '~> 2.2.0'

s.add_development_dependency 'byebug', '~> 10.0.0'
s.add_development_dependency 'rspec-rails', '~> 3.7.0'
Expand Down
Loading