Skip to content

Session Quick Start

Sessions are how your application remembers that a user is logged in. This guide will get you from zero to a working login/logout flow in just a few minutes.

By the end, you’ll have:

  • A backend endpoint that creates sessions on login
  • Session validation for protected routes
  • A complete logout flow

Before starting, make sure you have:

  1. PropelAuth BYO installed and running
  2. Backend client library set up
  3. A way to authenticate users (we’ll assume you have this - could be email/password, OAuth, etc.)

When a user successfully logs in, create a session for them and store it as an HTTP-only cookie:

app.post("/api/login", async (req, res) => {
// Verify the user's credentials (your existing logic)
const userId = await verifyUserCredentials(req.body);
const result = await client.session.create({
userId: userId,
});
if (!result.ok) {
console.error("Session creation failed:", result.error);
return res.status(500).json({ error: "Failed to create session" });
}
// Store the session token as an HTTP-only cookie
res.cookie("sessionToken", result.data.sessionToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
});
res.json({ success: true });
});
class LoginRequest(BaseModel):
# Your login fields
pass
@app.post("/api/login")
async def login(request: LoginRequest, response: Response):
# Verify the user's credentials (your existing logic)
user_id = await verify_user_credentials(request)
result = await client.session.create(user_id=user_id)
if is_err(result):
print("Session creation failed:", result.error)
raise HTTPException(status_code=500, detail="Failed to create session")
# Store the session token as an HTTP-only cookie
response.set_cookie(
key="sessionToken",
value=result.data.session_token,
httponly=True,
secure=True,
samesite="lax"
)
return {"success": True}
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request, HttpServletResponse response) {
// Verify the user's credentials (your existing logic)
String userId = verifyUserCredentials(request);
try {
CreateSessionResponse result = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.build()
);
// Store the session token as an HTTP-only cookie
Cookie cookie = new Cookie("sessionToken", result.getSessionToken());
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
response.addCookie(cookie);
return ResponseEntity.ok(Map.of("success", true));
} catch (CreateSessionException e) {
System.err.println("Session creation failed: " + e.getMessage());
return ResponseEntity.status(500).body(Map.of("error", "Failed to create session"));
}
}
[HttpPost("/api/login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
// Verify the user's credentials (your existing logic)
var userId = await VerifyUserCredentials(request);
try
{
var result = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId
});
// Store the session token as an HTTP-only cookie
Response.Cookies.Append("sessionToken", result.SessionToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Lax
});
return Ok(new { Success = true });
}
catch (CreateSessionException ex)
{
Console.Error.WriteLine($"Session creation failed: {ex.Message}");
return StatusCode(500, new { Error = "Failed to create session" });
}
}

Now when the user logs in, they’ll receive a cookie that automatically gets sent with every request.

Step 2: Validate Sessions on Protected Routes

Section titled “Step 2: Validate Sessions on Protected Routes”

For any route that requires authentication, validate the session token:

app.get("/api/protected-route", async (req, res) => {
const sessionToken = req.cookies.sessionToken;
const validation = await client.session.validate({
sessionToken,
});
if (!validation.ok) {
console.error("Session validation failed:", validation.error);
return res.status(401).json({ error: "Unauthorized" });
}
// The session is valid! You can access the user ID and metadata
const { userId, metadata } = validation.data;
// Your protected route logic here
res.json({
message: "You're authenticated!",
userId,
metadata,
});
});
@app.get("/api/protected-route")
async def protected_route(request: Request):
session_token = request.cookies.get("sessionToken")
validation = await client.session.validate(session_token=session_token)
if is_err(validation):
print("Session validation failed:", validation.error)
raise HTTPException(status_code=401, detail="Unauthorized")
# The session is valid! You can access the user ID and metadata
user_id = validation.data.user_id
metadata = validation.data.metadata
# Your protected route logic here
return {
"message": "You're authenticated!",
"userId": user_id,
"metadata": metadata,
}
@GetMapping("/api/protected-route")
public ResponseEntity<?> protectedRoute(@CookieValue(name = "sessionToken", required = false) String sessionToken) {
try {
ValidateSessionResponse validation = client.session.validate(
ValidateSessionCommand.builder()
.sessionToken(sessionToken)
.build()
);
// The session is valid! You can access the user ID and metadata
String userId = validation.getUserId();
JsonValue metadata = validation.getMetadata();
// Your protected route logic here
return ResponseEntity.ok(Map.of(
"message", "You're authenticated!",
"userId", userId,
"metadata", metadata
));
} catch (ValidateSessionException e) {
System.err.println("Session validation failed: " + e.getMessage());
return ResponseEntity.status(401).body(Map.of("error", "Unauthorized"));
}
}
[HttpGet("/api/protected-route")]
public async Task<IActionResult> ProtectedRoute()
{
var sessionToken = Request.Cookies["sessionToken"];
try
{
var validation = await client.Session.ValidateAsync(new ValidateSessionCommand
{
SessionToken = sessionToken
});
// The session is valid! You can access the user ID and metadata
var userId = validation.UserId;
var metadata = validation.Metadata;
// Your protected route logic here
return Ok(new
{
Message = "You're authenticated!",
UserId = userId,
Metadata = metadata
});
}
catch (ValidateSessionException ex)
{
Console.Error.WriteLine($"Session validation failed: {ex.Message}");
return StatusCode(401, new { Error = "Unauthorized" });
}
}

The Validate Session function checks if:

  • The token exists and is valid
  • The session hasn’t expired
  • The session hasn’t been invalidated

If validation succeeds, you get back the user ID and any metadata you stored when creating the session. If validation fails, you get a detailed error explaining exactly why (e.g. IP address not allowed, session expired, etc.).

