diff --git a/src/auth/application/authorize-oauth/authorize-oauth.use-case.ts b/src/auth/application/authorize-oauth/authorize-oauth.use-case.ts index 19199ca..2bd6828 100644 --- a/src/auth/application/authorize-oauth/authorize-oauth.use-case.ts +++ b/src/auth/application/authorize-oauth/authorize-oauth.use-case.ts @@ -8,9 +8,12 @@ export class AuthorizeOAuthUseCase { constructor(private readonly oAuthProviderFactory: OAuthProviderFactory) {} execute(requestDto: AuthorizeOAuthRequestDto): AuthorizeOAuthResponseDto { - const { oAuthProviderType } = requestDto; + const { oAuthProviderType, redirectUrl } = requestDto; const provider = this.oAuthProviderFactory.getProvider(oAuthProviderType); - const authUrl = provider.getAuthorizationUrl(); + + const encodedState = encodeURIComponent(redirectUrl || ''); + + const authUrl = provider.getAuthorizationUrl(encodedState); return { authUrl }; } diff --git a/src/auth/application/authorize-oauth/dto/authorize-oauth.request.dto.ts b/src/auth/application/authorize-oauth/dto/authorize-oauth.request.dto.ts index be78566..028223b 100644 --- a/src/auth/application/authorize-oauth/dto/authorize-oauth.request.dto.ts +++ b/src/auth/application/authorize-oauth/dto/authorize-oauth.request.dto.ts @@ -1,8 +1,11 @@ -import { IsEnum, IsNotEmpty } from 'class-validator'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { OAuthProviderType } from 'src/auth/domain/value-object/oauth-provider.enum'; export class AuthorizeOAuthRequestDto { @IsEnum(OAuthProviderType) @IsNotEmpty() oAuthProviderType: OAuthProviderType; + + @IsString() + redirectUrl?: string; } diff --git a/src/auth/application/oauth-login/dto/oauth-login.response.dto.ts b/src/auth/application/oauth-login/dto/oauth-login.response.dto.ts index d96259c..baa225d 100644 --- a/src/auth/application/oauth-login/dto/oauth-login.response.dto.ts +++ b/src/auth/application/oauth-login/dto/oauth-login.response.dto.ts @@ -8,4 +8,11 @@ export class OAuthLoginResponseDto { @IsString() @IsNotEmpty() refreshToken: string; + + @IsString() + @IsNotEmpty() + userId: string; + + @IsString() + redirectUrl: string; } diff --git a/src/auth/application/oauth-login/oauth-login.command.ts b/src/auth/application/oauth-login/oauth-login.command.ts index 1b5cc00..6a05f95 100644 --- a/src/auth/application/oauth-login/oauth-login.command.ts +++ b/src/auth/application/oauth-login/oauth-login.command.ts @@ -5,5 +5,6 @@ export class OAuthLoginCommand implements ICommand { constructor( public readonly oAuthProviderType: OAuthProviderType, public readonly code: string, + public readonly state?: string, ) {} } diff --git a/src/auth/application/oauth-login/oauth-login.handler.ts b/src/auth/application/oauth-login/oauth-login.handler.ts index bd5fc74..76c5a70 100644 --- a/src/auth/application/oauth-login/oauth-login.handler.ts +++ b/src/auth/application/oauth-login/oauth-login.handler.ts @@ -33,8 +33,9 @@ export class OAuthLoginUseCase { const { oauthId, provider, email } = await this.getOAuthUserInfo(oAuthProviderType, code); const auth = await this.findOrCreateAuth(oauthId, provider, email); const { accessToken, refreshToken } = await this.generateAndSaveTokens(auth); + const redirectUrl = this.decodeRedirectUrl(command.state); - return { accessToken, refreshToken }; + return { accessToken, refreshToken, userId: auth.userId.value, redirectUrl }; } // 소셜로그인 유저저 정보 가져오기 @@ -85,4 +86,13 @@ export class OAuthLoginUseCase { return { accessToken, refreshToken }; } + + // 리다이렉트 url 복호화 및 반환 + private decodeRedirectUrl(state?: string): string { + if (!state) return ''; + + const decodedState = decodeURIComponent(state); + + return decodedState; + } } diff --git a/src/auth/domain/entity/auth.ts b/src/auth/domain/entity/auth.ts index 6f46780..ad54c97 100644 --- a/src/auth/domain/entity/auth.ts +++ b/src/auth/domain/entity/auth.ts @@ -27,6 +27,10 @@ export class Auth extends AggregateRoot { return auth; } + public static of(props: AuthProps): Auth { + return new Auth(props); + } + public validate(): void { if (!this.props.oauthId) { throw new CustomException(CustomExceptionCode.AUTH_OAUTH_ID_EMPTY); diff --git a/src/auth/infrastructure/mapper/auth.mapper.ts b/src/auth/infrastructure/mapper/auth.mapper.ts index 5647274..82637be 100644 --- a/src/auth/infrastructure/mapper/auth.mapper.ts +++ b/src/auth/infrastructure/mapper/auth.mapper.ts @@ -6,7 +6,7 @@ import { UserEntity } from 'src/user/command/infrastructure/user.entity'; export class AuthMapper { static toDomain(entity: AuthEntity): Auth { - return Auth.create({ + return Auth.of({ id: Identifier.from(entity.id), createdAt: entity.createdAt, updatedAt: entity.updatedAt, diff --git a/src/auth/presentation/auth.controller.ts b/src/auth/presentation/auth.controller.ts index 5071a18..5d63a7d 100644 --- a/src/auth/presentation/auth.controller.ts +++ b/src/auth/presentation/auth.controller.ts @@ -25,30 +25,37 @@ export class AuthController { @Get('oauth/authorization') @AuthDocs('oauthAuthorization') - authorizeOAuth(@Res() res: Response) { - const { authUrl } = this.authorizeOAuthUseCase.execute({ oAuthProviderType: OAuthProviderType.KAKAO }); + authorizeOAuth(@Query('returnPath') returnPath?: string) { + const { authUrl } = this.authorizeOAuthUseCase.execute({ + oAuthProviderType: OAuthProviderType.KAKAO, + redirectUrl: returnPath, + }); - res.redirect(authUrl); + return { authUrl, returnPath }; } - @Get('login/oauth/callback') + @Post('login/oauth/callback') @AuthDocs('oauthCallback') - async oAuthLogin(@Res() res: Response, @Query('code') code: string, @Query('error') error?: string) { + async oAuthLogin( + @Res() res: Response, + @Query('code') code: string, + @Query('error') error?: string, + @Query('state') state?: string, + ) { if (error && error === 'access_denied') { - return res.redirect(`${this.configService.getOrThrow('frontend.url')}/login?error=cancelled`); + return res.status(HttpStatus.UNAUTHORIZED).json({ message: 'Access denied' }); } - const { accessToken, refreshToken } = await this.oAuthLoginUseCase.execute({ + const { accessToken, refreshToken, userId, redirectUrl } = await this.oAuthLoginUseCase.execute({ oAuthProviderType: OAuthProviderType.KAKAO, code, + state, }); res.cookie('accessToken', accessToken, accessTokenCookieOptions); res.cookie('refreshToken', refreshToken, refreshTokenCookieOptions); - res.redirect( - `${this.configService.getOrThrow('frontend.url')}/${this.configService.getOrThrow('frontend.loginRedirectPath')}`, - ); + res.json({ userId, redirectUrl }); } @Get('refresh') diff --git a/src/shared/core/infrastructure/oauth/base-oauth.provider.ts b/src/shared/core/infrastructure/oauth/base-oauth.provider.ts index 301f96d..649d4dd 100644 --- a/src/shared/core/infrastructure/oauth/base-oauth.provider.ts +++ b/src/shared/core/infrastructure/oauth/base-oauth.provider.ts @@ -3,7 +3,7 @@ import { OAuthProviderType } from 'src/auth/domain/value-object/oauth-provider.e export interface BaseOAuthProvider { getToken(code: string): Promise; getUserInfo(token: string): Promise; - getAuthorizationUrl(): string; + getAuthorizationUrl(state?: string): string; unlinkAccount(userId: string): Promise; } diff --git a/src/shared/core/infrastructure/oauth/kakao.provider.ts b/src/shared/core/infrastructure/oauth/kakao.provider.ts index 3429ac2..d2de518 100644 --- a/src/shared/core/infrastructure/oauth/kakao.provider.ts +++ b/src/shared/core/infrastructure/oauth/kakao.provider.ts @@ -71,7 +71,7 @@ export class KakaoOAuthProvider implements BaseOAuthProvider { return res.data.access_token; } - getAuthorizationUrl(): string { + getAuthorizationUrl(state?: string): string { const authorizeUrl = 'https://kauth.kakao.com/oauth/authorize'; const clientId = this.configService.get('kakao.clientId'); const redirectUri = this.configService.get('kakao.redirectUri'); @@ -84,6 +84,9 @@ export class KakaoOAuthProvider implements BaseOAuthProvider { params.append('response_type', 'code'); params.append('client_id', clientId); params.append('redirect_uri', redirectUri); + if (state) { + params.append('state', state); + } return `${authorizeUrl}?${params.toString()}`; }