Auth stuff

This commit is contained in:
Elizabeth Hunt 2023-04-02 21:02:49 -06:00
parent 4ce99d9434
commit b94bacc026
Signed by: simponic
GPG Key ID: 52B3774857EB24B1
13 changed files with 5280 additions and 123 deletions

87
package-lock.json generated
View File

@ -1,87 +0,0 @@
{
"name": "friends",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@types/cookie-parser": "^1.4.3"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cookie-parser": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
"integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/express": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
"integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.33",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz",
"integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA=="
},
"node_modules/@types/node": {
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"node_modules/@types/serve-static": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz",
"integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==",
"dependencies": {
"@types/mime": "*",
"@types/node": "*"
}
}
}
}

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"@types/cookie-parser": "^1.4.3"
}
}

View File

@ -13,10 +13,10 @@
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@prisma/client": "^4.12.0",
"@types/cookie-parser": "^1.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"openpgp": "^5.7.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
@ -24,9 +24,11 @@
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/openpgp": "^4.4.18",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
@ -1804,10 +1806,20 @@
"@babel/types": "^7.3.0"
}
},
"node_modules/@types/bn.js": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
"dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
@ -1817,6 +1829,7 @@
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@ -1825,6 +1838,7 @@
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.3.tgz",
"integrity": "sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
@ -1865,6 +1879,7 @@
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz",
"integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33",
@ -1876,6 +1891,7 @@
"version": "4.17.33",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz",
"integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
@ -1934,12 +1950,23 @@
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA=="
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
"dev": true
},
"node_modules/@types/node": {
"version": "18.15.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==",
"dev": true
},
"node_modules/@types/openpgp": {
"version": "4.4.18",
"resolved": "https://registry.npmjs.org/@types/openpgp/-/openpgp-4.4.18.tgz",
"integrity": "sha512-8PGX6byEni97ZRRp2fVguSk4hbw4e+0vd8XHmyUt0+PjRoMcOZHsn+StQQjfo/wA/3kf/KCbV6I4pwmSC5/LKg==",
"dev": true,
"dependencies": {
"@types/bn.js": "*"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@ -1956,12 +1983,14 @@
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw=="
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/semver": {
"version": "7.3.13",
@ -1973,6 +2002,7 @@
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz",
"integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==",
"dev": true,
"dependencies": {
"@types/mime": "*",
"@types/node": "*"
@ -2562,6 +2592,17 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"node_modules/asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
"integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
"dependencies": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -2719,6 +2760,11 @@
"node": ">= 6"
}
},
"node_modules/bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
},
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@ -5788,6 +5834,11 @@
"node": ">=6"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@ -6001,6 +6052,17 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openpgp": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/openpgp/-/openpgp-5.7.0.tgz",
"integrity": "sha512-wchYJQfFbSaocUvUIYqNrWD+lRSmFSG1d3Ak2CHeXFocDSEsf7Uc1zUzHjSdlZPTvGeeXPQ+MJrwVtalL4QCBg==",
"dependencies": {
"asn1.js": "^5.0.0"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",

View File

@ -24,10 +24,10 @@
"@nestjs/core": "^9.0.0",
"@nestjs/platform-express": "^9.0.0",
"@prisma/client": "^4.12.0",
"@types/cookie-parser": "^1.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"openpgp": "^5.7.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0"
},
@ -35,9 +35,11 @@
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.13",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/openpgp": "^4.4.18",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",

View File

@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -1,11 +1,9 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { TimerModule } from './timer/timer.module';
import { AuthModule } from './auth/auth.module';
@Module({
imports: [PrismaModule],
controllers: [AppController],
providers: [AppService],
imports: [PrismaModule, TimerModule, AuthModule],
})
export class AppModule {}

View File

