Skip to content

Commit efb433a

Browse files
authored
Merge pull request #38 from jcs224/cookie-encryption
encrypt cookie storage with iron-webcrypto
2 parents ba7c276 + 6e5c968 commit efb433a

File tree

11 files changed

+91
-62
lines changed

11 files changed

+91
-62
lines changed

.github/workflows/deno.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
DENO_ENV: testing
2121
BASE_URL: http://localhost:8000
2222
DENO_DIR: deno_dir
23-
APP_KEY: not-secret-at-all
23+
APP_KEY: password-at-least-32-characters-long
2424
steps:
2525
- name: Setup repo
2626
uses: actions/checkout@v3

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ import {
2323

2424
### Setup secret key
2525

26-
Fresh Session currently uses [JSON Web Token](https://jwt.io/) under the hood to
27-
create and manage session in the cookies.
26+
Fresh Session currently uses
27+
[iron-webcrypto](https://github.com/brc-dd/iron-webcrypto) encrypted cookie
28+
contents.
2829

29-
JWT requires a secret key to sign new tokens. Fresh Session uses the secret key
30-
from your [environment variable](https://deno.land/std/dotenv/load.ts)
31-
`APP_KEY`.
30+
iron-webcrypto requires a secret key to encrypt the session payload. Fresh
31+
Session uses the secret key from your
32+
[environment variable](https://deno.land/std/dotenv/load.ts) `APP_KEY`.
3233

3334
If you don't know how to setup environment variable locally, I wrote
3435
[an article about .env file in Deno Fresh](https://xstevenyung.com/blog/read-.env-file-in-deno-fresh).
@@ -124,7 +125,7 @@ export const handler = [
124125

125126
## cookie session based on Redis
126127

127-
In addition to JWT, values can be stored in Redis.
128+
In addition to storing session data in cookies, values can be stored in Redis.
128129

129130
```ts
130131
import { redisSession } from "fresh-session/mod.ts";

src/deps.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
export type { Plugin, MiddlewareHandlerContext, MiddlewareHandler} from "https://deno.land/x/fresh@1.5.4/server.ts";
1+
export type {
2+
MiddlewareHandler,
3+
MiddlewareHandlerContext,
4+
Plugin,
5+
} from "https://deno.land/x/fresh@1.5.4/server.ts";
26
export {
37
type Cookie,
48
deleteCookie,
59
getCookies,
610
setCookie,
711
} from "https://deno.land/std@0.207.0/http/mod.ts";
8-
export { create, decode, verify } from "https://deno.land/x/djwt@v3.0.1/mod.ts";
12+
export {
13+
defaults as ironDefaults,
14+
seal,
15+
unseal,
16+
} from "https://deno.land/x/iron@v1.0.0/mod.ts";

src/plugins/cookie_session_plugin.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type {
2-
Plugin,
3-
MiddlewareHandlerContext,
42
MiddlewareHandler,
3+
MiddlewareHandlerContext,
4+
Plugin,
55
} from "../deps.ts";
66
import { cookieSession } from "../stores/cookie.ts";
77
import { CookieOptions } from "../stores/cookie_option.ts";
88
import { sessionModule } from "../stores/interface.ts";
99

10-
export function getCookieSessionHandler(session: sessionModule, excludePath: string[]): MiddlewareHandler {
10+
export function getCookieSessionHandler(
11+
session: sessionModule,
12+
excludePath: string[],
13+
): MiddlewareHandler {
1114
return function (req: Request, ctx: MiddlewareHandlerContext) {
1215
if (excludePath.includes(new URL(req.url).pathname)) {
1316
return ctx.next();
@@ -16,7 +19,11 @@ export function getCookieSessionHandler(session: sessionModule, excludePath: str
1619
};
1720
}
1821

19-
export function getCookieSessionPlugin(path = "/", excludePath = [], cookieOptions?: CookieOptions): Plugin {
22+
export function getCookieSessionPlugin(
23+
path = "/",
24+
excludePath = [],
25+
cookieOptions?: CookieOptions,
26+
): Plugin {
2027
const session = cookieSession(cookieOptions);
2128
const handler = getCookieSessionHandler(session, excludePath);
2229

src/plugins/kv_session_plugin.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type {
2-
Plugin,
3-
MiddlewareHandlerContext,
42
MiddlewareHandler,
3+
MiddlewareHandlerContext,
4+
Plugin,
55
} from "../deps.ts";
66
import { kvSession } from "../stores/kv.ts";
77
import { CookieOptions } from "../stores/cookie_option.ts";
88
import { sessionModule } from "../stores/interface.ts";
99

10-
export function getKvSessionHandler(session: sessionModule, excludePath: string[]): MiddlewareHandler {
10+
export function getKvSessionHandler(
11+
session: sessionModule,
12+
excludePath: string[],
13+
): MiddlewareHandler {
1114
return function (req: Request, ctx: MiddlewareHandlerContext) {
1215
if (excludePath.includes(new URL(req.url).pathname)) {
1316
return ctx.next();
@@ -16,7 +19,12 @@ export function getKvSessionHandler(session: sessionModule, excludePath: string[
1619
};
1720
}
1821

19-
export function getKvSessionPlugin(storePath: string|null, path = "/", excludePath = [], cookieOptions?: CookieOptions): Plugin {
22+
export function getKvSessionPlugin(
23+
storePath: string | null,
24+
path = "/",
25+
excludePath = [],
26+
cookieOptions?: CookieOptions,
27+
): Plugin {
2028
const session = kvSession(storePath, cookieOptions);
2129
const handler = getKvSessionHandler(session, excludePath);
2230

src/plugins/redis_session_plugin.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import type {
2-
Plugin,
3-
MiddlewareHandlerContext,
42
MiddlewareHandler,
3+
MiddlewareHandlerContext,
4+
Plugin,
55
} from "../deps.ts";
6-
import { Store, redisSession } from "../stores/redis.ts";
6+
import { redisSession, Store } from "../stores/redis.ts";
77
import { CookieOptions } from "../stores/cookie_option.ts";
88
import { sessionModule } from "../stores/interface.ts";
99

10-
export function getRedisSessionHandler(session: sessionModule, excludePath: string[]): MiddlewareHandler {
10+
export function getRedisSessionHandler(
11+
session: sessionModule,
12+
excludePath: string[],
13+
): MiddlewareHandler {
1114
return function (req: Request, ctx: MiddlewareHandlerContext) {
1215
if (excludePath.includes(new URL(req.url).pathname)) {
1316
return ctx.next();
@@ -16,7 +19,12 @@ export function getRedisSessionHandler(session: sessionModule, excludePath: stri
1619
};
1720
}
1821

19-
export function getRedisSessionPlugin(store: Store, path = "/", excludePath = [], cookieOptions?: CookieOptions): Plugin {
22+
export function getRedisSessionPlugin(
23+
store: Store,
24+
path = "/",
25+
excludePath = [],
26+
cookieOptions?: CookieOptions,
27+
): Plugin {
2028
const session = redisSession(store, cookieOptions);
2129
const handler = getRedisSessionHandler(session, excludePath);
2230

src/stores/cookie.ts

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,15 @@
11
import {
2-
create,
3-
decode,
42
getCookies,
3+
ironDefaults,
54
MiddlewareHandlerContext,
5+
seal,
66
setCookie,
7-
verify,
7+
unseal,
88
} from "../deps.ts";
99
import { type CookieOptions } from "./cookie_option.ts";
1010
import { Session } from "../session.ts";
1111
import type { WithSession } from "./interface.ts";
1212

13-
export function key() {
14-
const key = Deno.env.get("APP_KEY");
15-
16-
if (!key) {
17-
console.warn(
18-
"[FRESH SESSION] Warning: We didn't detect a env variable `APP_KEY`, if you are in production please fix this ASAP to avoid any security issue.",
19-
);
20-
}
21-
22-
return crypto.subtle.importKey(
23-
"raw",
24-
new TextEncoder().encode(key || "not-secret"),
25-
{ name: "HMAC", hash: "SHA-512" },
26-
false,
27-
["sign", "verify"],
28-
);
29-
}
30-
3113
export function createCookieSessionStorage(cookieOptions?: CookieOptions) {
3214
let cookieOptionsParam = cookieOptions;
3315
if (!cookieOptionsParam) {
@@ -38,34 +20,43 @@ export function createCookieSessionStorage(cookieOptions?: CookieOptions) {
3820
}
3921

4022
export class CookieSessionStorage {
41-
#key: CryptoKey;
4223
#cookieOptions: CookieOptions;
4324

44-
constructor(key: CryptoKey, cookieOptions: CookieOptions) {
45-
this.#key = key;
25+
constructor(cookieOptions: CookieOptions) {
4626
this.#cookieOptions = cookieOptions;
4727
}
4828

49-
static async init(cookieOptions: CookieOptions) {
50-
return new this(await key(), cookieOptions);
29+
static init(cookieOptions: CookieOptions) {
30+
return new this(cookieOptions);
5131
}
5232

5333
create() {
5434
return new Session();
5535
}
5636

5737
exists(sessionId: string) {
58-
return verify(sessionId, this.#key)
38+
return unseal(
39+
globalThis.crypto,
40+
sessionId,
41+
Deno.env.get("APP_KEY") as string,
42+
ironDefaults,
43+
)
5944
.then(() => true)
6045
.catch((e) => {
61-
console.warn("Invalid JWT token, creating new session...");
46+
console.warn("Invalid session, creating new session...");
6247
return false;
6348
});
6449
}
6550

66-
get(sessionId: string) {
67-
const [, payload] = decode(sessionId);
68-
const { _flash = {}, ...data } = payload;
51+
async get(sessionId: string) {
52+
const decryptedData = await unseal(
53+
globalThis.crypto,
54+
sessionId,
55+
Deno.env.get("APP_KEY") as string,
56+
ironDefaults,
57+
);
58+
59+
const { _flash = {}, ...data } = decryptedData;
6960
return new Session(data as object, _flash);
7061
}
7162

@@ -74,13 +65,16 @@ export class CookieSessionStorage {
7465
this.keyRotate();
7566
}
7667

68+
const encryptedData = await seal(
69+
globalThis.crypto,
70+
{ ...session.data, _flash: session.flashedData },
71+
Deno.env.get("APP_KEY") as string,
72+
ironDefaults,
73+
);
74+
7775
setCookie(response.headers, {
7876
name: "sessionId",
79-
value: await create(
80-
{ alg: "HS512", typ: "JWT" },
81-
{ ...session.data, _flash: session.flashedData },
82-
this.#key,
83-
),
77+
value: encryptedData,
8478
path: "/",
8579
...this.#cookieOptions,
8680
});

src/stores/interface.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ export type WithSession = {
44
session: Session;
55
};
66

7-
export type sessionModule =(req: Request, ctx: MiddlewareHandlerContext) => Promise<Response>
7+
export type sessionModule = (
8+
req: Request,
9+
ctx: MiddlewareHandlerContext,
10+
) => Promise<Response>;

src/stores/kv.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ function hasKeyPrefix(
129129
}
130130

131131
export function kvSession(
132-
storePath: string|null,
132+
storePath: string | null,
133133
cookieWithRedisOptions?: CookieWithRedisOptions,
134134
) {
135135
let setupKeyPrefix = "session_";

tests/fixture/dev.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env -S deno run -A --watch=static/,routes/
22

3-
Deno.env.set("APP_KEY", "not-secret");
3+
Deno.env.set("APP_KEY", "password-at-least-32-characters-long");
44

55
import dev from "$fresh/dev.ts";
66

0 commit comments

Comments
 (0)