@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.
@@ -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>;
@@ -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.50.3",
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
  }