Skip to content

Commit c32a0c9

Browse files
authored
Merge pull request #62 from GeneralMagicio/add_cors_allow_list
feat: implement authentication flow
2 parents c9452cd + b9a75d3 commit c32a0c9

File tree

15 files changed

+308
-37
lines changed

15 files changed

+308
-37
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
DATABASE_URL=
22
WHITELISTED_ORIGINS=
3-
TUNNEL_DOMAINS=
3+
TUNNEL_DOMAINS=
4+
JWT_SECRET=

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"class-validator": "^0.14.1",
3535
"cookie-parser": "^1.4.7",
3636
"crypto": "^1.0.1",
37+
"jsonwebtoken": "^9.0.2",
3738
"react": "^19.1.0",
3839
"reflect-metadata": "^0.2.2",
3940
"rxjs": "^7.8.1",

src/app.controller.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Controller, Get } from '@nestjs/common';
22
import { AppService } from './app.service';
3+
import { Public } from './auth/jwt-auth.guard';
34

45
@Controller()
56
export class AppController {
67
constructor(private readonly appService: AppService) {}
78

9+
@Public()
810
@Get()
911
getHello(): string {
1012
return this.appService.getHello();

src/auth/auth.controller.ts

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,11 @@
1-
import {
2-
BadRequestException,
3-
Body,
4-
Controller,
5-
Get,
6-
Post,
7-
Req,
8-
Res,
9-
} from '@nestjs/common';
10-
import { MiniAppWalletAuthSuccessPayload } from '@worldcoin/minikit-js';
1+
import { Body, Controller, Get, Post, Req, Res } from '@nestjs/common';
112
import { Request, Response } from 'express';
123
import { AuthService } from './auth.service';
13-
14-
interface IRequestPayload {
15-
payload: MiniAppWalletAuthSuccessPayload;
16-
}
17-
18-
type RequestWithCookies = Request & {
19-
cookies: {
20-
siwe?: string;
21-
};
22-
};
4+
import { JwtService } from './jwt.service';
5+
import { Public } from './jwt-auth.guard';
6+
import { VerifyWorldIdDto } from './auth.dto';
7+
import { SignatureVerificationFailureException } from 'src/common/exceptions';
8+
import { BadRequestException } from '@nestjs/common';
239

2410
function isHttps(req: Request) {
2511
return (
@@ -29,8 +15,12 @@ function isHttps(req: Request) {
2915

3016
@Controller('auth')
3117
export class AuthController {
32-
constructor(private readonly authService: AuthService) {}
18+
constructor(
19+
private readonly authService: AuthService,
20+
private readonly jwtService: JwtService,
21+
) {}
3322

23+
@Public()
3424
@Get('nonce')
3525
generateNonce(@Req() req: Request, @Res() res: Response) {
3626
const nonce = this.authService.generateNonce();
@@ -41,20 +31,45 @@ export class AuthController {
4131
maxAge: 2 * 60 * 1000, //2 minutes
4232
});
4333

44-
return { nonce };
34+
return res.json({ nonce });
4535
}
4636

47-
@Post('verifyPayload')
48-
async verifyPayload(
49-
@Req() req: RequestWithCookies,
50-
@Body() body: IRequestPayload,
37+
@Public()
38+
@Post('verifyWorldId')
39+
async verifyWorldId(
40+
@Req() _req: Request,
41+
@Body() body: VerifyWorldIdDto,
42+
@Res() res: Response,
5143
) {
52-
const { payload } = body;
53-
const storedNonce = req.cookies.siwe;
54-
if (!storedNonce) {
55-
throw new BadRequestException('No nonce found in cookies');
44+
const { walletPayload, worldIdProof, nonce } = body;
45+
46+
try {
47+
const isValid = await this.authService.verifyPayload(
48+
walletPayload,
49+
nonce,
50+
);
51+
52+
if (!isValid) {
53+
throw new SignatureVerificationFailureException();
54+
}
55+
56+
const worldID = worldIdProof?.nullifier_hash;
57+
const walletAddress = walletPayload?.address;
58+
59+
const user = await this.authService.createUser(worldID, '');
60+
61+
const token = this.jwtService.sign({
62+
userId: user.id,
63+
worldID,
64+
address: walletAddress,
65+
});
66+
67+
return res.status(200).json({ isValid: true, token });
68+
} catch (error) {
69+
console.error(error);
70+
throw new BadRequestException(
71+
error instanceof Error ? error.message : 'Unknown error',
72+
);
5673
}
57-
const isValid = await this.authService.verifyPayload(payload, storedNonce);
58-
return { isValid };
5974
}
6075
}

src/auth/auth.dto.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { IsNotEmpty, IsString, ValidateNested } from 'class-validator';
2+
import {
3+
MiniAppWalletAuthSuccessPayload,
4+
ISuccessResult,
5+
} from '@worldcoin/minikit-js';
6+
import { Type } from 'class-transformer';
7+
8+
export class VerifyWorldIdDto {
9+
@IsNotEmpty()
10+
@ValidateNested()
11+
@Type(() => Object)
12+
walletPayload: MiniAppWalletAuthSuccessPayload;
13+
14+
@IsNotEmpty()
15+
@ValidateNested()
16+
@Type(() => Object)
17+
worldIdProof: ISuccessResult;
18+
19+
@IsNotEmpty()
20+
@IsString()
21+
nonce: string;
22+
}

src/auth/auth.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Module } from '@nestjs/common';
22
import { AuthController } from './auth.controller';
3+
import { JwtService } from './jwt.service';
34
import { AuthService } from './auth.service';
45

56
@Module({
67
controllers: [AuthController],
7-
providers: [AuthService],
8+
providers: [AuthService, JwtService],
9+
exports: [AuthService],
810
})
911
export class AuthModule {}

src/auth/auth.service.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ import * as crypto from 'crypto';
33
import {
44
MiniAppWalletAuthSuccessPayload,
55
verifySiweMessage,
6+
SiweMessage,
67
} from '@worldcoin/minikit-js';
78
import { DatabaseService } from 'src/database/database.service';
9+
import { CreateUserException } from 'src/common/exceptions';
10+
11+
interface IValidMessage {
12+
isValid: boolean;
13+
siweMessageData: SiweMessage;
14+
}
815

916
@Injectable()
1017
export class AuthService {
@@ -18,7 +25,22 @@ export class AuthService {
1825
}
1926

2027
async verifyPayload(payload: MiniAppWalletAuthSuccessPayload, nonce: string) {
21-
const validMessage = await verifySiweMessage(payload, nonce);
28+
const validMessage = (await verifySiweMessage(
29+
payload,
30+
nonce,
31+
)) as IValidMessage;
2232
return validMessage.isValid;
2333
}
34+
35+
async createUser(worldID: string, name: string) {
36+
const user = await this.databaseService.user.upsert({
37+
where: { worldID },
38+
update: { name },
39+
create: { worldID, name },
40+
});
41+
42+
if (!user) throw new CreateUserException();
43+
44+
return user;
45+
}
2446
}

src/auth/jwt-auth.guard.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
CanActivate,
3+
ExecutionContext,
4+
Injectable,
5+
UnauthorizedException,
6+
} from '@nestjs/common';
7+
import { Request } from 'express';
8+
import { Reflector } from '@nestjs/core';
9+
import { SetMetadata } from '@nestjs/common';
10+
import { JwtService } from './jwt.service';
11+
12+
const IS_PUBLIC_KEY = 'isPublic';
13+
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
14+
15+
export interface JwtPayload {
16+
userId: number;
17+
worldID: string;
18+
address: string;
19+
}
20+
21+
@Injectable()
22+
export class JwtAuthGuard implements CanActivate {
23+
constructor(
24+
private reflector: Reflector,
25+
private readonly jwtService: JwtService,
26+
) {}
27+
28+
canActivate(context: ExecutionContext): boolean {
29+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
30+
context.getHandler(),
31+
context.getClass(),
32+
]);
33+
if (isPublic) {
34+
return true;
35+
}
36+
37+
const request = context.switchToHttp().getRequest<Request>();
38+
const authHeader = request.headers.authorization;
39+
if (!authHeader?.startsWith('Bearer ')) {
40+
throw new UnauthorizedException('Missing or invalid token');
41+
}
42+
43+
const token = authHeader.replace('Bearer ', '');
44+
45+
try {
46+
const payload: JwtPayload = this.jwtService.verify(token);
47+
request.user = payload;
48+
return true;
49+
} catch (err) {
50+
console.error(err);
51+
throw new UnauthorizedException('Invalid or expired token');
52+
}
53+
}
54+
}

src/auth/jwt.service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Injectable } from '@nestjs/common';
2+
import jwt, { JwtPayload } from 'jsonwebtoken';
3+
import { JwtVerificationFailureException } from 'src/common/exceptions';
4+
5+
@Injectable()
6+
export class JwtService {
7+
private readonly secret =
8+
process.env.JWT_SECRET ||
9+
(function () {
10+
throw new Error('JWT_SECRET environment variable must be set');
11+
})();
12+
private readonly expiresIn = '7d';
13+
14+
sign(payload: object): string {
15+
return jwt.sign(payload, this.secret, { expiresIn: this.expiresIn });
16+
}
17+
18+
verify(token: string): JwtPayload | string {
19+
try {
20+
return jwt.verify(token, this.secret);
21+
} catch (error) {
22+
console.error(error);
23+
throw new JwtVerificationFailureException();
24+
}
25+
}
26+
27+
decode(token: string): null | { [key: string]: any } | string {
28+
return jwt.decode(token);
29+
}
30+
}

src/auth/user.docerator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2+
import { JwtPayload } from './jwt-auth.guard';
3+
import { Request } from 'express';
4+
5+
export const User = createParamDecorator(
6+
(field: keyof JwtPayload, ctx: ExecutionContext) => {
7+
const request = ctx.switchToHttp().getRequest<Request>();
8+
return field ? request.user?.[field] : request.user;
9+
},
10+
);

0 commit comments

Comments
 (0)