Skip to content

Commit 5dbdccc

Browse files
Overhaul sessions guide (#1801)
1 parent a3d76c3 commit 5dbdccc

22 files changed

+754
-2610
lines changed

malta.config.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
"title": "Sessions",
1010
"pages": [
1111
["Overview", "/sessions/overview"],
12-
["Basic API", "/sessions/basic-api"],
13-
["Cookies", "/sessions/cookies"]
12+
["Basic implementation", "/sessions/basic"],
13+
["Inactivity timeout", "/sessions/inactivity-timeout"],
14+
["Stateless tokens", "/sessions/stateless-tokens"],
15+
["Frameworks", "/sessions/frameworks"]
1416
]
1517
},
1618
{

pages/index.md

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@ title: "Lucia"
44

55
# Lucia
66

7-
_Lucia is now a learning resource on implementing auth from scratch. See the [announcement](https://github.com/lucia-auth/lucia/discussions/1714) for details and migration path._
8-
9-
Lucia is an open source project to provide resources on implementing authentication with JavaScript and TypeScript.
10-
11-
The main section is on implementing sessions with your database, library, and framework of choice. Using the API you just created, you can continue learning by going through the tutorials or by referencing one of the fully-fledged examples.
7+
Lucia is an open source project to provide resources on implementing authentication using JavaScript and TypeScript.
128

139
If you have any questions on auth, feel free to ask them in our [Discord server](https://discord.com/invite/PwrK3kpVR3) or on [GitHub Discussions](https://github.com/lucia-auth/lucia/discussions)!
1410

15-
## Why not a library?
16-
17-
We've found it extremely hard to develop a library that:
18-
19-
1. Supports the many database libraries, ORMs, frameworks, runtimes, and deployment options available in the ecosystem.
20-
2. Provides enough flexibility for the majority of use cases.
21-
3. Does not add significant complexity to projects.
11+
## Implementation notes
2212

23-
We came to the conclusion that at least for the core of auth - sessions - it's better to teach the code and concepts rather than to try cramming it into a library. The code is very straightforward and shouldn't take more than 10 minutes to write it once you understand it. As an added bonus, it's fully customizable.
13+
- The code example in this website uses the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) (`crypto`). It's not anything great but it is available in many modern runtimes. Use whatever secure crypto package is available in your runtime.
14+
- We may also reference packages from the [Oslo project](https://oslojs.dev). As a disclaimer, this package is written by the main author of Lucia. These packages are runtime-agnostic and light-weight, but can be considered as a placeholder for your own implementation or preferred packages.
15+
- SQLite is used for SQL queries but the TypeScript code uses a placeholder database client.
2416

2517
## Related projects
2618

pages/lucia-v3/migrate.md

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,119 @@ Lucia v3 has been deprecated. Lucia is now a learning resource for implementing
1010

1111
We ultimately came to the conclusion that it'd be easier and faster to just implement sessions from scratch. The database adapter model wasn't flexible enough for such a low-level library and severely limited the library design.
1212

13-
## Migration path
13+
## Migrating your project
1414

1515
Replacing Lucia v3 with your own implementation should be a straight-forward path, especially since most of your knowledge will still be very useful. No database migrations are necessary.
1616

17-
APIs on sessions are covered in the [Basic session API](/sessions/basic-api) page.
17+
If you're fine with invalidating all sessions (and signing out everyone), consider reading through the [new implementation guide](/sessions/basic).
1818

19-
- `Lucia.createSession()` => `generateSessionToken()` and `createSession()`
20-
- `Lucia.validateSession()` => `validateSessionToken()`
21-
- `Lucia.invalidateSession()` => `invalidateSession()`
19+
### Sessions
2220

23-
APIs on cookies are covered in the [Session cookies](/sessions/cookies) page.
21+
```ts
22+
function generateSessionId(): string {
23+
const bytes = new Uint8Array(25);
24+
crypto.getRandomValues(bytes);
25+
const token = encodeBase32LowerCaseNoPadding(bytes);
26+
return token;
27+
}
2428

25-
- `Lucia.createSessionCookie()` => `setSessionTokenCookie()`
26-
- `Lucia.createBlankSessionCookie()` => `deleteSessionTokenCookie()`
29+
const sessionExpiresInSeconds = 60 * 60 * 24 * 30; // 30 days
2730

28-
The one change to how sessions work is that session tokens are now hashed before storage. The pre-hash token is the client-assigned session ID and the hash is the internal session ID. The easiest option would be to purge all existing sessions, but if you want keep existing sessions, SHA-256 and hex-encode the session IDs stored in the database. Or, you can skip the hashing altogether. Hashing is a good measure against database leaks, but not absolutely necessary.
31+
export function createSession(dbPool: DBPool, userId: number): Promise<Session> {
32+
const now = new Date();
33+
const sessionId = generateSessionId();
34+
const session: Session = {
35+
id: sessionId,
36+
userId,
37+
expiresAt: new Date(now.getTime() + 1000 * sessionExpiresInSeconds)
38+
};
39+
await executeQuery(
40+
dbPool,
41+
"INSERT INTO user_session (id, user_id, expires_at) VALUES (?, ?, ?)",
42+
[session.id, session.userId, Math.floor(session.expiresAt.getTime() / 1000)]
43+
);
44+
return session;
45+
}
2946

30-
```ts
31-
export function createSession(userId: number): Session {
32-
const bytes = new Uint8Array(20);
33-
crypto.getRandomValues(bytes);
34-
const sessionId = encodeBase32LowerCaseNoPadding(bytes);
35-
// Insert session into database.
47+
export function validateSession(dbPool: DBPool, sessionId: string): Promise<Session | null> {
48+
const now = Date.now();
49+
const result = dbPool.executeQuery(
50+
dbPool,
51+
"SELECT id, user_id, expires_at FROM session WHERE id = ?",
52+
[sessionId]
53+
);
54+
if (result.rows.length < 1) {
55+
return null;
56+
}
57+
const row = result.rows[0];
58+
const session: Session = {
59+
id: row[0],
60+
userId: row[1],
61+
expiresAt: new Date(row[2] * 1000)
62+
};
63+
if (now.getTime() >= session.expiresAt.getTime()) {
64+
await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [session.id]);
65+
return null;
66+
}
67+
if (now.getTime() >= session.expiresAt.getTime() - (1000 * sessionExpiresInSeconds) / 2) {
68+
session.expiresAt = new Date(Date.now() + 1000 * sessionExpiresInSeconds);
69+
await executeQuery(dbPool, "UPDATE session SET expires_at = ? WHERE id = ?", [
70+
Math.floor(session.expiresAt.getTime() / 1000),
71+
session.id
72+
]);
73+
}
3674
return session;
3775
}
3876

39-
export function validateSessionToken(sessionId: string): SessionValidationResult {
40-
// Get and validate session
41-
return { session, user };
77+
export async function invalidateSession(dbPool: DBPool, sessionId: string): Promise<void> {
78+
await executeQuery(dbPool, "DELETE FROM user_session WHERE id = ?", [sessionId]);
79+
}
80+
81+
export async function invalidateAllSessions(dbPool: DBPool, userId: number): Promise<void> {
82+
await executeQuery(dbPool, "DELETE FROM user_session WHERE user_id = ?", [userId]);
83+
}
84+
85+
export interface Session {
86+
id: string;
87+
userId: number;
88+
expiresAt: Date;
4289
}
4390
```
4491

45-
If you need help or have any questions, please ask them on [Discord](https://discord.com/invite/PwrK3kpVR3) or on [GitHub discussions](https://github.com/lucia-auth/lucia/discussions).
92+
### Cookies
93+
94+
Cookies should have the following attributes:
95+
96+
- `HttpOnly`: Cookies are only accessible server-side.
97+
- `SameSite=Lax`: Use Strict for critical websites.
98+
- `Secure`: Cookies can only be sent over HTTPS (should be omitted when testing on localhost).
99+
- `Max-Age` or `Expires`: Must be defined to persist cookies.
100+
- `Path=/`: Cookies can be accessed from all routes.
101+
102+
```ts
103+
export function setSessionCookie(response: HTTPResponse, sessionId: string, expiresAt: Date): void {
104+
if (env === ENV.PROD) {
105+
response.headers.add(
106+
"Set-Cookie",
107+
`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure;`
108+
);
109+
} else {
110+
response.headers.add(
111+
"Set-Cookie",
112+
`session=${sessionId}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/`
113+
);
114+
}
115+
}
116+
117+
// Set empty session cookie that expires immediately.
118+
export function deleteSessionCookie(response: HTTPResponse): void {
119+
if (env === ENV.PROD) {
120+
response.headers.add(
121+
"Set-Cookie",
122+
"session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure;"
123+
);
124+
} else {
125+
response.headers.add("Set-Cookie", "session=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/");
126+
}
127+
}
128+
```

0 commit comments

Comments
 (0)