Skip to content

Commit 24bbfd0

Browse files
Merge pull request #1 from CodeSignal/auth
Fixed logout with JWT bug and reorganized
2 parents fbcbac0 + 9da0725 commit 24bbfd0

File tree

3 files changed

+263
-85
lines changed

3 files changed

+263
-85
lines changed

app/routes/auth.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
1-
from flask import Blueprint, request, jsonify, session
2-
import jwt
3-
import datetime
1+
from flask import Blueprint, request, jsonify
42
from config.auth_config import AuthMethod, AuthConfig
5-
from services.auth_service import generate_jwt_token, is_username_taken, add_user, signup_user, login_user, logout_user, blacklist_token, validate_refresh_token, refresh_tokens
3+
from services.auth_service import (
4+
signup_user,
5+
validate_refresh_token,
6+
generate_jwt_token,
7+
refresh_tokens,
8+
login_jwt,
9+
login_session,
10+
logout_jwt,
11+
logout_session
12+
)
613

7-
auth_bp = Blueprint("auth", __name__)
8-
auth_config = None
14+
# --- Configuration ---
15+
auth_bp = Blueprint("auth", __name__) # Flask blueprint for auth routes
16+
auth_config = None # Global configuration object set during initialization
917

1018
def init_auth_routes(config: AuthConfig):
1119
global auth_config
1220
auth_config = config
1321

22+
# --- Authentication Routes ---
1423
@auth_bp.route("/signup", methods=["POST"])
1524
def signup():
25+
"""
26+
Register a new user
27+
Expects JSON: {"username": "user", "password": "pass"}
28+
"""
1629
if auth_config.auth_method == AuthMethod.API_KEY:
1730
return jsonify({"error": "Signup not available with API key authentication"}), 400
1831

@@ -21,14 +34,60 @@ def signup():
2134

2235
@auth_bp.route("/login", methods=["POST"])
2336
def login():
37+
"""
38+
Authenticate user and return tokens (JWT) or create session
39+
Expects JSON: {"username": "user", "password": "pass"}
40+
"""
2441
if auth_config.auth_method == AuthMethod.API_KEY:
2542
return jsonify({"error": "Login not available with API key authentication"}), 400
2643

2744
data = request.get_json()
28-
return login_user(data)
45+
if not data or "username" not in data or "password" not in data:
46+
return jsonify({"error": "Username and password are required"}), 400
47+
48+
username = data["username"]
49+
password = data["password"]
50+
51+
if auth_config.auth_method == AuthMethod.JWT:
52+
return login_jwt(username, password)
53+
54+
return login_session(username, password)
55+
56+
@auth_bp.route("/logout", methods=["POST"])
57+
def logout():
58+
"""
59+
End user session or invalidate JWT tokens
60+
For JWT: Requires Authorization header with Bearer token and refresh_token in JSON body
61+
For Session: No additional requirements
62+
"""
63+
if auth_config.auth_method == AuthMethod.API_KEY:
64+
return jsonify({"error": "Logout not available with API key authentication"}), 400
65+
66+
if auth_config.auth_method == AuthMethod.JWT:
67+
auth_header = request.headers.get('Authorization')
68+
if not auth_header or not auth_header.startswith('Bearer '):
69+
return jsonify({"error": "Access token is required in Authorization header"}), 401
70+
access_token = auth_header.split(' ')[1]
71+
72+
if not request.is_json:
73+
return jsonify({"error": "Request must be JSON"}), 415
74+
75+
data = request.get_json()
76+
refresh_token = data.get("refresh_token")
77+
if not refresh_token:
78+
return jsonify({"error": "Refresh token is required in request body"}), 400
79+
80+
return logout_jwt(access_token, refresh_token)
81+
82+
return logout_session()
2983

3084
@auth_bp.route("/refresh", methods=["POST"])
3185
def refresh_token():
86+
"""
87+
Get new access token using refresh token
88+
Expects JSON: {"refresh_token": "token"}
89+
Returns: New access token and refresh token pair
90+
"""
3291
data = request.get_json()
3392
refresh_token = data.get("refresh_token")
3493
username = validate_refresh_token(refresh_token)
@@ -37,36 +96,10 @@ def refresh_token():
3796

3897
access_token, new_refresh_token = generate_jwt_token(username)
3998

40-
# Remove old refresh token and store new one
4199
if refresh_token in refresh_tokens:
42100
del refresh_tokens[refresh_token]
43101

44102
return jsonify({
45103
"access_token": access_token,
46104
"refresh_token": new_refresh_token
47-
}), 200
48-
49-
@auth_bp.route("/logout", methods=["POST"])
50-
def logout():
51-
if auth_config.auth_method == AuthMethod.API_KEY:
52-
return jsonify({"error": "Logout not available with API key authentication"}), 400
53-
54-
elif auth_config.auth_method == AuthMethod.JWT:
55-
auth_header = request.headers.get('Authorization')
56-
if auth_header and auth_header.startswith('Bearer '):
57-
token = auth_header.split(' ')[1]
58-
blacklist_token(token)
59-
60-
# Invalidate refresh token
61-
data = request.get_json()
62-
refresh_token = data.get("refresh_token")
63-
if refresh_token in refresh_tokens:
64-
del refresh_tokens[refresh_token]
65-
66-
return jsonify({"message": "Logout successful"})
67-
68-
elif auth_config.auth_method == AuthMethod.SESSION:
69-
session.clear()
70-
return jsonify({"message": "Logout successful"})
71-
72-
return jsonify({"error": "Invalid authentication method"}), 500
105+
}), 200

app/services/auth_service.py

Lines changed: 68 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,98 +5,116 @@
55
from models.user import User
66
from flask import session, jsonify
77

