@dev-crew-berlin/enter-js-utils 0.50.3 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/csrf.d.ts +8 -0
- package/dist/lib/csrf.js +83 -0
- package/dist/lib/headers.d.ts +26 -0
- package/dist/lib/headers.js +31 -0
- package/dist/lib/tokens.d.ts +31 -0
- package/dist/lib/tokens.js +79 -0
- package/package.json +2 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CookieGetter, CookieSetter } from './headers';
|
|
2
|
+
export declare type CsrfOptions = {
|
|
3
|
+
cookieSignSecret: string;
|
|
4
|
+
cookieName?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function setCsrfToken(getCookie: CookieGetter, setCookie: CookieSetter, options: CsrfOptions): Promise<void>;
|
|
7
|
+
export declare function getCsrfToken(getCookie: CookieGetter, options: CsrfOptions): Promise<string | null>;
|
|
8
|
+
export declare function verifyCsrfToken(csrfToken: string | undefined, getCookie: CookieGetter, options: CsrfOptions): Promise<boolean>;
|
package/dist/lib/csrf.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const CSRF_COOKIE_NAME = 'enter-frontend.csrf-token';
|
|
2
|
+
async function importCookieSingKey(cookieSignSecret) {
|
|
3
|
+
return crypto.subtle.importKey('raw', new TextEncoder().encode(cookieSignSecret), {
|
|
4
|
+
name: 'HMAC',
|
|
5
|
+
hash: 'SHA-256'
|
|
6
|
+
}, false, ['sign', 'verify']);
|
|
7
|
+
}
|
|
8
|
+
async function validateCsrfCookie(csrfCookie, cookieSignSecret) {
|
|
9
|
+
if (!csrfCookie) return {
|
|
10
|
+
cookieIsValid: false,
|
|
11
|
+
tokenFromCookie: undefined
|
|
12
|
+
};
|
|
13
|
+
const [tokenFromCookie, signatureString] = csrfCookie.split('.');
|
|
14
|
+
const key = await importCookieSingKey(cookieSignSecret);
|
|
15
|
+
const signature = Uint8Array.from(atob(signatureString), c => c.charCodeAt(0));
|
|
16
|
+
const cookieIsValid = await crypto.subtle.verify('HMAC', key, signature, new TextEncoder().encode(tokenFromCookie));
|
|
17
|
+
if (!cookieIsValid) {
|
|
18
|
+
return {
|
|
19
|
+
cookieIsValid: false,
|
|
20
|
+
tokenFromCookie: undefined
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
cookieIsValid: true,
|
|
25
|
+
tokenFromCookie
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function setCsrfToken(getCookie, setCookie, options) {
|
|
29
|
+
const {
|
|
30
|
+
cookieSignSecret,
|
|
31
|
+
cookieName = CSRF_COOKIE_NAME
|
|
32
|
+
} = options;
|
|
33
|
+
const currentCookie = getCookie(cookieName);
|
|
34
|
+
const {
|
|
35
|
+
cookieIsValid
|
|
36
|
+
} = await validateCsrfCookie(currentCookie?.value, cookieSignSecret);
|
|
37
|
+
const cookieOptions = {
|
|
38
|
+
sameSite: 'lax',
|
|
39
|
+
path: '/'
|
|
40
|
+
};
|
|
41
|
+
if (currentCookie && cookieIsValid) {
|
|
42
|
+
// if we already have a valid token just make sure that we set it agin
|
|
43
|
+
// this is needed since `getCookie` could read from a different location
|
|
44
|
+
// than `setCookie`
|
|
45
|
+
setCookie(cookieName, currentCookie.value, cookieOptions);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const csrfToken = crypto.randomUUID();
|
|
49
|
+
const encodedToken = new TextEncoder().encode(csrfToken);
|
|
50
|
+
const key = await importCookieSingKey(cookieSignSecret);
|
|
51
|
+
const signature = await crypto.subtle.sign('HMAC', key, encodedToken);
|
|
52
|
+
const signatureString = btoa(String.fromCharCode(...new Uint8Array(signature)));
|
|
53
|
+
const csrfCookie = `${csrfToken}.${signatureString}`;
|
|
54
|
+
setCookie(cookieName, csrfCookie, cookieOptions);
|
|
55
|
+
}
|
|
56
|
+
export async function getCsrfToken(getCookie, options) {
|
|
57
|
+
const {
|
|
58
|
+
cookieSignSecret,
|
|
59
|
+
cookieName = CSRF_COOKIE_NAME
|
|
60
|
+
} = options;
|
|
61
|
+
const currentCookie = getCookie(cookieName)?.value;
|
|
62
|
+
const {
|
|
63
|
+
cookieIsValid,
|
|
64
|
+
tokenFromCookie
|
|
65
|
+
} = await validateCsrfCookie(currentCookie, cookieSignSecret);
|
|
66
|
+
if (!cookieIsValid) return null;
|
|
67
|
+
return tokenFromCookie;
|
|
68
|
+
}
|
|
69
|
+
export async function verifyCsrfToken(csrfToken, getCookie, options) {
|
|
70
|
+
const {
|
|
71
|
+
cookieSignSecret,
|
|
72
|
+
cookieName = CSRF_COOKIE_NAME
|
|
73
|
+
} = options;
|
|
74
|
+
const csrfCookie = getCookie(cookieName)?.value;
|
|
75
|
+
const {
|
|
76
|
+
cookieIsValid,
|
|
77
|
+
tokenFromCookie
|
|
78
|
+
} = await validateCsrfCookie(csrfCookie, cookieSignSecret);
|
|
79
|
+
if (!cookieIsValid) return false;
|
|
80
|
+
if (!csrfToken) return false;
|
|
81
|
+
const payloadMatchesCooie = tokenFromCookie === csrfToken;
|
|
82
|
+
return payloadMatchesCooie;
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
3
|
+
export declare type CookieOptions = {
|
|
4
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
5
|
+
path?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare type RequestCookie = {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
export declare type CookieGetter = (name: string) => RequestCookie | undefined;
|
|
12
|
+
export declare type CookieSetter = (name: string, value: string, options?: CookieOptions) => void;
|
|
13
|
+
export declare type CookieRemover = (name: string) => void;
|
|
14
|
+
export declare type HeaderGetter = (name: string) => string | null;
|
|
15
|
+
export declare function nodeCookies(request: {
|
|
16
|
+
cookies: Partial<{
|
|
17
|
+
[key: string]: string;
|
|
18
|
+
}>;
|
|
19
|
+
}, response: ServerResponse): {
|
|
20
|
+
getCookie: CookieGetter;
|
|
21
|
+
setCookie: CookieSetter;
|
|
22
|
+
deleteCookie: CookieRemover;
|
|
23
|
+
};
|
|
24
|
+
export declare function nodeHeaders(request: IncomingMessage): {
|
|
25
|
+
getHeader: HeaderGetter;
|
|
26
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import nookies from 'nookies';
|
|
2
|
+
export function nodeCookies(request, response) {
|
|
3
|
+
return {
|
|
4
|
+
getCookie: name => {
|
|
5
|
+
const value = request.cookies[name];
|
|
6
|
+
if (value === undefined) return undefined;
|
|
7
|
+
return {
|
|
8
|
+
name,
|
|
9
|
+
value
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
setCookie: (name, value, options) => {
|
|
13
|
+
nookies.set({
|
|
14
|
+
res: response
|
|
15
|
+
}, name, value, options);
|
|
16
|
+
},
|
|
17
|
+
deleteCookie: name => nookies.destroy({
|
|
18
|
+
res: response
|
|
19
|
+
}, name)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function nodeHeaders(request) {
|
|
23
|
+
return {
|
|
24
|
+
getHeader: name => {
|
|
25
|
+
const value = request.headers[name] ?? null;
|
|
26
|
+
if (value === null) return value;
|
|
27
|
+
if (typeof value !== 'string') return value.join();
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CookieGetter, CookieRemover, CookieSetter } from './headers';
|
|
2
|
+
import { Result } from './result';
|
|
3
|
+
export declare type TokenOptions = {
|
|
4
|
+
jwtSecret: string;
|
|
5
|
+
instanceName: string;
|
|
6
|
+
};
|
|
7
|
+
export declare type TokenValidationError = 'token_missing' | 'token_invalid' | 'token_expired' | 'security_question_check_failed';
|
|
8
|
+
export declare type RegistrationTokenOptions = {
|
|
9
|
+
registrationTokenCookieName?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare type RegistrationToken = {
|
|
12
|
+
attendeeId: string;
|
|
13
|
+
sessionType: string;
|
|
14
|
+
flags: {
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
isExpired: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function parseRegistrationToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, RegistrationToken>;
|
|
20
|
+
export declare function parseUnsubscribeToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, {
|
|
21
|
+
emailAddress: string;
|
|
22
|
+
attendeeId: string;
|
|
23
|
+
emailName: string;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function parseEmailHtmlToken(tokenString: string | undefined, options: TokenOptions): Result<TokenValidationError, {
|
|
26
|
+
attendeeId: string;
|
|
27
|
+
emailName: string;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function readRegistrationToken(getCookie: CookieGetter, options: TokenOptions & RegistrationTokenOptions): Result<TokenValidationError, RegistrationToken>;
|
|
30
|
+
export declare function storeRegistrationToken(token: string, setCookie: CookieSetter, options?: RegistrationTokenOptions): void;
|
|
31
|
+
export declare function deleteRegistrationToken(deleteCookie: CookieRemover, options?: RegistrationTokenOptions): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import jws from 'jws';
|
|
2
|
+
import { failure, success } from './result';
|
|
3
|
+
const REGISTRATION_TOKEN_COOKIE_NAME = 'enter-frontend.registation-token';
|
|
4
|
+
function parseJWS(tokenString, options) {
|
|
5
|
+
if (!tokenString) return failure('token_missing');
|
|
6
|
+
const hasValidFormat = jws.isValid(tokenString);
|
|
7
|
+
if (!hasValidFormat) return failure('token_invalid');
|
|
8
|
+
const signatureIsValid = jws.verify(tokenString, 'HS256', options.jwtSecret);
|
|
9
|
+
if (!signatureIsValid) return failure('token_invalid');
|
|
10
|
+
return success(jws.decode(tokenString));
|
|
11
|
+
}
|
|
12
|
+
export function parseRegistrationToken(tokenString, options) {
|
|
13
|
+
const tokenResult = parseJWS(tokenString, options);
|
|
14
|
+
if (!tokenResult.success) return tokenResult;
|
|
15
|
+
const payload = tokenResult.data.payload;
|
|
16
|
+
if (payload.instance_name !== options.instanceName || typeof payload.attendee_id !== 'string' || typeof payload.exp !== 'number') {
|
|
17
|
+
return failure('token_invalid');
|
|
18
|
+
}
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const expires = new Date(payload.exp * 1000);
|
|
21
|
+
const isExpired = now >= expires;
|
|
22
|
+
return success({
|
|
23
|
+
attendeeId: payload.attendee_id,
|
|
24
|
+
sessionType: payload.session_type ?? 'register',
|
|
25
|
+
flags: payload.flags ?? {},
|
|
26
|
+
isExpired
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function parseUnsubscribeToken(tokenString, options) {
|
|
30
|
+
const tokenResult = parseJWS(tokenString, options);
|
|
31
|
+
if (!tokenResult.success) return tokenResult;
|
|
32
|
+
const payload = tokenResult.data.payload;
|
|
33
|
+
if (payload.instance_name !== options.instanceName || typeof payload.email_address !== 'string' || typeof payload.attendee_id !== 'string' || typeof payload.email_name !== 'string') {
|
|
34
|
+
return failure('token_invalid');
|
|
35
|
+
}
|
|
36
|
+
return success({
|
|
37
|
+
emailAddress: payload.email_address,
|
|
38
|
+
attendeeId: payload.attendee_id,
|
|
39
|
+
emailName: payload.email_name
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function parseEmailHtmlToken(tokenString, options) {
|
|
43
|
+
const tokenResult = parseJWS(tokenString, options);
|
|
44
|
+
if (!tokenResult.success) return tokenResult;
|
|
45
|
+
const payload = tokenResult.data.payload;
|
|
46
|
+
if (payload.instance_name !== options.instanceName || typeof payload.attendee_id !== 'string' || typeof payload.email_name !== 'string' || typeof payload.exp !== 'number') {
|
|
47
|
+
return failure('token_invalid');
|
|
48
|
+
}
|
|
49
|
+
const now = new Date();
|
|
50
|
+
const expires = new Date(payload.exp * 1000);
|
|
51
|
+
const isExpired = now >= expires;
|
|
52
|
+
if (isExpired) return failure('token_expired');
|
|
53
|
+
return success({
|
|
54
|
+
attendeeId: payload.attendee_id,
|
|
55
|
+
emailName: payload.email_name
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function readRegistrationToken(getCookie, options) {
|
|
59
|
+
const {
|
|
60
|
+
registrationTokenCookieName = REGISTRATION_TOKEN_COOKIE_NAME
|
|
61
|
+
} = options;
|
|
62
|
+
const registationToken = getCookie(registrationTokenCookieName)?.value;
|
|
63
|
+
return parseRegistrationToken(registationToken, options);
|
|
64
|
+
}
|
|
65
|
+
export function storeRegistrationToken(token, setCookie, options = {}) {
|
|
66
|
+
const {
|
|
67
|
+
registrationTokenCookieName = REGISTRATION_TOKEN_COOKIE_NAME
|
|
68
|
+
} = options;
|
|
69
|
+
setCookie(registrationTokenCookieName, token, {
|
|
70
|
+
sameSite: 'lax',
|
|
71
|
+
path: '/'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function deleteRegistrationToken(deleteCookie, options = {}) {
|
|
75
|
+
const {
|
|
76
|
+
registrationTokenCookieName = REGISTRATION_TOKEN_COOKIE_NAME
|
|
77
|
+
} = options;
|
|
78
|
+
deleteCookie(registrationTokenCookieName);
|
|
79
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev-crew-berlin/enter-js-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"description": "utils such as vaildation and other helpers to work with data from the enter app",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"dependencies": {
|
|
75
75
|
"fp-ts": "^2.12.1",
|
|
76
76
|
"jws": "^4.0.0",
|
|
77
|
+
"nookies": "^2.5.2",
|
|
77
78
|
"styled-jsx": "^5.0.2",
|
|
78
79
|
"uuid": "^9.0.0"
|
|
79
80
|
}
|