Skip to content

Commit

Permalink
feat(auth): add dom-based CSRF token (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
cruisade authored and derevnjuk committed Jun 2, 2021
1 parent 5f4271e commit 1212a38
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 12 deletions.
25 changes: 25 additions & 0 deletions docker-compose.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: '3'

services:
db:
image: postgres
environment:
POSTGRES_DB: bc
POSTGRES_USER: bc
POSTGRES_PASSWORD: bc
ports:
- 5432:5432
volumes:
- ./pg.sql:/docker-entrypoint-initdb.d/pg.sql

proxy:
image: nginx:1.17.3-alpine

ports:
- 8090:80

volumes:
- ./nginx-config/local.conf:/etc/nginx/conf.d/default.conf
- ./public/build:/htdocs


19 changes: 19 additions & 0 deletions nginx-config/local.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

server {
gzip on;
listen 80;

sendfile off;
charset utf8;

location / {
root /htdocs/;
index index.html;
try_files $uri /index.html;

location /api {
proxy_pass http://172.17.0.1:3000;
}
}

}
1 change: 1 addition & 0 deletions public/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ COPY package*.json ./
COPY tsconfig.json ./
COPY .env ./
COPY src ./src
COPY typings ./typings
COPY public ./public
COPY vcs ./vcs

Expand Down
5 changes: 5 additions & 0 deletions public/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@testing-library/user-event": "^12.1.10",
"axios": "^0.21.0",
"dangerously-set-html-content": "^1.0.8",
"get-browser-fingerprint": "^2.0.1",
"history": "^4.10.1",
"jest": "^26.6.3",
"react": "^17.0.1",
Expand Down
16 changes: 14 additions & 2 deletions public/src/api/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,23 @@ export function getLdap(ldapProfileLink: string): Promise<any> {
});
}

export function loadDomXsrfToken(fingerprint: string): Promise<string> {
const config: AxiosRequestConfig = {
url: `${ApiUrl.Auth}/dom-csrf-flow`,
method: 'get',
headers: { fingerprint }
};

return makeApiRequest(config);
}

export function loadXsrfToken(): Promise<string> {
return makeApiRequest({
const config: AxiosRequestConfig = {
url: `${ApiUrl.Auth}/simple-csrf-flow`,
method: 'get'
});
};

return makeApiRequest(config);
}