Now let’s connect a frontend. Here’s a simple login form that sends credentials to your backend:

Login.tsx
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleLogin = async (e) => {
e.preventDefault();
const response = await fetch("/api/login", {
method: "POST",
credentials: "include", // Important: include cookies
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
// Redirect to dashboard or protected area
window.location.href = "/dashboard";
} else {
// Handle error
const error = await response.json();
alert(error.error);
}
};
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit">Log In</button>
</form>
);
}

Note: credentials: "include" ensures cookies are included when making requests to protected routes after login.

When users log out, invalidate their session to ensure it can’t be used again:

app.post("/api/logout", async (req, res) => {
const sessionToken = req.cookies.sessionToken;
await client.session.invalidateByToken({
sessionToken,
});
// Clear the cookie
res.clearCookie("sessionToken");
res.json({ success: true });
});
@app.post("/api/logout")
async def logout(request: Request, response: Response):
session_token = request.cookies.get("sessionToken")
await client.session.invalidate_by_token(session_token=session_token)
# Clear the cookie
response.delete_cookie("sessionToken")
return {"success": True}
@PostMapping("/api/logout")
public ResponseEntity<?> logout(@CookieValue(name = "sessionToken", required = false) String sessionToken, HttpServletResponse response) {
client.session.invalidateByToken(
InvalidateSessionByTokenCommand.builder()
.sessionToken(sessionToken)
.build()
);
// Clear the cookie
Cookie cookie = new Cookie("sessionToken", "");
cookie.setMaxAge(0);
response.addCookie(cookie);
return ResponseEntity.ok(Map.of("success", true));
}
[HttpPost("/api/logout")]
public async Task<IActionResult> Logout()
{
var sessionToken = Request.Cookies["sessionToken"];
await client.Session.InvalidateByTokenAsync(new InvalidateSessionByTokenCommand
{
SessionToken = sessionToken
});
// Clear the cookie
Response.Cookies.Delete("sessionToken");
return Ok(new { Success = true });
}

And the corresponding frontend:

Logout.tsx
function LogoutButton() {
const handleLogout = async () => {
const response = await fetch("/api/logout", {
method: "POST",
credentials: "include", // Include the session cookie
});
if (response.ok) {
// Redirect to login page
window.location.href = "/login";
}
};
return <button onClick={handleLogout}>Log Out</button>;
}

You now have a working session management system. Users can log in, stay logged in across requests, and securely log out.

Your basic session management is working, but you can enhance security with just a few additions:

// When creating the session
const session = await auth.session.create({
userId: userId,
userAgent: req.headers["user-agent"],
});
// When validating
const validation = await auth.session.validate({
sessionToken,
userAgent: req.headers["user-agent"],
});
# When creating the session
session = await client.session.create(
user_id=user_id,
user_agent=request.headers.get("user-agent")
)
# When validating
validation = await client.session.validate(
session_token=session_token,
user_agent=request.headers.get("user-agent")
)
// When creating the session
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.userAgent(request.getHeader("User-Agent"))
.build()
);
// When validating
ValidateSessionResponse validation = client.session.validate(
ValidateSessionCommand.builder()
.sessionToken(sessionToken)
.userAgent(request.getHeader("User-Agent"))
.build()
);
// When creating the session
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId,
UserAgent = Request.Headers["User-Agent"].ToString()
});
// When validating
var validation = await client.Session.ValidateAsync(new ValidateSessionCommand
{
SessionToken = sessionToken,
UserAgent = Request.Headers["User-Agent"].ToString()
});

Some changes in the user agent are suspicious (like switching from Mac to Windows), whereas others aren’t (like a minor Chrome version update). PropelAuth BYO automatically detects suspicious changes that could indicate a stolen session and invalidates the session for you.

// When creating the session
const session = await client.session.create({
userId: userId,
ipAddress: req.ip,
});
// When validating
const validation = await client.session.validate({
sessionToken,
ipAddress: req.ip,
});
# When creating the session
session = await client.session.create(
user_id=user_id,
ip_address=request.client.host
)
# When validating
validation = await client.session.validate(
session_token=session_token,
ip_address=request.client.host
)
// When creating the session
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.ipAddress(request.getRemoteAddr())
.build()
);
// When validating
ValidateSessionResponse validation = client.session.validate(
ValidateSessionCommand.builder()
.sessionToken(sessionToken)
.ipAddress(request.getRemoteAddr())
.build()
);
// When creating the session
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
});
// When validating
var validation = await client.Session.ValidateAsync(new ValidateSessionCommand
{
SessionToken = sessionToken,
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString()
});

Including IP addresses enables powerful features like IP allowlists/blocklists, better audit logging, and the ability to configure IP-based rules per customer.

const session = await client.session.create({
userId: userId,
metadata: {
email: user.email,
role: user.role,
teamId: user.teamId,
},
});
session = await client.session.create(
user_id=user_id,
metadata={
"email": user.email,
"role": user.role,
"teamId": user.team_id,
}
)
CreateSessionResponse session = client.session.create(
CreateSessionCommand.builder()
.userId(userId)
.metadata(JsonValue.of(Map.of(
"email", user.getEmail(),
"role", user.getRole(),
"teamId", user.getTeamId()
)))
.build()
);
var session = await client.Session.CreateAsync(new CreateSessionCommand
{
UserId = userId,
Metadata = JsonSerializer.SerializeToElement(new Dictionary<string, object>
{
{ "email", user.Email },
{ "role", user.Role },
{ "teamId", user.TeamId }
})
});

This data comes back when you validate the session, saving database lookups.

Now that you have sessions working, you can:

Check out the Session Overview for a complete guide to all features, or dive into the API Reference for detailed documentation.