Compare commits
No commits in common. "8e56d5bd6dfcff958c8e05aefa62c3b12a18d62d" and "906f31b2403137ac728e2c97bf9825c3fdd2d19f" have entirely different histories.
8e56d5bd6d
...
906f31b240
|
@ -1,5 +0,0 @@
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWGTG/X7/vLtzInr9
|
|
||||||
k482dpSC7bZvV3us2FkWqJFe1XWhRANCAASmrapDgQf6kVlMPsSOHeoCB+Z0+P0L
|
|
||||||
K3x95uUa6igELdVtXJPaSJJksytjXWGCpsn9RhkCO2XKdSvqW/9QVcYn
|
|
||||||
-----END PRIVATE KEY-----
|
|
|
@ -1,22 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDkTCCAfmgAwIBAgIRAItJkjr6cwdtBbymbjslLOowDQYJKoZIhvcNAQELBQAw
|
|
||||||
gZMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE0MDIGA1UECwwrSk9U
|
|
||||||
U1ItVklWT1xKdWxpZW5ASk9UU1ItVklWTyAoSnVsaWVuIE9jdWxpKTE7MDkGA1UE
|
|
||||||
AwwybWtjZXJ0IEpPVFNSLVZJVk9cSnVsaWVuQEpPVFNSLVZJVk8gKEp1bGllbiBP
|
|
||||||
Y3VsaSkwHhcNMjQwNzA2MTUwNjE0WhcNMjYxMDA2MTUwNjE0WjBfMScwJQYDVQQK
|
|
||||||
Ex5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxNDAyBgNVBAsMK0pPVFNS
|
|
||||||
LVZJVk9cSnVsaWVuQEpPVFNSLVZJVk8gKEp1bGllbiBPY3VsaSkwWTATBgcqhkjO
|
|
||||||
PQIBBggqhkjOPQMBBwNCAASmrapDgQf6kVlMPsSOHeoCB+Z0+P0LK3x95uUa6igE
|
|
||||||
LdVtXJPaSJJksytjXWGCpsn9RhkCO2XKdSvqW/9QVcYno14wXDAOBgNVHQ8BAf8E
|
|
||||||
BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUmZZ5b76pxQAu
|
|
||||||
0prvUf+X+pZICf0wFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA
|
|
||||||
A4IBgQC7Fro6IEMsPu2I4qlhtzWUCVogSUE1PeMCVNxy6HzpVHw1oKiVQJf32A4m
|
|
||||||
VLfGQLkXb2pmM7/Ph/owAsH/RMRFXO44vLeJ+j0aMJHxl9m6W6QJJU++FtpMJ0lX
|
|
||||||
iot0Ojy6xzSjmaLw9eqBkptXRRvLkbglVNQI/LuEg2JRj1zmYaPsTXpHkm+pOEoM
|
|
||||||
l+3ik9UPh04hCBhlCxIP6PJMZ8qfU51nfQA7P+9PB1FJuzuhi7goEdkUGCWCHGJl
|
|
||||||
CTefaInBiFNthXXHMmE6RdAqUUGfzkY9VcfjoWfk30UMF+yBibXJOYa1v59IR+rw
|
|
||||||
4fijUlujOastp4TYD3W0MuL9HT/XeDChRgNjwyTRrl/Lc4sAvb8Dm7LeC/CbqPlX
|
|
||||||
Y1uy31GDBfydERjRIwe8qp69+rysKHmDfyjH932o6ENVEWMdwvNly6MC2SsZHHQd
|
|
||||||
jj/O04tWmdN7gtEkmxZfGTGSmx2GUPfJssNEH8pLYxU/MUENAOk2cl/2SIeziaf8
|
|
||||||
gNtrzYA=
|
|
||||||
-----END CERTIFICATE-----
|
|
8
dev.ts
8
dev.ts
|
@ -5,10 +5,4 @@ import config from './fresh.config.ts'
|
||||||
|
|
||||||
import '$std/dotenv/load.ts'
|
import '$std/dotenv/load.ts'
|
||||||
|
|
||||||
await dev(import.meta.url, './main.ts', {
|
await dev(import.meta.url, './main.ts', config)
|
||||||
...config,
|
|
||||||
server: {
|
|
||||||
cert: await Deno.readTextFile('./cert/localhost.pem'),
|
|
||||||
key: await Deno.readTextFile('./cert/localhost-key.pem'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
import { FreshContext } from '$fresh/server.ts'
|
||||||
import { useCsp } from ':src/csp/middleware.ts'
|
|
||||||
import { useSecurityHeaders } from ':src/security_headers/middleware.ts'
|
|
||||||
import { useServiceworker } from ':src/serviceworker/middleware.ts'
|
|
||||||
import { useSession } from ':src/session/middleware.ts'
|
|
||||||
import { SessionStore } from ':src/session/mod.ts'
|
import { SessionStore } from ':src/session/mod.ts'
|
||||||
|
import { getCookies, setCookie } from '@std/http/cookie'
|
||||||
|
|
||||||
export async function handler(request: Request, ctx: FreshContext) {
|
export async function handler(request: Request, ctx: FreshContext) {
|
||||||
// Update fresh context state with session
|
// Update fresh context state with session
|
||||||
|
@ -12,11 +9,69 @@ export async function handler(request: Request, ctx: FreshContext) {
|
||||||
// Get response
|
// Get response
|
||||||
const response = await ctx.next()
|
const response = await ctx.next()
|
||||||
|
|
||||||
// Use custom middleware hooks
|
//Add security headers
|
||||||
useSecurityHeaders(request, response, ctx)
|
// See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/TLS#http_strict_transport_security
|
||||||
await useCsp(request, response, ctx)
|
response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload')
|
||||||
useSession(request, response, ctx)
|
response.headers.set('Content-Security-Policy', "frame-ancestors 'none'; upgrade-insecure-requests")
|
||||||
useServiceworker(request, response, ctx)
|
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Referrer_policy
|
||||||
|
response.headers.set('Referrer-Policy', 'no-referrer, strict-origin-when-cross-origin')
|
||||||
|
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/MIME_types
|
||||||
|
response.headers.set('X-Content-Type-Options', 'nosniff')
|
||||||
|
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Clickjacking
|
||||||
|
response.headers.set('X-Frame-Options', 'DENY')
|
||||||
|
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CORP
|
||||||
|
response.headers.set('Cross-Origin-Resource-Policy', 'same-origin')
|
||||||
|
//See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
|
||||||
|
//? SRI plugin for non local resources only ?
|
||||||
|
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSP
|
||||||
|
//? fresh useCSP https://fresh.deno.dev/docs/examples/using-csp
|
||||||
|
|
||||||
|
// Allow service worker to serve root scope
|
||||||
|
if (ctx.url.pathname.endsWith('island-startserviceworker.js')) {
|
||||||
|
response.headers.set('Service-Worker-Allowed', '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start session
|
||||||
|
if (SessionStore.getFromRequest(request) === undefined) {
|
||||||
|
// Clear outdated cookies
|
||||||
|
for (const cookie in getCookies(request.headers)) {
|
||||||
|
setCookie(response.headers, {
|
||||||
|
name: cookie,
|
||||||
|
value: '',
|
||||||
|
path: '/',
|
||||||
|
expires: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new session
|
||||||
|
const session = SessionStore.createSession()
|
||||||
|
ctx.state = { ...ctx.state, session }
|
||||||
|
|
||||||
|
// Set session cookie
|
||||||
|
setCookie(response.headers, {
|
||||||
|
name: '__Secure-SESSION',
|
||||||
|
value: session.uuid,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
secure: true,
|
||||||
|
path: '/',
|
||||||
|
expires: SessionStore.maxAge,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set csrf
|
||||||
|
const csrf = crypto.randomUUID()
|
||||||
|
session.set('_csrf', csrf)
|
||||||
|
|
||||||
|
setCookie(response.headers, {
|
||||||
|
name: '__Host-CSRF',
|
||||||
|
value: csrf,
|
||||||
|
httpOnly: false,
|
||||||
|
sameSite: 'Strict',
|
||||||
|
secure: true,
|
||||||
|
path: '/',
|
||||||
|
expires: SessionStore.maxAge,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
|
||||||
import { applyCspRulesWithNonce, CspRules } from ':src/csp/mod.ts'
|
|
||||||
|
|
||||||
export function useCsp(
|
|
||||||
_request: Request,
|
|
||||||
response: Response,
|
|
||||||
ctx: FreshContext,
|
|
||||||
) {
|
|
||||||
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CSP
|
|
||||||
|
|
||||||
const trustedDomains = ["'self'", 'https://git.cohabit.fr']
|
|
||||||
|
|
||||||
const cspRules: CspRules = {
|
|
||||||
defaultSrc: ["'none'"],
|
|
||||||
frameAncestors: ["'none'"],
|
|
||||||
upgradeInsecureRequests: true,
|
|
||||||
styleSrc: [...trustedDomains, "'unsafe-inline'"], //set nonce to inline script
|
|
||||||
manifestSrc: [`${ctx.url.origin}/manifest.json`],
|
|
||||||
imgSrc: [
|
|
||||||
...trustedDomains,
|
|
||||||
'data:',
|
|
||||||
'https://picsum.photos',
|
|
||||||
'https://fastly.picsum.photos',
|
|
||||||
],
|
|
||||||
fontSrc: [...trustedDomains, 'https://cdn.jsdelivr.net'],
|
|
||||||
scriptSrc: ["'self'", "'strict-dynamic'"],
|
|
||||||
connectSrc: ["'self'"],
|
|
||||||
formAction: ["'none'"],
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyCspRulesWithNonce(response, cspRules)
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import type { ContentSecurityPolicyDirectives } from '$fresh/runtime.ts'
|
|
||||||
import { getFreshNonce, toSnakeCase } from ':src/utils.ts'
|
|
||||||
|
|
||||||
export type CspRules = ContentSecurityPolicyDirectives & {
|
|
||||||
upgradeInsecureRequests: true
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyCspRules(
|
|
||||||
{ headers }: { headers: Headers },
|
|
||||||
rules: CspRules,
|
|
||||||
): void {
|
|
||||||
const rulesString: string[] = []
|
|
||||||
|
|
||||||
for (const rule in rules) {
|
|
||||||
const value = rules[rule as unknown as keyof CspRules]
|
|
||||||
const ruleName = toSnakeCase(rule)
|
|
||||||
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
rulesString.push(`${ruleName}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
rulesString.push(`${ruleName} ${value.join(' ')}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
rulesString.push(`${ruleName} ${value}`)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
throw TypeError(`unsupported csp rule "${rule}" with value (${value})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
headers.set('Content-Security-Policy', rulesString.join('; '))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function applyCspRulesWithNonce(
|
|
||||||
response: Response,
|
|
||||||
rules: CspRules,
|
|
||||||
): Promise<void> {
|
|
||||||
// Get nonce from any html response
|
|
||||||
const nonce = await getFreshNonce(response)
|
|
||||||
|
|
||||||
// Add nonce to script src if defined
|
|
||||||
if (nonce) {
|
|
||||||
rules.scriptSrc?.push(`'nonce-${nonce}'`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyCspRules(response, rules)
|
|
||||||
}
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { Db, Group } from '@cohabit/ressources_manager/mod.ts'
|
import { Db, Group } from '@cohabit/ressources_manager/mod.ts'
|
||||||
|
|
||||||
// Import Datas
|
// Import Datas
|
||||||
import { exists } from '$std/fs/exists.ts'
|
import groups from './mock/groups.json' with { type: 'json' }
|
||||||
import { ensureDir } from '$std/fs/mod.ts'
|
import users from './mock/users.json' with { type: 'json' }
|
||||||
import { User } from '@cohabit/ressources_manager/src/models/mod.ts'
|
import { User } from '@cohabit/ressources_manager/src/models/mod.ts'
|
||||||
import { MailAddress } from '@cohabit/ressources_manager/types.ts'
|
import { MailAddress } from '@cohabit/ressources_manager/types.ts'
|
||||||
import groups from ":src/db/mock/groups.json" with { type: 'json' }
|
import { exists } from '$std/fs/exists.ts'
|
||||||
import users from ":src/db/mock/users.json" with { type: 'json' }
|
|
||||||
|
|
||||||
await ensureDir('./cache')
|
const dbPath = './_fresh/db.sqlite'
|
||||||
const dbPath = './cache/db.sqlite'
|
|
||||||
const dbExist = await exists(dbPath)
|
const dbExist = await exists(dbPath)
|
||||||
|
|
||||||
export const db = await Db.init(dbPath)
|
export const db = await Db.init(dbPath)
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
|
||||||
|
|
||||||
export function useSecurityHeaders(
|
|
||||||
_request: Request,
|
|
||||||
response: Response,
|
|
||||||
_ctx: FreshContext,
|
|
||||||
) {
|
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/TLS#http_strict_transport_security
|
|
||||||
response.headers.set(
|
|
||||||
'Strict-Transport-Security',
|
|
||||||
'max-age=63072000; includeSubDomains; preload',
|
|
||||||
)
|
|
||||||
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Referrer_policy
|
|
||||||
response.headers.set(
|
|
||||||
'Referrer-Policy',
|
|
||||||
'no-referrer, strict-origin-when-cross-origin',
|
|
||||||
)
|
|
||||||
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/MIME_types
|
|
||||||
response.headers.set('X-Content-Type-Options', 'nosniff')
|
|
||||||
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/Clickjacking
|
|
||||||
response.headers.set('X-Frame-Options', 'DENY')
|
|
||||||
//See https://developer.mozilla.org/en-US/docs/Web/Security/Practical_implementation_guides/CORP
|
|
||||||
response.headers.set('Cross-Origin-Resource-Policy', 'same-origin')
|
|
||||||
//See https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
|
|
||||||
//? SRI plugin for non local resources only ?
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
|
||||||
|
|
||||||
export function useServiceworker(
|
|
||||||
_request: Request,
|
|
||||||
response: Response,
|
|
||||||
ctx: FreshContext,
|
|
||||||
) {
|
|
||||||
// Allow service worker to serve root scope
|
|
||||||
if (ctx.url.pathname.endsWith('island-startserviceworker.js')) {
|
|
||||||
response.headers.set('Service-Worker-Allowed', '/')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { FreshContext } from '$fresh/server.ts'
|
|
||||||
import { SessionStore } from ':src/session/mod.ts'
|
|
||||||
import { getCookies, setCookie } from 'jsr:@std/http@^0.224.4/cookie'
|
|
||||||
|
|
||||||
export function useSession(
|
|
||||||
request: Request,
|
|
||||||
response: Response,
|
|
||||||
ctx: FreshContext,
|
|
||||||
) {
|
|
||||||
// Check if session already started
|
|
||||||
if (SessionStore.getFromRequest(request) !== undefined) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear outdated cookies
|
|
||||||
for (const cookie in getCookies(request.headers)) {
|
|
||||||
setCookie(response.headers, {
|
|
||||||
name: cookie,
|
|
||||||
value: '',
|
|
||||||
path: '/',
|
|
||||||
expires: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new session
|
|
||||||
const session = SessionStore.createSession()
|
|
||||||
ctx.state = { ...ctx.state, session }
|
|
||||||
|
|
||||||
// Set session cookie
|
|
||||||
setCookie(response.headers, {
|
|
||||||
name: '__Secure-SESSION',
|
|
||||||
value: session.uuid,
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'Strict',
|
|
||||||
secure: true,
|
|
||||||
path: '/',
|
|
||||||
expires: SessionStore.maxAge,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set csrf
|
|
||||||
const csrf = crypto.randomUUID()
|
|
||||||
session.set('_csrf', csrf)
|
|
||||||
|
|
||||||
setCookie(response.headers, {
|
|
||||||
name: '__Host-CSRF',
|
|
||||||
value: csrf,
|
|
||||||
httpOnly: false,
|
|
||||||
sameSite: 'Strict',
|
|
||||||
secure: true,
|
|
||||||
path: '/',
|
|
||||||
expires: SessionStore.maxAge,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -47,7 +47,7 @@ export class SessionStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
static getFromRequest(request: Request): Session | undefined {
|
static getFromRequest(request: Request): Session | undefined {
|
||||||
const sessionId = getCookies(request.headers)['__Secure-SESSION'] ?? ''
|
const sessionId = getCookies(request.headers)['_SESSION'] ?? ''
|
||||||
return this.getSession(sessionId)
|
return this.getSession(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,8 +184,7 @@ input[type='checkbox'] {
|
||||||
.cta:focus-visible,
|
.cta:focus-visible,
|
||||||
.button:hover,
|
.button:hover,
|
||||||
.button:focus-visible {
|
.button:focus-visible {
|
||||||
background-color: var(--_background-color);
|
background-color: var(--lime-1);
|
||||||
font-weight: 500;
|
|
||||||
color: var(--_accent-color);
|
color: var(--_accent-color);
|
||||||
box-shadow: 0 0 0 0 var(--_accent-color);
|
box-shadow: 0 0 0 0 var(--_accent-color);
|
||||||
}
|
}
|
||||||
|
|
14
src/utils.ts
14
src/utils.ts
|
@ -191,17 +191,3 @@ export function unwrapSignalOrValue<T>(valueOrSignal: T | SignalLike<T>): T {
|
||||||
|
|
||||||
return valueOrSignal
|
return valueOrSignal
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toSnakeCase(str: string): string {
|
|
||||||
return str.replaceAll(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getFreshNonce(response: Response) {
|
|
||||||
if (!response.headers.get('Content-Type')?.startsWith('text/html')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = await response.clone().text()
|
|
||||||
const match = source.match(/nonce="(?<nonce>\w+)"/)
|
|
||||||
return match?.groups?.nonce
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue