Technical
Feb 1, 2024
Aneesh Arora
Cofounder and CTO
External user authentication is not as simple, secure and cheap as it seems
Most startups nowadays rely on external user authentication services called "Identity as a Service" (IDaaS) or "Authentication as a Service" (AuthaaS). Even big companies like OpenAI use auth0 . Firebase by Google is also really popular. While I guess it makes sense in the world of “move fast and break things” we have been sold a lot of lies about user authentication by these providers which I would like to dispel.
Developers think outsourcing user authentication to an external provider is easier, more secure, cheaper in the short run but it’s not true. If you don’t understand security well enough you will still store important information like user authentication tokens in localstorage or cookies that can be read by client side javascript. Javascript injection attacks will allow hackers to steal user credentials and pretend to be them, exposing user data and consuming their credits in a Saas application.
User authentication and authorization may initially appear challenging and intimidating, but this perception changes once you grasp the underlying mechanisms. Moreover, it's an essential aspect you can't bypass. Despite popular belief, third-party user authentication isn't the panacea it's often made out to be.
In the sections below, I'll guide you through a secure method for implementing user authentication and authorization. While there are other approaches, they fall beyond the scope of this blog post.
First let’s understand the difference between user authentication and user authorization:
Upon successful user authentication, that is, when they log in, the server issues a cookie containing a token(a unique string). This cookie is sent by the browser with each subsequent request, allowing the server to verify the user's identity and respond to their requests.
There are two primary methods to verify this token:
Many libraries support JWT so you don’t have to implement it yourself but you do need to understand it. One good one for python is PyJWT.
pip install PyJWT
JWTs are advantageous as they encapsulate user details, such as the user ID, along with other pertinent information chosen by the developer. This data is encrypted using a secret key and stored in the cookie. When the user makes a request, the server decrypts the JWT to identify the user and access their data.
Benefits of using JWT include:
At Afterword, for instance, access tokens expire after one hour for added user safety.
Example code to create and decode JWT in python using PyJWT:
import jwt
from jwt import PyJWTError
from datetime import datetime, timedelta
SECRET_KEY = "Generate a secret key and put it here"
ALGORITHM = "HS256"
TOKEN_EXPIRE_MINUTES = 60
def create_jwt(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# Verify JWT token
def decode_token(token):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except PyJWTError:
raise Exception(status_code=401, detail="Could not validate credentials")
create_jwt({"user_id":user_id}) #you can also put other user data in this dict as key value pairs
To address the issue of token expiration and avoid frequent re-logins, which could detract from user experience, we employ a dual-token approach:
The refresh token is only sent to the server when an access token is rejected, not with every request. This strategy minimizes the risk of a refresh token being compromised. For heightened security, the refresh token can be stored in the database and validated against it. Upon user logout, or to prevent misuse, refresh tokens can be either deleted or added to a blacklist, ensuring that outdated, yet valid, tokens cannot be used to gain unauthorized access.
Since local storage and other storage methods like IndexedDB can be accessed by javascript running in the browser they are not a safe place to store user authorization credentials. Hackers can exploit it using Cross-Site Scripting (XSS) to inject malicious code and access these tokens.
An alternative to this is the use of cookies. While regular cookies can be accessed by JavaScript, HTTP-only cookies are more secure as they are accessible only to the server. The browser is designed to prevent any JavaScript from reading HTTP-only cookies. Moreover, cookies set by the server should be flagged as 'Secure' to ensure they are transmitted exclusively over HTTPS connections, not unsecured HTTP.
Another critical attribute of cookies for security is the 'SameSite' flag. Setting this flag to either 'Strict' or 'Lax', instead of 'None', significantly enhances security. This configuration helps prevent the browser from sending the user's cookies in response to Cross-Site Request Forgery (CSRF) attacks. When combined with the HTTP-only and Secure flags, setting SameSite to 'Strict' provides robust protection by safeguarding against XSS, CSRF, and ensuring that all data is transmitted securely over HTTPS.
For additional layers of security, the implementation of CSRF tokens can be considered.
Example of setting a secure, HTTP-only and SameSite="Strict" using a fastapi server:
from fastapi import FastAPI, Response, Form
from typing import Annotated
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Set up CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=[FRONTEND_URL],
allow_credentials=True,
allow_methods=["*"], # Allows all methods
allow_headers=["*"], # Allows all headers
)
TOKEN_EXPIRE_MINUTES = 60
@app.post("/login")
async def login(email: Annotated[str, Form()], password: Annotated[str, Form()], response: Response):
user = #Retrieve user from database using email
if user and password == user["password"]: #Don't store password in plain text in a real application
response.set_cookie("token", value=JWT_token, max_age=TOKEN_EXPIRE_MINUTES*60, httponly=True, secure=True, samesite="Strict", domain="your site domain")
return True
else:
return False
This provides a solid foundation in understanding the user authentication and authorization flow, valuable knowledge regardless of whether you opt for an external provider. To develop a fully in-house user authentication and authorization system, more in-depth information is required. The second part of this blog post covers password encryption and the intricacies of sending emails.