From bce5153293ed7fc4b5667b5918ce8f16c16a3787 Mon Sep 17 00:00:00 2001 From: kakiba <97882386+kotto5@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:17:15 +0900 Subject: [PATCH] =?UTF-8?q?oauth2.0=20signup=E3=81=AE=E5=AE=9F=E8=A3=85=20?= =?UTF-8?q?(#211)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [frontend] user can signup with /frontend/app/(guest-only)/signup/oauth --- .env.template | 7 ++- backend/src/auth/auth.controller.ts | 7 +++ backend/src/auth/auth.service.ts | 59 +++++++++++++++++++ backend/src/auth/dto/oauth.dto.ts | 8 +++ compose.yml | 5 ++ .../app/(guest-only)/signup/oauth/page.tsx | 20 +++++++ .../signup/oauth/redirect/page.tsx | 19 ++++++ frontend/app/lib/actions.ts | 22 +++++++ frontend/package-lock.json | 7 +++ frontend/package.json | 1 + 10 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 backend/src/auth/dto/oauth.dto.ts create mode 100644 frontend/app/(guest-only)/signup/oauth/page.tsx create mode 100644 frontend/app/(guest-only)/signup/oauth/redirect/page.tsx diff --git a/.env.template b/.env.template index 38445b16..70c19aef 100644 --- a/.env.template +++ b/.env.template @@ -9,4 +9,9 @@ POSTGRES_DB= JWT_PUBLIC_KEY= JWT_PRIVATE_KEY= FRONTEND_JWT_SECRET= -TWO_FACTOR_AUTHENTICATION_APP_NAME= \ No newline at end of file +TWO_FACTOR_AUTHENTICATION_APP_NAME= +OAUTH_GOOGLE_CLIENT_ID= +OAUTH_GOOGLE_CLIENT_SECRET= +OAUTH_42_CLIENT_ID= +OAUTH_42_CLIENT_SECRET= +OAUTH_REDIRECT_URI= \ No newline at end of file diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts index e0375d96..b0f6919d 100644 --- a/backend/src/auth/auth.controller.ts +++ b/backend/src/auth/auth.controller.ts @@ -13,6 +13,7 @@ import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto'; import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto'; import { AuthEntity } from './entity/auth.entity'; import { JwtGuardWithout2FA } from './jwt-auth.guard'; +import { OauthDto } from './dto/oauth.dto'; @Controller('auth') @ApiTags('auth') @@ -25,6 +26,12 @@ export class AuthController { return this.authService.login(email, password); } + @Post('oauth2/signup/42') + @ApiCreatedResponse({ type: AuthEntity }) + async signupWith42(@Body() dto: OauthDto) { + return this.authService.signupWith42(dto); + } + @Post('2fa/generate') @UseGuards(JwtGuardWithout2FA) @ApiBearerAuth() diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts index 0ea19e9b..61e458f7 100644 --- a/backend/src/auth/auth.service.ts +++ b/backend/src/auth/auth.service.ts @@ -14,6 +14,9 @@ import { jwtConstants } from './auth.module'; import { TwoFactorAuthenticationDto } from './dto/twoFactorAuthentication.dto'; import { TwoFactorAuthenticationEnableDto } from './dto/twoFactorAuthenticationEnable.dto'; import { AuthEntity } from './entity/auth.entity'; +import { CreateUserDto } from 'src/user/dto/create-user.dto'; +import { UserEntity } from 'src/user/entities/user.entity'; +import { OauthDto } from './dto/oauth.dto'; @Injectable() export class AuthService { @@ -77,6 +80,62 @@ export class AuthService { }); } + async signupWith42(dto: OauthDto): Promise { + // 1. Get access token + const client_id = process.env.OAUTH_42_CLIENT_ID; + const client_secret = process.env.OAUTH_42_CLIENT_SECRET; + + const form = new URLSearchParams({ + grant_type: 'authorization_code', + client_id, + client_secret, + code: dto.code, + redirect_uri: process.env.OAUTH_REDIRECT_URI, + state: '42', // TODO : implement state system for enhanced security + }); + + const token = await fetch('https://api.intra.42.fr/oauth/token', { + method: 'POST', + body: form, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }).then((res) => { + if (!res.ok) { + throw new Error(res.statusText); + } + return res.json(); + }); + const { access_token } = token; + console.log('token', token); + + // 2. Get user info + const userRes = await fetch('https://api.intra.42.fr/v2/me', { + headers: { + Authorization: `Bearer ${access_token}`, + }, + }); + if (!userRes.ok) { + throw new Error(userRes.statusText); + } + const userJson = await userRes.json(); + const { email, login } = userJson; + if (!email || !login) { + throw new Error('Invalid user info'); + } + + // 3. Create user + const hashedPassword = await bcrypt.hash(login, 10); + // TODO : random password? without password? + // TODO : save access_token in db + const userData: CreateUserDto = { + email, + password: hashedPassword, + name: login, + }; + return this.prisma.user.create({ data: userData }); + } + async pipeQrCodeStream(stream: Response, otpAuthUrl: string) { return toFileStream(stream, otpAuthUrl); } diff --git a/backend/src/auth/dto/oauth.dto.ts b/backend/src/auth/dto/oauth.dto.ts new file mode 100644 index 00000000..a7d9ef21 --- /dev/null +++ b/backend/src/auth/dto/oauth.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class OauthDto { + @IsString() + @ApiProperty() + code: string; +} diff --git a/compose.yml b/compose.yml index fff82e77..794b6883 100644 --- a/compose.yml +++ b/compose.yml @@ -19,6 +19,8 @@ services: NEXT_PUBLIC_WEB_URL: ${PUBLIC_WEB_URL} JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY} JWT_SECRET: ${FRONTEND_JWT_SECRET} + OAUTH_42_CLIENT_ID: ${OAUTH_42_CLIENT_ID} + OAUTH_REDIRECT_URI: ${OAUTH_REDIRECT_URI} depends_on: backend: condition: service_healthy @@ -36,6 +38,9 @@ services: POSTGRES_DB: ${POSTGRES_DB} DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} TWO_FACTOR_AUTHENTICATION_APP_NAME: ${TWO_FACTOR_AUTHENTICATION_APP_NAME} + OAUTH_42_CLIENT_ID: ${OAUTH_42_CLIENT_ID} + OAUTH_42_CLIENT_SECRET: ${OAUTH_42_CLIENT_SECRET} + OAUTH_REDIRECT_URI: ${OAUTH_REDIRECT_URI} healthcheck: test: ["CMD", "curl", "-f", "http://localhost:${BACKEND_PORT}/api"] interval: 5s diff --git a/frontend/app/(guest-only)/signup/oauth/page.tsx b/frontend/app/(guest-only)/signup/oauth/page.tsx new file mode 100644 index 00000000..a1be518d --- /dev/null +++ b/frontend/app/(guest-only)/signup/oauth/page.tsx @@ -0,0 +1,20 @@ +import { redirect } from "next/navigation"; + +const signupWith42 = () => { + const response_type = "code"; + const client_id = process.env.OAUTH_42_CLIENT_ID; + const redirect_uri = process.env.OAUTH_REDIRECT_URI; + const scope = "public"; + const state = "42"; + + const url = + "https://api.intra.42.fr/oauth/authorize?" + + `&client_id=${client_id}` + + `&redirect_uri=${redirect_uri}` + + `&response_type=${response_type}` + + `&scope=${scope}` + + `&state=${state}`; + redirect(url); +}; + +export default signupWith42; diff --git a/frontend/app/(guest-only)/signup/oauth/redirect/page.tsx b/frontend/app/(guest-only)/signup/oauth/redirect/page.tsx new file mode 100644 index 00000000..755b1069 --- /dev/null +++ b/frontend/app/(guest-only)/signup/oauth/redirect/page.tsx @@ -0,0 +1,19 @@ +import { createUserWithOauth } from "@/app/lib/actions"; + +const Callback = async ({ + searchParams, +}: { + searchParams?: { [key: string]: string | string[] | undefined }; +}) => { + if (searchParams === undefined) { + return

hoge

; + } + console.log(searchParams); + if (searchParams["code"] === undefined) { + return

hoge

; + } + await createUserWithOauth(searchParams["code"], "42"); + return

