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

Add Authentication API #616

Open
wants to merge 5 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
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ gem "redcarpet"
# It is critical to not include any of the jquery gems when following this pattern or
# else you might have multiple jQuery versions.

gem "devise"

gem "jwt"

group :development do
# Access an IRB console on exceptions page and /console in development
gem "web-console"
Expand Down
17 changes: 17 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ GEM
execjs (~> 2)
awesome_print (1.9.2)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
bigdecimal (3.1.8)
bindex (0.8.1)
Expand Down Expand Up @@ -134,6 +135,12 @@ GEM
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
devise (4.9.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
diff-lcs (1.5.1)
docile (1.4.0)
drb (2.2.1)
Expand Down Expand Up @@ -165,6 +172,8 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.7.2)
jwt (2.10.1)
base64
language_server-protocol (3.17.0.3)
launchy (3.0.1)
addressable (~> 2.8)
Expand Down Expand Up @@ -204,6 +213,7 @@ GEM
racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux)
racc (~> 1.4)
orm_adapter (0.5.0)
package_json (0.1.0)
parallel (1.26.3)
parser (3.3.3.0)
Expand Down Expand Up @@ -304,6 +314,9 @@ GEM
reline (0.5.9)
io-console (~> 0.5)
require_all (3.0.0)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rexml (3.3.1)
strscan
rspec-core (3.13.0)
Expand Down Expand Up @@ -425,6 +438,8 @@ GEM
unicode-emoji (4.0.4)
uri (1.0.2)
useragent (0.16.10)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
Expand Down Expand Up @@ -454,10 +469,12 @@ DEPENDENCIES
coveralls_reborn (~> 0.25.0)
database_cleaner
debug (>= 1.0.0)
devise
factory_bot_rails
foreman
generator_spec
jbuilder
jwt
launchy
listen
net-pop!
Expand Down
55 changes: 55 additions & 0 deletions app/controllers/api/authentication_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module Api
class AuthenticationController < ApplicationController
def create
user = User.find_by(email: params[:email].downcase.strip)

if user&.valid_password?(params[:password])
begin
token = user.generate_jwt
render json: {
message: "Login successful",
token: token
}, status: :ok
rescue JWT::EncodeError
render json: { error: "Authentication failed" }, status: :internal_server_error
end
else
render json: { error: "Invalid credentials" }, status: :unauthorized
end
end
Comment on lines +5 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add rate limiting to prevent brute force attacks.

The login endpoint currently has no protection against brute force attacks. Consider implementing rate limiting based on IP address and/or account.

You could implement this by adding a before_action that checks for too many failed attempts:

before_action :check_request_rate_limit, only: [:create]

def check_request_rate_limit
  # Use Rails.cache or Redis to track attempts
  key = "auth_attempts:#{request.ip}"
  attempts = Rails.cache.read(key) || 0
  
  if attempts >= 5 # Limit to 5 attempts per hour
    Rails.cache.write(key, attempts + 1, expires_in: 1.hour)
    render json: { error: "Too many attempts. Please try again later." }, 
           status: :too_many_requests
  else
    Rails.cache.write(key, attempts + 1, expires_in: 1.hour)
  end
end


def signup
return render json: { error: "Invalid email format" }, status: :unprocessable_entity unless
params.dig(:user, :email)&.match?(URI::MailTo::EMAIL_REGEXP)

return render json: { error: "Password must be at least 6 characters" }, status: :unprocessable_entity if
params.dig(:user, :password)&.length.to_i < 6

user = User.new(user_params)

if user.save
begin
token = user.generate_jwt
render json: {
message: "Signup successful",
token: token
}, status: :created
rescue JWT::EncodeError
render json: { error: "Failed to generate authentication token" }, status: :internal_server_error
end
else
render json: {
errors: user.errors.full_messages
}, status: :unprocessable_entity
end
end

private

def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
Comment on lines +1 to +54
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Add token revocation functionality.

The controller handles token generation but lacks functionality for token revocation or logout. Consider adding a method to invalidate tokens.

You could add a logout method:

def logout
  # If using a token blacklist or revocation strategy
  current_user&.revoke_jwt
  
  render json: { message: "Logged out successfully" }, status: :ok
end

This would require implementing a revoke_jwt method in the User model and updating your routes to include a logout endpoint.


🌐 Web query:

What are the best practices for JWT token revocation?

💡 Result:

JWTs (JSON Web Tokens) present unique challenges for revocation due to their stateless nature, but several best practices ensure secure token management:


Core Strategies for JWT Revocation

