Skip to content

Commit

Permalink
Rework CSRF protection (#812)
Browse files Browse the repository at this point in the history
  • Loading branch information
pilcrowonpaper authored Jul 6, 2023
1 parent 4d612a9 commit 7df238f
Show file tree
Hide file tree
Showing 24 changed files with 258 additions and 33 deletions.
6 changes: 6 additions & 0 deletions .auri/$2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "lucia" # package name
type: "major" # "major", "minor", "patch"
---

Remove `allowedRequestOrigins` configuration
6 changes: 6 additions & 0 deletions .auri/$r.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-mongoose" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$toles4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-mysql" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$tolesr2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-postgresql" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$tolesr3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-prisma" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$tolesrm1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-session-redis" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$tolesrm2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-sqlite" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$tolesrm6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/adapter-test" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
6 changes: 6 additions & 0 deletions .auri/$tolesrm6r.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
package: "@lucia-auth/oauth" # package name
type: "minor" # "major", "minor", "patch"
---

Update peer dependency
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,12 @@ const auth = lucia({
},

// autoDatabaseCleanup: false, <= removed for now
csrfProtection: true, // no change
// generateCustomUserId, <= removed
csrfProtection: {
allowedSubdomains: ["foo"] // allow https://foo.example.com
} // can be boolean
// generateCustomUserId, <= removed, see `csrfProtection`
passwordHash, // previously `hash`
allowedRequestOrigins: ["https://foo.example.com"], // previously `origin`
// origin, <= removed
sessionCookie: {
name: "user_session", // session cookie name
attributes: {
Expand Down
2 changes: 1 addition & 1 deletion documentation-v2/content/main/1.basics/5.using-cookies.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ setResponseHeaders("Set-Cookie", sessionCookie.serialize());

## Validate request origin

[`Auth.validateRequestOrigin()`](/reference/lucia/interfaces/auth#validaterequestorigin) prevents CSRF by checking if the source of the request is from a trusted host (origin) by comparing the request url and the origin header. Trusted origins include where the API is hosted and origins defined in [`allowedRequestOrigins`](/basics/configuration#allowedrequestorigins) configuration. This check is only done on POST and other non-GET method requests. **GET requests are not protected by Lucia and they should not modify server state (e.g. update password and profile) without additional protections.**
[`Auth.validateRequestOrigin()`](/reference/lucia/interfaces/auth#validaterequestorigin) prevents CSRF by checking if the source of the request is from a trusted host (origin) by comparing the request url and the origin header. Trusted origins include where the server is hosted and its subdomains defined with [`csrfProtection.allowedSubdomains`](/basics/configuration#csrfprotection) configuration. This check is only done on POST and other non-GET method requests. **GET requests are not protected by Lucia and they should not modify server state (e.g. update password and profile) without additional protections.**

```ts
import { auth } from "./lucia.js";
Expand Down
32 changes: 19 additions & 13 deletions documentation-v2/content/main/1.basics/7.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ type Configuration = {
env: "DEV" | "PROD";

// optional
allowedRequestOrigins: string[];
csrfProtection?: boolean;
csrfProtection?:
| boolean
| {
allowedSubdomains: "*" | string[];
};
getSessionAttributes?: (databaseSession: SessionSchema) => Record<any, any>;
getUserAttributes?: (databaseUser: UserSchema) => Record<any, any>;
middleware?: Middleware<any>;
Expand Down Expand Up @@ -80,22 +83,25 @@ Provides Lucia with the current server context.

## Optional

### `allowedRequestOrigins`
### `csrfProtection`

`true` by default. When set to `true`, [`AuthRequest.validate()`](/reference/lucia/interfaces/authrequest#validate) checks if the incoming request is from a trusted origin, which by default only includes where the server is hosted. You can define trusted subdomains by adding them to `csrfProtection.allowedSubdomains`. If your app is hosted on `https://foo.example.com`, adding `"bar"` will allow `https://bar.example.com`.

```ts
const allowedRequestOrigins: string[];
const csrfProtection = boolean | {
allowedSubdomains: "*" | string[]
}
```

A list of allowed request origins for CSRF check used by [`Auth.validateRequestOrigin()`](/reference/lucia/interfaces/auth#validaterequestorigin) and [`AuthRequest.validate()`](/reference/lucia/interfaces/authrequest#validate). Does not accept wildcard `*`.

### `csrfProtection`

Enabled by default. When enabled, [`AuthRequest.validate()`](/reference/lucia/interfaces/authrequest#validate) checks if the incoming request is from a trusted origin. Trusted origin includes the origin where the server is hosted and those defined in [`allowedRequestOrigins`](/basics/configuration#allowedrequestorigins) configuration.
| value | description |
| -------- | ----------------------------------- |
| `true` | CSRF protection enabled |
| `false` | CSRF protection disabled |
| `object` | CSRF protection enabled - see below |

| value | description |
| ------- | ------------------------ |
| `true` | CSRF protection enabled |
| `false` | CSRF protection disabled |
| name | type | description |
| ------------------- | ----------------- | ------------------------------------------------------------------------------------ |
| `allowedSubdomains` | `"*" \| string[]` | List of allowed subdomains (not full urls/origins) - set to `*` allow all subdomains |

### `getSessionAttributes()`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,7 @@ const key = await auth.useKey("github", githubUserId, null);

## `validateRequestOrigin()`

Used for CSRF protection. Checks if the request origin is trusted for non-GET and non-HEAD requests (e.g. POST, PUT, DELETE), and throws an error if the origin is invalid. Trusted origins include the request url and those defined in [`allowedRequestOrigins`](/basics/configuration#allowedrequestorigins) configuration.
Used for CSRF protection. Checks if the request origin is trusted for non-GET and non-HEAD requests (e.g. POST, PUT, DELETE), and throws an error if the origin is invalid. Trusted origins include where the server is hosted and its subdomains defined with [`csrfProtection.allowedSubdomains`](/basics/configuration#csrfprotection) configuration.

```ts
const validateRequestOrigin: (request: {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-mongoose/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
".": "./dist/index.js"
},
"peerDependencies": {
"lucia": "2.0.0-beta.4",
"lucia": "2.0.0-beta.5",
"mongoose": "6.x - 7.x"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-mysql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
".": "./dist/index.js"
},
"peerDependencies": {
"lucia": "2.0.0-beta.4",
"lucia": "2.0.0-beta.5",
"mysql2": "^3.0.0",
"@planetscale/database": "^1.0.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-postgresql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
".": "./dist/index.js"
},
"peerDependencies": {
"lucia": "2.0.0-beta.4",
"lucia": "2.0.0-beta.5",
"pg": "^8.8.0",
"postgres": "^3.3.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-prisma/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
},
"peerDependencies": {
"@prisma/client": "^4.2.0",
"lucia": "2.0.0-beta.4"
"lucia": "2.0.0-beta.5"
},
"devDependencies": {
"lucia": "latest",
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-session-redis/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
".": "./dist/index.js"
},
"peerDependencies": {
"lucia": "2.0.0-beta.4",
"lucia": "2.0.0-beta.5",
"redis": "^4.0.0",
"@upstash/redis": "^1.20.0"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-sqlite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"peerDependencies": {
"better-sqlite3": "^8.0.0",
"@libsql/client": "^0.2.1",
"lucia": "2.0.0-beta.4"
"lucia": "2.0.0-beta.5"
},
"peerDependenciesMeta": {
"better-sqlite3": {
Expand Down
2 changes: 1 addition & 1 deletion packages/adapter-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"lucia": "latest"
},
"peerDependencies": {
"lucia": "2.0.0-beta.4"
"lucia": "2.0.0-beta.5"
},
"dependencies": {
"mocha": "^10.2.0"
Expand Down
31 changes: 24 additions & 7 deletions packages/lucia/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AuthRequest } from "./request.js";
import { lucia as defaultMiddleware } from "../middleware/index.js";
import { debug } from "../utils/debug.js";
import { isWithinExpiration } from "../utils/date.js";
import { isAllowedUrl } from "../utils/url.js";

import type { Cookie, SessionCookieAttributes } from "./cookie.js";
import type { UserSchema, SessionSchema } from "./schema.js";
Expand Down Expand Up @@ -78,7 +79,7 @@ export class Auth<_Configuration extends Configuration = any> {
? _Configuration["middleware"]
: ReturnType<typeof defaultMiddleware>;
public csrfProtectionEnabled: boolean;
private allowedRequestOrigins: string[];
private allowedSubdomains: string[] | "*";
private experimental: {
debugMode: boolean;
};
Expand All @@ -89,16 +90,19 @@ export class Auth<_Configuration extends Configuration = any> {
if ("user" in config.adapter) {
let userAdapter = config.adapter.user(LuciaError);
let sessionAdapter = config.adapter.session(LuciaError);

if ("getSessionAndUserBySessionId" in userAdapter) {
const { getSessionAndUserBySessionId: _, ...extractedUserAdapter } =
userAdapter;
userAdapter = extractedUserAdapter;
}

if ("getSessionAndUserBySessionId" in sessionAdapter) {
const { getSessionAndUserBySessionId: _, ...extractedSessionAdapter } =
sessionAdapter;
sessionAdapter = extractedSessionAdapter;
}

this.adapter = {
...userAdapter,
...sessionAdapter
Expand All @@ -107,7 +111,8 @@ export class Auth<_Configuration extends Configuration = any> {
this.adapter = config.adapter(LuciaError);
}
this.env = config.env;
this.csrfProtectionEnabled = config.csrfProtection ?? true;
this.csrfProtectionEnabled =
typeof config.csrfProtection === "boolean" ? config.csrfProtection : true;
this.sessionExpiresIn = {
activePeriod:
config.sessionExpiresIn?.activePeriod ?? 1000 * 60 * 60 * 24,
Expand Down Expand Up @@ -139,7 +144,10 @@ export class Auth<_Configuration extends Configuration = any> {
validate: config.passwordHash?.validate ?? validateScryptHash
};
this.middleware = config.middleware ?? defaultMiddleware();
this.allowedRequestOrigins = config.allowedRequestOrigins ?? [];
this.allowedSubdomains =
!config.csrfProtection || typeof config.csrfProtection === "boolean"
? []
: config.csrfProtection.allowedSubDomains;
this.experimental = {
debugMode: config.experimental?.debugMode ?? false
};
Expand Down Expand Up @@ -219,10 +227,12 @@ export class Auth<_Configuration extends Configuration = any> {
if (this.adapter.getSessionAndUser) {
const [databaseSession, databaseUser] =
await this.adapter.getSessionAndUser(sessionId);

if (!databaseSession) {
debug.session.fail("Session not found", sessionId);
throw new LuciaError("AUTH_INVALID_SESSION_ID");
}

if (!isValidDatabaseSession(databaseSession)) {
debug.session.fail(
`Session expired at ${new Date(
Expand All @@ -232,6 +242,7 @@ export class Auth<_Configuration extends Configuration = any> {
);
throw new LuciaError("AUTH_INVALID_SESSION_ID");
}

return [databaseSession, databaseUser];
}
const databaseSession = await this.getDatabaseSession(sessionId);
Expand Down Expand Up @@ -495,9 +506,11 @@ export class Auth<_Configuration extends Configuration = any> {
try {
const url = new URL(request.url);
if (
![url.origin, ...this.allowedRequestOrigins].includes(requestOrigin)
!isAllowedUrl(requestOrigin, {
url,
allowedSubdomains: this.allowedSubdomains
})
) {
debug.request.fail("Invalid request origin", requestOrigin);
throw new LuciaError("AUTH_INVALID_REQUEST");
}
debug.request.info("Valid request origin", requestOrigin);
Expand Down Expand Up @@ -657,8 +670,12 @@ export type Configuration<
env: Env;

middleware?: Middleware;
csrfProtection?: boolean;
allowedRequestOrigins?: string[];
csrfProtection?:
| boolean
| {
baseDomain: string;
allowedSubDomains: string[] | "*";
};
sessionExpiresIn?: {
activePeriod: number;
idlePeriod: number;
Expand Down
Loading

0 comments on commit 7df238f

Please sign in to comment.