hoge

; +}; + +export default Callback; diff --git a/frontend/app/lib/actions.ts b/frontend/app/lib/actions.ts index be50bc66..6324dea8 100644 --- a/frontend/app/lib/actions.ts +++ b/frontend/app/lib/actions.ts @@ -802,3 +802,25 @@ export async function leaveRoom(roomId: number) { return "Success"; } } + +export async function createUserWithOauth( + code: string | string[] | undefined, + provider: string, +) { + if (!code) return; + const url = `${process.env.API_URL}/auth/oauth2/signup/${provider}`; + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code }), + }); + if (!res.ok) { + console.error("createUserWithOauth error: ", await res.json()); + // TODO Implement user notification for signup requirement + redirect("/signup"); + } else { + redirect("/login"); + } +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 771aab00..e6fb1e3b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,6 +39,7 @@ "devDependencies": { "@types/node": "^20", "@types/qrcode": "^1.5.5", + "@types/qs": "^6.9.11", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.7", @@ -1301,6 +1302,12 @@ "@types/node": "*" } }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "dev": true + }, "node_modules/@types/react": { "version": "18.2.45", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6326f4f9..bde20b94 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "devDependencies": { "@types/node": "^20", "@types/qrcode": "^1.5.5", + "@types/qs": "^6.9.11", "@types/react": "^18", "@types/react-dom": "^18", "@types/uuid": "^9.0.7",