Skip to content

Commit

Permalink
oauth2.0 signupの実装 (#211)
Browse files Browse the repository at this point in the history
* [frontend] user can signup with /frontend/app/(guest-only)/signup/oauth
  • Loading branch information
kotto5 authored Jan 16, 2024
1 parent 735cbbc commit bce5153
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 1 deletion.
7 changes: 6 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ POSTGRES_DB=
JWT_PUBLIC_KEY=
JWT_PRIVATE_KEY=
FRONTEND_JWT_SECRET=
TWO_FACTOR_AUTHENTICATION_APP_NAME=
TWO_FACTOR_AUTHENTICATION_APP_NAME=
OAUTH_GOOGLE_CLIENT_ID=
OAUTH_GOOGLE_CLIENT_SECRET=
OAUTH_42_CLIENT_ID=
OAUTH_42_CLIENT_SECRET=
OAUTH_REDIRECT_URI=
7 changes: 7 additions & 0 deletions backend/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()
Expand Down
59 changes: 59 additions & 0 deletions backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -77,6 +80,62 @@ export class AuthService {
});
}

async signupWith42(dto: OauthDto): Promise<UserEntity> {
// 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);
}
Expand Down
8 changes: 8 additions & 0 deletions backend/src/auth/dto/oauth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';

export class OauthDto {
@IsString()
@ApiProperty()
code: string;
}
5 changes: 5 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions frontend/app/(guest-only)/signup/oauth/page.tsx
Original file line number Diff line number Diff line change
@@ -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;
19 changes: 19 additions & 0 deletions frontend/app/(guest-only)/signup/oauth/redirect/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createUserWithOauth } from "@/app/lib/actions";

const Callback = async ({
searchParams,
}: {
searchParams?: { [key: string]: string | string[] | undefined };
}) => {
if (searchParams === undefined) {
return <h1>hoge</h1>;
}
console.log(searchParams);
if (searchParams["code"] === undefined) {
return <h1>hoge</h1>;
}
await createUserWithOauth(searchParams["code"], "42");
return <h1>hoge</h1>;
};

export default Callback;
22 changes: 22 additions & 0 deletions frontend/app/lib/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
7 changes: 7 additions & 0 deletions frontend/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 frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit bce5153

Please sign in to comment.