Skip to content
72 changes: 27 additions & 45 deletions blueprints/brlogin.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,51 +135,30 @@ def broker_callback(broker, para=None):
)

elif broker == "aliceblue":
if request.method == "GET":
# Redirect to React TOTP page
return redirect("/broker/aliceblue/totp")

elif request.method == "POST":
logger.info("Aliceblue Login Flow initiated")
userid = request.form.get("userid")
# Step 1: Get encryption key
# Use the shared httpx client with connection pooling
from utils.httpx_client import get_httpx_client

client = get_httpx_client()

# AliceBlue API expects only userId in the encryption key request
# Do not include API key in this initial request
payload = {"userId": userid}
headers = {"Content-Type": "application/json"}
try:
# Get encryption key
url = "https://ant.aliceblueonline.com/rest/AliceBlueAPIService/api/customer/getAPIEncpkey"
response = client.post(url, json=payload, headers=headers)
response.raise_for_status()
data_dict = response.json()
logger.debug(f"Aliceblue response data: {data_dict}")

# Check if we successfully got the encryption key
if data_dict.get("stat") == "Ok" and data_dict.get("encKey"):
enc_key = data_dict["encKey"]
# Step 2: Authenticate with encryption key
auth_token, error_message = auth_function(userid, enc_key)

if auth_token:
return handle_auth_success(auth_token, session["user"], broker)
else:
return handle_auth_failure(error_message, forward_url="broker.html")
else:
# Failed to get encryption key
error_msg = data_dict.get("emsg", "Failed to get encryption key")
return handle_auth_failure(
f"Failed to get encryption key: {error_msg}", forward_url="broker.html"
)
except Exception as e:
return jsonify(
{"status": "error", "message": f"Authentication error: {str(e)}"}
), 500
# New OAuth redirect flow:
# 1. GET without authCode → redirect to AliceBlue login page with appcode
# 2. GET with authCode + userId (callback) → authenticate and get session
authCode = request.args.get("authCode")
userId = request.args.get("userId")

if authCode and userId:
# Callback from AliceBlue with authorization code
logger.info(f"AliceBlue OAuth callback received for user {userId}")
auth_token, client_id, error_message = auth_function(userId, authCode)
user_id = client_id or userId # clientId from API response, fallback to OAuth userId
feed_token = None # AliceBlue doesn't use a separate feed token
forward_url = "broker.html"
else:
# Initial visit — redirect to AliceBlue login page
logger.info("Redirecting to AliceBlue login page")
appcode = os.environ.get("BROKER_API_KEY")
if not appcode:
return handle_auth_failure(
"BROKER_API_KEY (appCode) not configured in environment",
forward_url="broker.html",
)
aliceblue_login_url = f"https://ant.aliceblueonline.com/?appcode={appcode}"
return redirect(aliceblue_login_url)
Comment on lines +160 to +161

Choose a reason for hiding this comment

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

P1 Badge Return navigable OAuth response for AliceBlue login start

When authCode is absent this branch returns an external redirect, but the current login UI starts AliceBlue auth with fetch('/aliceblue/callback', { method: 'POST' }) and immediately calls response.json() (see frontend/src/pages/BrokerTOTP.tsx around the submit handler). In that context the 302 is handled inside XHR (not a top-level navigation), which leads to CORS/JSON failure instead of opening the broker login page, so AliceBlue login cannot be initiated from the existing web flow.

Useful? React with 👍 / 👎.


elif broker == "fivepaisaxts":
code = "fivepaisaxts"
Expand Down Expand Up @@ -771,6 +750,9 @@ def broker_callback(broker, para=None):
auth_token = f"{auth_token}"