export function postMetadata(): Promise<any> {
Expand Down
4 changes: 3 additions & 1 deletion public/src/interfaces/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ export interface LoginUser {
password: string;
csrf?: string;
op?: LoginFormMode;
fingerprint?: string;
}

export enum LoginFormMode {
BASIC = 'basic',
HTML = 'html',
CSRF = 'csrf'
CSRF = 'csrf',
DOM_BASED_CSRF = 'csrf_dom'
}

export interface LoginResponse {
Expand Down
29 changes: 25 additions & 4 deletions public/src/pages/auth/Login/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { AxiosRequestConfig } from 'axios';
import getBrowserFingerprint from 'get-browser-fingerprint';
import React, { FC, FormEvent, useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { getLdap, getUser, loadXsrfToken } from '../../../api/httpClient';
import {
getLdap,
getUser,
loadDomXsrfToken,
loadXsrfToken
} from '../../../api/httpClient';
import {
LoginFormMode,
LoginResponse,
Expand Down Expand Up @@ -78,6 +84,9 @@ export const Login: FC = () => {
switch (mode) {
case LoginFormMode.CSRF:
return { ...data, csrf };
case LoginFormMode.DOM_BASED_CSRF:
const fingerprint = getBrowserFingerprint();
return { ...data, csrf, fingerprint };
default:
return data;
}
Expand All @@ -87,12 +96,20 @@ export const Login: FC = () => {
loadXsrfToken().then((token) => setCsrf(token));
};

const loadDomCsrf = (fingerprint: string) => {
loadDomXsrfToken(fingerprint).then((token) => setCsrf(token));
};

useEffect(() => sendLdap(), [loginResponse]);
useEffect(() => {
switch (mode) {
case LoginFormMode.CSRF: {
return loadCsrf();
}
case LoginFormMode.DOM_BASED_CSRF: {
const fingerprint = getBrowserFingerprint();
return loadDomCsrf(fingerprint);
}
}
}, [mode]);

Expand All @@ -118,6 +135,9 @@ export const Login: FC = () => {
<option value={LoginFormMode.CSRF}>
Simple CSRF-based Authentication
</option>
<option value={LoginFormMode.DOM_BASED_CSRF}>
DOM based CSRF Authentication
</option>
</select>
</div>

Expand All @@ -144,9 +164,10 @@ export const Login: FC = () => {
onInput={onInput}
/>
</div>
{mode === LoginFormMode.CSRF && csrf && (
<input name="xsrf" type="hidden" value={csrf} />
)}

{(mode === LoginFormMode.CSRF ||
mode === LoginFormMode.DOM_BASED_CSRF) &&
csrf && <input name="xsrf" type="hidden" value={csrf} />}

{loginResponse && showLoginResponse(loginResponse)}
<br />
Expand Down
1 change: 1 addition & 0 deletions public/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"typeRoots": [ "./typings", "./node_modules/@types"],
"jsx": "react",
"target": "es5",
"module": "esnext",
Expand Down
1 change: 1 addition & 0 deletions public/typings/get-browser-fingerprint/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'get-browser-fingerprint';
3 changes: 3 additions & 0 deletions src/auth/api/login.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export enum FormMode {
BASIC = 'basic',
HTML = 'html',
CSRF = 'csrf',
DOM_BASED_CSRF = 'csrf_dom',
}

export class LoginRequest {
Expand All @@ -19,4 +20,6 @@ export class LoginRequest {
op?: string;

csrf?: string;

fingerprint?: string;
}
32 changes: 28 additions & 4 deletions src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import {
BadRequestException,
Body,
Controller,
Get,
HttpStatus,
InternalServerErrorException,
Logger,
Post,
Post, Req,
Res,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import { createHash } from 'crypto';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { User } from '../model/user.entity';
import { LdapQueryHandler } from '../users/ldap.query.handler';
Expand All @@ -36,7 +38,7 @@ import { AuthGuard } from './auth.guard';
import { AuthService, JwtProcessorType } from './auth.service';
import { passwordMatches } from './credentials.utils';
import { JwtType } from './jwt/jwt.type.decorator';
import { FastifyReply } from 'fastify';
import { FastifyReply, FastifyRequest } from 'fastify';
import { randomBytes } from 'crypto';
import { CsrfGuard } from './csrf.guard';

Expand Down Expand Up @@ -118,9 +120,9 @@ export class AuthController {
res.header(
'authorization',
await this.authService.createToken(
{
{
user: profile.email,
exp: 90 + Math.floor(Date.now() / 1000)
exp: 90 + Math.floor(Date.now() / 1000),
},
JwtProcessorType.RSA,
),
Expand All @@ -129,6 +131,28 @@ export class AuthController {
return profile;
}

@Get('dom-csrf-flow')
async getDomCsrfToken(
@Req() request: FastifyRequest,
@Res({ passthrough: true }) res: FastifyReply,
): Promise<string> {
const fp = request.headers['fingerprint'] as string;

if (!fp) {
throw new BadRequestException('Fingerprint header is required')
}
const token = createHash('md5')
.update(fp)
.digest('hex');

res.setCookie(this.CSRF_COOKIE_HEADER, token, {
httpOnly: true,
sameSite: 'strict',
});

return token;
}

@Get('simple-csrf-flow')
async getCsrfToken(
@Res({ passthrough: true }) res: FastifyReply,
Expand Down
14 changes: 13 additions & 1 deletion src/auth/csrf.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ExecutionContext,
Logger,
} from '@nestjs/common';
import { createHash } from 'crypto';
import { FastifyRequest } from 'fastify';
import { FormMode, LoginRequest } from './api/login.request';

Expand All @@ -20,11 +21,22 @@ export class CsrfGuard implements CanActivate {

try {
const body: LoginRequest = request.body as LoginRequest;
if (body?.op === FormMode.CSRF) {
const mode = body?.op
if ( mode === FormMode.CSRF || mode === FormMode.DOM_BASED_CSRF) {
const csrfCookie = request.cookies[CsrfGuard.CSRF_COOKIE_HEADER];

if (decodeURIComponent(csrfCookie) !== body.csrf) {
this.throwError();
}

if (mode === FormMode.DOM_BASED_CSRF && !body.fingerprint ) {
const fpHash = createHash('md5')
.update(body.fingerprint)
.digest('hex');
if (body.csrf !== fpHash) {
this.throwError()
}
}
}

return true;
Expand Down

0 comments on commit 1212a38

Please sign in to comment.