1. Short-Lived Tokens with Refresh Tokens

  • Short expiration times: Limit JWT validity to minutes (e.g., 15–60 minutes) to reduce exposure if compromised[7][8].
  • Revoke refresh tokens: Invalidate long-lived refresh tokens (e.g., 2 weeks) to block new JWT generation. This forces reauthentication but leaves existing JWTs active until expiration[1][8].
  • Example:
    # Flask-JWT-Extended revoking refresh tokens
    @app.route("/logout", methods=["DELETE"])
    @jwt_required(refresh=True)
    def logout():
        jti = get_jwt()["jti"]
        db.session.add(TokenBlocklist(jti=jti))
        db.session.commit()
        return jsonify(msg="Refresh token revoked")[2]

2. Distributed Event Systems

  • Webhook notifications: Broadcast refresh token revocations to services via events (e.g., jwt.refresh-token.revoke). Services cache revoked user IDs/JWT IDs (jti) and validate against this list[1][6][8].
  • Implementation:
    // FusionAuth webhook handler
    router.post('/revoke', (req, res) => {
      const { userId, applicationId } = req.body.event;
      JWTManager.revoke(userId, applicationId); // Updates cached revocation list[8]
    });

3. Blocklist/Denylist

  • Storing revoked tokens: Maintain a database/Redis cache of invalidated tokens checked during validation. Entries auto-expire based on JWT exp[5][6][11].
  • Code example:
    // Node.js blocklist check middleware
    const isRevoked = async (req, token) => {
      const jti = token.jti;
      const exists = await redis.get(`bl_${jti}`);
      return !!exists;
    }[9][11]

4. Refresh Token Rotation

  • Rotate on use: Issue new refresh tokens after each use, invalidating old ones. Limits stolen token utility[3][10].
    // Rotating refresh tokens
    const rotateRefreshToken = (oldToken) => {
      invalidateToken(oldToken);
      return { newAccessToken, newRefreshToken };
    }[3]

Additional Best Practices

  • Token versioning: Include a version claim (e.g., tokenVersion) in JWTs. Increment globally to invalidate all prior tokens[3].
  • HTTPS enforcement: Prevent token interception by requiring encrypted connections[7][12].
  • Validate critical claims: Verify iss (issuer), aud (audience), and signature during JWT validation[7][13].
  • Avoid sensitive data: JWTs are base64-encoded, not encrypted. Store minimal user data[7][12].

Tradeoffs and Recommendations

Method Pros Cons
Short-lived JWTs Simple, no state management Delayed revocation (until expiry)
Blocklist Immediate revocation Stateful, scalability challenges
Refresh token revocation Balances security and usability Requires frontend coordination
Event systems Real-time, scalable Complex infrastructure needed

For most applications, combining short-lived JWTs with refresh token rotation and event-driven revocation provides robust security. Services like Auth0 or FusionAuth simplify implementation with built-in token management and webhooks[8][12].

Citations:


Implement Token Revocation (Logout) Functionality

The AuthenticationController currently handles token generation for login and signup but lacks any means to invalidate tokens on logout. Given JWT’s stateless nature, simply “destroying” a token on the client is insufficient for immediate revocation. Instead, consider the following best practices when implementing a logout endpoint:

  • Blocklist/Denylist Strategy:
    Add a logout method that marks the token as revoked (e.g., by saving its unique identifier in a blocklist). During authentication on subsequent requests, check against this list.

  • Short-Lived Tokens + Refresh Token Rotation:
    Use short-lived access tokens to limit exposure and pair them with refresh tokens. On logout, revoke the refresh token to prevent issuance of new access tokens.

For instance, you could add a logout method as follows:

def logout
  # Revoke the current JWT using a blocklist or similar strategy.
  # Ensure that the 'revoke_jwt' method in the User model properly invalidates the token,
  # such as by storing its JTI in a token blocklist.
  current_user&.revoke_jwt if current_user
  
  render json: { message: "Logged out successfully" }, status: :ok
end

Action Items:

  • Implement and test a revoke_jwt method in the User model to support token invalidation.
  • Update your routes to expose this new logout endpoint.
  • Review your token strategy to consider using short-lived access tokens combined with refresh token rotation for improved security.

end
2 changes: 1 addition & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception,
protect_from_forgery with: :null_session,
if: proc { request.headers["X-Auth"] != "tutorial_secret" }
end
15 changes: 15 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
def generate_jwt
JWT.encode(
{
id: id,
exp: 60.days.from_now.to_i
},
Rails.application.credentials.secret_key_base
)
end
end
Loading
Loading