# For brokers that have user_id and feed_token from authenticate_broker
if broker in ["angel", "aliceblue", "compositedge", "pocketful", "definedge", "dhan"]:
# For Compositedge, handle missing session user
if broker == "compositedge" and "user" not in session:
if broker in ["angel", "compositedge", "pocketful", "definedge", "dhan", "rmoney"]:
# For OAuth brokers, handle missing session user
if broker in ("compositedge", "rmoney") and "user" not in session:
Expand Down
50 changes: 34 additions & 16 deletions broker/aliceblue/api/alicebluewebsocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ class AliceBlueWebSocket:
PRIMARY_URL = "wss://ws1.aliceblueonline.com/NorenWS/"
ALTERNATE_URL = "wss://ws2.aliceblueonline.com/NorenWS/"

# REST API base URL for WebSocket session management
BASE_URL = "https://ant.aliceblueonline.com/rest/AliceBlueAPIService/api/"
# REST API base URL for WebSocket session management (V2 API)
BASE_URL = "https://a3.aliceblueonline.com/"

# Maximum reconnection attempts
MAX_RECONNECT_ATTEMPTS = 5
Expand Down Expand Up @@ -61,7 +61,7 @@ def __init__(self, user_id: str, session_id: str):
def _get_auth_header(self) -> dict:
"""Get authorization header for REST API calls."""
return {
"Authorization": f"Bearer {self.user_id.upper()} {self.session_id}",
"Authorization": f"Bearer {self.session_id}",
"Content-Type": "application/json",
}

Expand All @@ -74,9 +74,8 @@ def _invalidate_socket_session(self) -> bool:
bool: True if successful, False otherwise
"""
try:
# Try both endpoint variants (createWsSession and createSocketSess)
url = self.BASE_URL + "ws/invalidateWsSession"
payload = {"loginType": "API"}
url = self.BASE_URL + "open-api/od/v1/profile/invalidateWsSess"
payload = {"source": "API", "userId": self.user_id}

with httpx.Client() as client:
response = client.post(
Expand Down Expand Up @@ -105,9 +104,8 @@ def _create_socket_session(self) -> bool:
bool: True if successful, False otherwise
"""
try:
# Use the same endpoint as aliceblue_client.py: ws/createWsSession
url = self.BASE_URL + "ws/createWsSession"
payload = {"loginType": "API"}
url = self.BASE_URL + "open-api/od/v1/profile/createWsSess"
payload = {"source": "API", "userId": self.user_id}

