Skip to content

Add Authentication API #616

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

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
Comment on lines +1 to +55
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

Review JWT token expiration period.

The User model sets token expiration to 60 days, which is quite long by security standards. Consider implementing a shorter expiration period with a refresh token mechanism.


🏁 Script executed:

#!/bin/bash
# Check the JWT token expiration time in the User model
echo "Checking JWT token expiration in User model:"
cat app/models/user.rb | grep -A 10 "generate_jwt"

Length of output: 320


🌐 Web query:

What is the recommended JWT token expiration time for security?

💡 Result:

Recommended JWT Token Expiration Time for Security

JWT (JSON Web Token) expiration time is a central part of token security. Setting it appropriately helps balance security risks against user experience.

Best Practice Guidance

  • Short Expiration for Access Tokens:
    Security experts recommend that access tokens should have short lifetimes. The typical best practice is to set the expiration to a matter of minutes (e.g., 5–15 minutes) or, at most, a few hours[1][4][6][7]. This reduces the window in which a stolen or leaked token can be exploited by an attacker.

  • Why Short Expiration Matters:
    JWTs are usually stateless—once issued, they cannot be revoked until they expire. Thus, a shorter expiration minimizes the risk posed by compromised tokens[1][7][3]. When a JWT expires, users are required to re-authenticate or obtain a new token via a refresh workflow.

  • Typical Recommendations:

    • High-security APIs (banking, financial): 5–15 minutes[7][6].
    • General web applications: 15 minutes to 1 hour[1][4][7][9].
    • Longer lifetimes (days or weeks) are only appropriate for refresh tokens, which are stored more securely and rotated regularly[4][7].
  • Refresh Tokens:
    Use short-lived access tokens alongside longer-lived refresh tokens. This way, users can continue their sessions seamlessly while keeping security tight: the access token expires quickly, but the refresh token (stored securely) allows for session continuity and periodic token renewal[4][7][6].

Examples from Industry Guidance

  • Curity: Recommends token expiration "to minutes or hours at maximum" and cautions against validity for days or months[1].
  • Snyk: Shows example code for 30-minute expirations and warns that long-lived tokens increase risk[3].
  • MojoAuth: Suggests 5–15 minutes for high-security cases, 1–24 hours for typical web apps, and 7–30 days only for refresh tokens[7].
  • Auth0: Emphasizes that tokens should have an explicit expiration and a strategy for renewal or revocation; otherwise, they could remain valid indefinitely[2].

Balancing Security and Usability

While shorter expiration improves security, it also means users may need to refresh tokens more frequently, which can impact user experience. The right balance depends on your application's risk profile, user expectations, and how you handle token refreshes[4][7].

Summary Table

Token Type Recommended Expiration Typical Use
Access Token 5–15 minutes (up to 1 hour) API access, auth
Refresh Token 7–30 days Session continuation

Conclusion:
Set JWT access token expiration to no more than 1 hour—preferably 15–30 minutes for most web applications, and even shorter for sensitive environments. Pair this with secure, longer-lived refresh tokens to maintain both security and usability[1][3][4][6][7].

Never issue access tokens valid for days or months. Always set explicit expiration times and have a strategy for secure token refresh and storage.

Citations:


Review and update JWT expiration and token strategy

The current generate_jwt sets the token to expire in 60 days—well beyond security best practices for access tokens. Industry guidance recommends access tokens live no more than 15 minutes–1 hour, and longer lifetimes (days/weeks) should be reserved for refresh tokens stored and rotated securely. Please:

• File app/models/user.rb
– At def generate_jwt, change
diff - exp: 60.days.from_now.to_i + exp: 15.minutes.from_now.to_i # or appropriate short-lived duration
• Introduce a refresh token mechanism:
– Generate and store a long-lived refresh token (e.g., 7–30 days) per user
– Expose an endpoint to exchange a valid refresh token for a new access token
– Implement secure storage and rotation of refresh tokens (e.g., blacklist on use)

This will align access tokens with best practices and limit exposure if a token is compromised.

Committable suggestion skipped: line range outside the PR's diff.

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