feat: add authentication using Discord oauth2 and JWT

Temporarily disabling GraphQL using Mercuris due to moving to Express (previously fastify)
This commit is contained in:
M1000fr 2024-11-28 15:25:01 +01:00
parent 782698788b
commit fbf7272526
9 changed files with 660 additions and 58 deletions

View File

@ -14,6 +14,7 @@
"preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('You must use Yarn to install, not NPM')\"" "preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('You must use Yarn to install, not NPM')\""
}, },
"dependencies": { "dependencies": {
"@fastify/passport": "^3.0.1",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.3.0", "@nestjs/config": "^3.3.0",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
@ -21,6 +22,7 @@
"@nestjs/jwt": "^10.2.0", "@nestjs/jwt": "^10.2.0",
"@nestjs/mercurius": "^12.2.1", "@nestjs/mercurius": "^12.2.1",
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.11",
"@nestjs/platform-fastify": "^10.4.9", "@nestjs/platform-fastify": "^10.4.9",
"@prisma/client": "5.22.0", "@prisma/client": "5.22.0",
"axios": "^1.7.7", "axios": "^1.7.7",
@ -29,6 +31,9 @@
"graphql-tools": "^9.0.4", "graphql-tools": "^9.0.4",
"graphql-ws": "^5.16.0", "graphql-ws": "^5.16.0",
"mercurius": "14", "mercurius": "14",
"passport": "^0.7.0",
"passport-discord": "^0.1.4",
"passport-jwt": "^4.0.1",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@ -39,6 +44,7 @@
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/node": "^20.3.1", "@types/node": "^20.3.1",
"@types/passport-discord": "^0.1.14",
"@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0", "@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.0.0", "eslint": "^9.0.0",

View File

@ -8,6 +8,7 @@ import { AppService } from "./app.service";
import { UserModule } from "./user/user.module"; import { UserModule } from "./user/user.module";
import { GraphqlOptions } from "./graphql/graphqlOptions"; import { GraphqlOptions } from "./graphql/graphqlOptions";
import { AuthModule } from './auth/auth.module';
@Module({ @Module({
imports: [ imports: [
@ -18,11 +19,12 @@ import { GraphqlOptions } from "./graphql/graphqlOptions";
global: true, global: true,
secret: process.env.JWT_SECRET, secret: process.env.JWT_SECRET,
}), }),
GraphQLModule.forRootAsync<MercuriusDriverConfig>({ // GraphQLModule.forRootAsync<MercuriusDriverConfig>({
driver: MercuriusDriver, // driver: MercuriusDriver,
useClass: GraphqlOptions, // useClass: GraphqlOptions,
}), // }),
UserModule, UserModule,
AuthModule,
], ],
providers: [AppService], providers: [AppService],
}) })

View File

@ -0,0 +1,33 @@
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Profile, Strategy } from "passport-discord";
@Injectable()
export class DiscordStrategy extends PassportStrategy(Strategy, "discord") {
constructor(private jwtService: JwtService) {
super({
clientID: process.env.DISCORD_CLIENT_ID,
clientSecret: process.env.DISCORD_CLIENT_SECRET,
callbackURL: process.env.DISCORD_CALLBACK_URL,
scope: ["identify", "email"]
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: Profile,
done: Function,
) {
const jwtPayload = {
id: profile.id,
username: profile.username,
email: profile.email,
};
const jwt = this.jwtService.sign(jwtPayload);
done(null, { jwt });
}
}

View File

@ -0,0 +1,19 @@
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Profile } from "passport-discord";
import { Strategy, ExtractJwt } from "passport-jwt";
@Injectable()
export class JWTStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExipration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(profile: Profile): Promise<any> {
return profile;
}
}

View File

@ -0,0 +1,36 @@
import { Controller, Get, Req, Res, UseGuards } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
import { URLSearchParams } from "node:url";
@Controller("auth")
export class AuthController {
@Get("providers")
Providers() {
const discordOauth2Params = new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID,
response_type: "code",
redirect_uri: process.env.DISCORD_CALLBACK_URL,
scope: "identify email",
});
return {
discord: {
url: `https://discord.com/oauth2/authorize?${discordOauth2Params.toString()}`,
},
};
}
@Get("discord/callback")
@UseGuards(AuthGuard("discord"))
CallbackDiscord(@Req() req, @Res() res) {
const { user } = req;
res.send(user);
}
@Get("profile")
@UseGuards(AuthGuard("jwt"))
Profile(@Req() req) {
return req.user;
}
}

12
src/auth/auth.module.ts Normal file
View File

@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { DiscordStrategy } from "./Strategy/discord.strategy";
import { JWTStrategy } from "./Strategy/jwt.strategy";
@Module({
controllers: [AuthController],
providers: [AuthService, DiscordStrategy, JWTStrategy],
})
export class AuthModule {}

4
src/auth/auth.service.ts Normal file
View File

@ -0,0 +1,4 @@
import { Injectable } from "@nestjs/common";
@Injectable()
export class AuthService {}

View File

@ -1,12 +1,8 @@
import { NestFactory } from "@nestjs/core"; import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module"; import { AppModule } from "./app.module";
import {
FastifyAdapter,
NestFastifyApplication,
} from "@nestjs/platform-fastify";
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule, new FastifyAdapter()); const app = await NestFactory.create(AppModule);
app.enableCors({ app.enableCors({
origin: "*", origin: "*",

592
yarn.lock

File diff suppressed because it is too large Load Diff