with httpx.Client() as client:
response = client.post(
Expand All @@ -119,7 +117,7 @@ def _create_socket_session(self) -> bool:
logger.info(f"Create socket session response: {data}")

# Check for success
if data.get("stat") == "Ok":
if data.get("status") == "Ok":
logger.info("WebSocket session created successfully")
return True
else:
Expand Down Expand Up @@ -366,10 +364,10 @@ def _process_tick_data(self, data):
else:
# Fallback to broker symbol from AliceBlue data
symbol = data.get("ts", f"TOKEN_{token}")
logger.warning(
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
logger.debug(
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
)
logger.warning(f"Available subscriptions: {list(self.subscriptions.keys())}")
logger.debug(f"Available subscriptions: {list(self.subscriptions.keys())}")

# Use consistent key format for data storage: exchange:token
key = f"{exchange}:{token}"
Expand All @@ -396,6 +394,10 @@ def _process_tick_data(self, data):
"prev_open_interest": int(float(data.get("poi", 0))) if data.get("poi") else 0,
"total_buy_quantity": int(data.get("tbq", 0)),
"total_sell_quantity": int(data.get("tsq", 0)),
"bid": float(data.get("bp1", 0)),
"ask": float(data.get("sp1", 0)),
"bid_qty": int(data.get("bq1", 0)),
"ask_qty": int(data.get("sq1", 0)),
"symbol": symbol, # Use OpenAlgo symbol from subscription
"broker_symbol": data.get("ts", ""), # Keep broker symbol for reference
"timestamp": datetime.now().isoformat(),
Expand Down Expand Up @@ -502,10 +504,10 @@ def _process_depth_data(self, data):
else:
# Fallback to broker symbol from AliceBlue data
symbol = data.get("ts", f"TOKEN_{token}")
logger.warning(
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
logger.debug(
f"Using broker symbol: {symbol} for {subscription_key} (subscription not found)"
)
logger.warning(f"Available subscriptions: {list(self.subscriptions.keys())}")
logger.debug(f"Available subscriptions: {list(self.subscriptions.keys())}")

# Use consistent key format for data storage: exchange:token
key = f"{exchange}:{token}"
Expand Down Expand Up @@ -543,6 +545,12 @@ def _process_depth_data(self, data):
"token": token,
"bids": bids,
"asks": asks,
"open": float(data.get("o", 0)),
"high": float(data.get("h", 0)),
"low": float(data.get("l", 0)),
"close": float(data.get("c", 0)),
"volume": int(data.get("v", 0)),
"last_trade_quantity": int(data.get("ltq", 0)),
"total_buy_quantity": int(data.get("tbq", 0)),
"total_sell_quantity": int(data.get("tsq", 0)),
"ltp": float(data.get("lp", 0)),
Expand Down Expand Up @@ -576,6 +584,16 @@ def _process_depth_data(self, data):
# Update specific fields if they exist in the feed
if "lp" in data:
depth["ltp"] = float(data.get("lp", 0))
if "o" in data:
depth["open"] = float(data.get("o", 0))
if "h" in data:
depth["high"] = float(data.get("h", 0))
if "l" in data:
depth["low"] = float(data.get("l", 0))
if "c" in data:
depth["close"] = float(data.get("c", 0))
if "v" in data:
depth["volume"] = int(data.get("v", 0))
if "pc" in data:
depth["percent_change"] = float(data.get("pc", 0))
if "ft" in data:
Expand Down
114 changes: 52 additions & 62 deletions broker/aliceblue/api/auth_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import base64
import hashlib
import json
import os
Expand All @@ -11,40 +10,51 @@
logger = get_logger(__name__)


def authenticate_broker(userid, encKey):
def authenticate_broker(userid, authCode):
"""
Authenticate with AliceBlue using the new V2 vendor API.

Returns:
Tuple of (userSession, clientId, error_message)

Flow:
1. Compute SHA-256 checksum of: userId + authCode + apiSecret
2. POST {"checkSum": checksum} to /open-api/od/v1/vendor/getUserDetails
3. Return the userSession from the response

Environment variables:
BROKER_API_KEY = App Code (appCode)
BROKER_API_SECRET = API Secret (apiSecret)
"""
try:
# Fetching the necessary credentials from environment variables
BROKER_API_KEY = os.environ.get("BROKER_API_KEY")
# BROKER_API_KEY = appCode (used for the login redirect, not needed here)
# BROKER_API_SECRET = apiSecret (used to build the checksum)
BROKER_API_SECRET = os.environ.get("BROKER_API_SECRET")

if not BROKER_API_SECRET or not BROKER_API_KEY:
logger.error("API keys not found in environment variables")
return None, "API keys not set in environment variables"
if not BROKER_API_SECRET:
logger.error("BROKER_API_SECRET not found in environment variables")
return None, None, "API secret not set in environment variables"

logger.debug(f"Authenticating with AliceBlue for user {userid}")

# Proper AliceBlue API authentication flow according to V2 API docs:
# Step 1: Get the shared httpx client with connection pooling
client = get_httpx_client()

# Step 2: Generate SHA-256 hash using the combination of User ID + API Key + Encryption Key
# This is the pattern specified in their API docs
# Step 2: Generate SHA-256 checksum = hash(userId + authCode + apiSecret)
logger.debug("Generating checksum for authentication")
# The AliceBlue API V2 documentation specifies: User ID + API Key + Encryption Key
# This is the official order specified in their documentation
checksum_input = f"{userid}{BROKER_API_SECRET}{encKey}"
logger.debug("Checksum input pattern: userId + apiSecret + encKey")
checksum_input = f"{userid}{authCode}{BROKER_API_SECRET}"
logger.debug("Checksum input pattern: userId + authCode + apiSecret")
checksum = hashlib.sha256(checksum_input.encode()).hexdigest()

# Step 3: Prepare request payload with exact parameters matching their API documentation
payload = {"userId": userid, "userData": checksum, "source": "WEB"}
# Step 3: Prepare request payload matching the new API documentation
payload = {"checkSum": checksum}

# Set the headers exactly as expected by their API
headers = {"Content-Type": "application/json", "Accept": "application/json"}

# Step 4: Make the API request to get session ID
logger.debug("Making getUserSID request to AliceBlue API")
url = "https://ant.aliceblueonline.com/rest/AliceBlueAPIService/api/customer/getUserSID"
# Step 4: POST to the new vendor getUserDetails endpoint
logger.debug("Making getUserDetails request to AliceBlue API")
url = "https://ant.aliceblueonline.com/open-api/od/v1/vendor/getUserDetails"
response = client.post(url, json=payload, headers=headers)

logger.debug(f"AliceBlue API response status: {response.status_code}")
Expand All @@ -53,57 +63,37 @@ def authenticate_broker(userid, encKey):
# Log full response for debugging
logger.info(f"AliceBlue API response: {json.dumps(data_dict, indent=2)}")

# Extract the session ID from the response
# Handle all possible response formats from AliceBlue API
# --- Parse the response ---

# Success case: stat == "Ok" and userSession is present
if data_dict.get("stat") == "Ok" and data_dict.get("userSession"):
client_id = data_dict.get("clientId")
logger.info(f"Authentication successful for user {userid} (clientId={client_id})")
return data_dict["userSession"], client_id, None

# Case 1: Check if response has sessionID field (typical success case)
if data_dict.get("sessionID"):
logger.info(f"Authentication successful for user {userid}")
return data_dict.get("sessionID"), None
# Error case: stat == "Not_ok" with an error message
if data_dict.get("stat") == "Not_ok":
error_msg = data_dict.get("emsg", "Unknown error occurred")
logger.error(f"API returned Not_ok: {error_msg}")
return None, None, f"API error: {error_msg}"

# Case 2: Check for specific error messages and handle them
# Fallback: check for emsg in any other shape of response
if "emsg" in data_dict and data_dict["emsg"]:
error_msg = data_dict["emsg"]
# Special case handling for common errors
if "User does not login" in error_msg:
logger.error(f"User not logged in: {error_msg}")
return (
None,
"User is not logged in. Please login to the AliceBlue platform first and then try again.",
)
elif "Invalid Input" in error_msg:
logger.error(f"Invalid input error: {error_msg}")
return None, "Invalid input. Please check your user ID and API credentials."
else:
logger.error(f"API error: {error_msg}")
return None, f"API error: {error_msg}"

# Case 3: Handle status field
if data_dict.get("stat") == "Not ok":
error_msg = data_dict.get("emsg", "Unknown error occurred")
logger.error(f"API returned Not ok status: {error_msg}")
return None, f"API error: {error_msg}"

# Case 4: Try to find any field that might contain the session token
for field_name in ["sessionID", "session_id", "sessionId", "token"]:
if field_name in data_dict and data_dict[field_name]:
session_id = data_dict[field_name]
logger.info(f"Found session ID in field {field_name}")
return session_id, None

# Case 5: If we got this far, we couldn't find a session ID
logger.error(f"Couldn't extract session ID from response: {data}")
logger.error(f"API error: {error_msg}")
return None, None, f"API error: {error_msg}"

# If we got here, we couldn't find a session token
logger.error(f"Couldn't extract userSession from response: {data_dict}")
return (
None,
"Failed to extract session ID from response. Please check API credentials and try again.",
None,
"Failed to extract session from response. Please check API credentials and try again.",
)

except json.JSONDecodeError:
# Handle invalid JSON response
return None, "Invalid response format from AliceBlue API."
return None, None, "Invalid response format from AliceBlue API."
except httpx.HTTPError as e:
# Handle HTTPX connection errors
return None, f"HTTP connection error: {str(e)}"
return None, None, f"HTTP connection error: {str(e)}"
except Exception as e:
# General exception handling
return None, f"An exception occurred: {str(e)}"
return None, None, f"An exception occurred: {str(e)}"
Loading
Loading