8-
# Initialize auth_config as None
9-
auth_config = None
10-
11-
# List to store User objects
12-
users = []
13-
14-
# In-memory store for refresh tokens and blacklisted tokens
15-
refresh_tokens = {}
16-
blacklisted_tokens = set()
8+
# --- Configuration ---
9+
auth_config = None # Global configuration object set during initialization
1710

1811
def init_auth_service(config: AuthConfig):
1912
global auth_config
2013
auth_config = config
2114

15+
# --- Storage ---
16+
users = [] # In-memory storage for user objects
17+
refresh_tokens = {} # Maps refresh tokens to usernames
18+
blacklisted_tokens = set() # Set of invalidated access tokens
19+
20+
# --- User Management ---
21+
def is_username_taken(username):
22+
"""Check if a username is already registered"""
23+
return any(user.username == username for user in users)
24+
25+
def add_user(username, password):
26+
"""Add a new user if username is not taken"""
27+
if not is_username_taken(username):
28+
users.append(User(username, password))
29+
return True
30+
return False
31+
32+
def validate_credentials(username, password):
33+
"""Verify username/password combination and return user if valid"""
34+
user = next((user for user in users if user.username == username), None)
35+
if not user or not user.check_password(password):
36+
return None
37+
return user
38+
39+
# --- Token Management ---
2240
def generate_refresh_token(username):
41+
"""Create and store a new refresh token for a user"""
2342
refresh_token = secrets.token_hex(32)
2443
refresh_tokens[refresh_token] = username
2544
return refresh_token
2645

2746
def validate_refresh_token(refresh_token):
47+
"""Check if refresh token is valid and return associated username"""
2848
return refresh_tokens.get(refresh_token)
2949

3050
def blacklist_token(token):
51+
"""Invalidate an access token"""
3152
blacklisted_tokens.add(token)
3253

3354
def generate_jwt_token(username):
55+
"""Generate a new JWT access token and refresh token pair"""
3456
access_token = jwt.encode(
3557
{
3658
"sub": username,
3759
"iat": datetime.datetime.utcnow(),
38-
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15) # Shorter expiry for access token
60+
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
3961
},
4062
auth_config.jwt_secret,
4163
algorithm="HS256"
4264
)
4365
refresh_token = generate_refresh_token(username)
4466
return access_token, refresh_token
4567

46-
# Function to check if a username already exists
47-
def is_username_taken(username):
48-
return any(user.username == username for user in users)
49-
50-
# Function to add a new user
51-
def add_user(username, password):
52-
if not is_username_taken(username):
53-
users.append(User(username, password))
54-
return True
55-
return False
56-
68+
# --- Authentication Operations ---
5769
def signup_user(data):
70+
"""Register a new user with username and password"""
5871
if not data or "username" not in data or "password" not in data:
5972
return jsonify({"error": "Username and password are required"}), 400
6073

6174
if is_username_taken(data["username"]):
6275
return jsonify({"error": "Username already exists"}), 400
6376

6477
add_user(data["username"], data["password"])
65-
6678
return jsonify({"message": "Signup successful. Please log in to continue."}), 201
6779

68-
69-
def login_user(data):
70-
if not data or "username" not in data or "password" not in data:
71-
return jsonify({"error": "Username and password are required"}), 400
80+
def login_jwt(username, password):
81+
"""Authenticate user and return JWT tokens if valid"""
82+
user = validate_credentials(username, password)
83+
if not user:
84+
return jsonify({"error": "Invalid username or password"}), 401
7285

73-
# Find user and validate credentials
74-
user = next((user for user in users if user.username == data["username"]), None)
75-
if not user or not user.check_password(data["password"]):
86+
access_token, refresh_token = generate_jwt_token(username)
87+
return jsonify({
88+
"message": "Login successful",
89+
"access_token": access_token,
90+
"refresh_token": refresh_token
91+
})
92+
93+
def login_session(username, password):
94+
"""Authenticate user and create session if valid"""
95+
user = validate_credentials(username, password)
96+
if not user:
7697
return jsonify({"error": "Invalid username or password"}), 401
7798

78-
if auth_config.auth_method == AuthMethod.JWT:
79-
access_token, refresh_token = generate_jwt_token(data["username"])
80-
return jsonify({
81-
"message": "Login successful",
82-
"access_token": access_token,
83-
"refresh_token": refresh_token
84-
})
99+
session["authenticated"] = True
100+
session["username"] = username
101+
return jsonify({"message": "Login successful"})
102+
103+
def logout_jwt(access_token, refresh_token):
104+
"""Invalidate JWT access and refresh tokens"""
105+
if not access_token or not refresh_token:
106+
return jsonify({"error": "Both access token and refresh token are required"}), 400
85107

86-
elif auth_config.auth_method == AuthMethod.SESSION:
87-
session["authenticated"] = True
88-
session["username"] = data["username"]
89-
return jsonify({"message": "Login successful"})
108+
blacklist_token(access_token)
109+
if refresh_token in refresh_tokens:
110+
del refresh_tokens[refresh_token]
90111

91-
return jsonify({"error": "Invalid authentication method"}), 500
92-
112+
return jsonify({"message": "Logout successful"})
93113

94-
def logout_user():
95-
if auth_config.auth_method == AuthMethod.JWT:
96-
return jsonify({"message": "Logout successful"})
97-
98-
elif auth_config.auth_method == AuthMethod.SESSION:
99-
session.clear()
100-
return jsonify({"message": "Logout successful"})
114+
def logout_session():
115+
"""Clear user session if authenticated"""
116+
if not session.get("authenticated"):
117+
return jsonify({"error": "Not authenticated"}), 401
101118

102-
return jsonify({"error": "Invalid authentication method"}), 500
119+
session.clear()
120+
return jsonify({"message": "Logout successful"})

0 commit comments

Comments
 (0)