@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@ -0,0 +1,88 @@
import {
Controller,
Get,
Post,
Body,
Param,
NotFoundException,
BadRequestException,
Res,
UseGuards,
Req,
} from '@nestjs/common';
import { IsNotEmpty } from 'class-validator';
import { readKey, readCleartextMessage, verify } from 'openpgp';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
export class LoginUserDTO {
@IsNotEmpty()
signature: string;
}
@Controller('/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@UseGuards(AuthGuard)
@Get('/logout')
async logout(@Res({ passthrough: true }) res, @Req() req) {
res.cookie('god_token', '', {
maxAge: -1,
path: '/',
});
return await this.authService.deleteToken(req.token);
}
@Get('/:name')
async retrieveGodToken(@Param('name') name: string) {
const friend = await this.authService.findFriendByName(name);
if (!friend) throw new NotFoundException('Friend not found with that name');
return await this.authService.createTokenForFriend(friend);
}
@Post()
async verifyFriend(
@Res({ passthrough: true }) res,
@Body() { signature }: LoginUserDTO,
) {
let signatureObj;
try {
signatureObj = await readCleartextMessage({
cleartextMessage: signature,
});
} catch (e) {
throw new BadRequestException('Invalid PGP Signature');
}
const { text: token } = signatureObj;
const referencedToken = await this.authService.findGodTokenWithFriend(
token,
);
if (!referencedToken)
throw new NotFoundException('Could not find God Token to sign');
const { friend } = referencedToken;
const publicKeyObj = await readKey({ armoredKey: friend.public_key });
const verificationResult = await verify({
message: signatureObj,
verificationKeys: publicKeyObj,
});
const { verified } = verificationResult.signatures[0];
if (await verified) {
res.cookie('god_token', token, {
httpOnly: true,
path: '/',
expires: referencedToken.expiration,
});
return await this.authService.signToken(token);
}
throw new BadRequestException(
"PGP signature could not be verified with user's public key",
);
}
}

View File

@ -0,0 +1,32 @@
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
if (!req.cookies.god_token) throw new UnauthorizedException('No session');
const token = await this.authService.findGodTokenWithFriend(
req.cookies.god_token,
);
if (!token) throw new UnauthorizedException('Could not verify session');
if (new Date().getTime() > token.expiration.getTime())
throw new UnauthorizedException('Session has expired');
const { friend } = token;
req.friend = friend;
req.token = token.token;
return true;
}
}

View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../prisma/prisma.module';
import { AuthController } from './auth.controller';
import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
@Module({
imports: [PrismaModule],
controllers: [AuthController],
exports: [AuthGuard],
providers: [AuthService, AuthGuard],
})
export class AuthModule {}

View File

@ -0,0 +1,60 @@
import { Injectable } from '@nestjs/common';
import { Friend } from '@prisma/client';
import { randomInt } from 'crypto';
import { PrismaService } from '../prisma/prisma.service';
import words from './words';
export const GOD_TOKEN_EXPIRATION_MS = 420 * 69 * 1000;
export const GOD_TOKEN_LENGTH = 25;
@Injectable()
export class AuthService {
constructor(private readonly prismaService: PrismaService) {}
private makeGodToken = (godTokenLength = GOD_TOKEN_LENGTH) =>
Array(godTokenLength)
.fill(null)
.map(() => words[randomInt(0, words.length)])
.join(' ');
public findFriendByName(name: string) {
return this.prismaService.friend.findUnique({
where: { name },
});
}
public createTokenForFriend(friend: Friend) {
return this.prismaService.godToken.create({
data: {
token: this.makeGodToken(),
friend_id: friend.id,
expiration: new Date(new Date().getTime() + GOD_TOKEN_EXPIRATION_MS),
},
});
}
public deleteToken(token: string) {
return this.prismaService.godToken.delete({
where: { token },
});
}
public signToken(token: string) {
return this.prismaService.godToken.update({
where: { token },
data: {
signed: true,
},
});
}
public findGodTokenWithFriend(token: string) {
return this.prismaService.godToken.findUnique({
where: { token },
include: {
friend: true,
},
});
}
}

5010
server/src/auth/words.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,4 @@
import { Module } from '@nestjs/common';
@Module({})
export class TimerModule {}