diff --git a/src/auth/oauth.controller.ts b/src/auth/oauth.controller.ts index c76b8ef..faf6015 100644 --- a/src/auth/oauth.controller.ts +++ b/src/auth/oauth.controller.ts @@ -1,4 +1,12 @@ -import { Controller, Get, Res, Query, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Res, + Query, + UseGuards, + Post, + Body, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { ApiTags, @@ -91,4 +99,34 @@ export class OAuthController { ): Promise { return this.oauthService.googleOauth(user); } + + @ApiOperation({ + summary: '애플 계정 로그인 요청', + description: '애플 계정 로그인 요청 메서드', + }) + @Get('apple-auth') + async getAppleRedirectUrl(@Res() res: Response): Promise { + const { url } = await this.oauthService.getAppleRedirectUrl(); + return res.redirect(url); + } + + @ApiOperation({ + summary: '애플 로그인 리다이렉트 후 클라이언트 리다이렉트', + }) + @Post('apple-code') + async appleCode(@Body('code') code: string, @Res() res: Response) { + return res.redirect( + `${process.env.APPLE_REDIRECT_FRONTEND_URI}?code=${code}`, + ); + } + + @ApiOperation({ + summary: '애플 로그인', + description: + '애플 로그인 메서드. (회원가입이 안되어 있으면 회원가입 처리 후 로그인 처리)', + }) + @Get('apple-login') + async appleLogin(@Query('code') code: string): Promise { + return this.oauthService.appleLogin(code); + } } diff --git a/src/auth/oauth.service.ts b/src/auth/oauth.service.ts index 71769ac..688ff86 100644 --- a/src/auth/oauth.service.ts +++ b/src/auth/oauth.service.ts @@ -2,6 +2,7 @@ import { Injectable, UnauthorizedException, BadRequestException, + InternalServerErrorException, } from '@nestjs/common'; import axios from 'axios'; import { refreshTokenExpirationInCache } from './auth.module'; @@ -159,4 +160,54 @@ export class OAuthService { secure: true, }; } + + public async getAppleRedirectUrl() { + const appleClientId = process.env.APPLE_CLIENT_ID; + const redirectUri = process.env.APPLE_REDIRECT_URI; + + const config = { + client_id: appleClientId, // This is the service ID we created. + redirect_uri: redirectUri, // As registered along with our service ID + response_type: 'code id_token', + state: 'origin:web', // Any string of your choice that you may use for some logic. It's optional and you may omit it. + scope: 'name email', // To tell apple we want the user name and emails fields in the response it sends us. + response_mode: 'form_post', + m: 11, + v: '1.5.4', + }; + const queryString = Object.entries(config) + .map(([key, value]) => `${key}=${encodeURIComponent(value!)}`) + .join('&'); + + return { url: `https://appleid.apple.com/auth/authorize?${queryString}` }; + } + + public async appleLogin(code: string) { + const { data } = await this.oauthUtil.getAppleToken(code); + + if (!data.id_token) { + throw new InternalServerErrorException( + `No token: ${JSON.stringify(data)}`, + ); + } + + const { sub: id, email } = this.jwtService.verify(data.id_token); + + let user = await this.userRepository.findOneByEmail(email); + + if (!user) { + user = new User(); + user.email = email; + user.name = email.split('@')[0]; + user.password = this.encodePasswordFromEmail( + email, + process.env.APPLE_CLIENT_ID, + ); + + await this.userRepository.createOne(user); + await this.categoryRepository.createDefaultCategories(user); + } + + return this.oauthLogin(user.email); + } } diff --git a/src/auth/util/oauth.util.ts b/src/auth/util/oauth.util.ts index 55a9103..29a3653 100644 --- a/src/auth/util/oauth.util.ts +++ b/src/auth/util/oauth.util.ts @@ -9,9 +9,12 @@ import { GetKakaoAccessTokenOutput, GetKakaoUserInfoOutput, } from '../dtos/kakao.dto'; +import { JwtService } from '@nestjs/jwt'; @Injectable() export class OAuthUtil { + constructor(private readonly jwtService: JwtService) {} + // Get access token from Kakao Auth Server async getKakaoAccessToken(code: string): Promise { try { @@ -68,4 +71,36 @@ export class OAuthUtil { throw e; } } + + getAppleAccessToken(): string { + return this.jwtService.sign( + {}, + { + audience: 'https://appleid.apple.com', + issuer: process.env.APPLE_TEAM_ID, + subject: process.env.APPLE_CLIENT_ID, + expiresIn: '1h', + keyid: process.env.APPLE_KEY_ID, + algorithm: 'ES256', + }, + ); + } + + async getAppleToken(code: string) { + return await axios.post( + 'https://appleid.apple.com/auth/token', + qs.stringify({ + grant_type: 'authorization_code', + code, + client_secret: this.getAppleAccessToken(), + client_id: process.env.APPLE_CLIENT_ID, + redirect_uri: process.env.APPLE_REDIRECT_URI, + }), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + } }