JJONG`sFACTORY
반응형

Third party api를 이용하면서, 자체 API 서버를 구축하고 싶다.

개발을 진행하다보면 언젠가 오는 순간인데, Third party api를 사용하면서 자체적인 api 서버도 구축해야 할 때가 많다.

Third party api를 이용하기 위해선 Oauth 방식으로 인증을 구현하면 되는데, 자체 API 서버도 Oauth로 구축하게 된다면 SPA 형태로 개발된 클라이언트 사이드 측에서는 API서버의 resource와 third party 의 resource를 이용하기 위해선 두번의 인증을 거쳐야 하는 불편함이 있다.

사용자는 왜 로그인을 두번하냐고 생각하지 않을까

또한 요즘 많이 다양한 어플리케이션에서 지원되는 기능 중 계정 통합이라는 기능이 있는데, 과거에는 kakao로 로그인, naver로 로그인 등 third party api로 회원가입 하는 계정과 email + password로 회원가입 하는 계정이 각각 따로 만들어 졌지만 계정 통합으로 인해서 어떤 third party api로 회원가입/로그인을 진행 하더라도 하나의 계정으로 로그인 되는 경험을 한번쯤을 겪어 보았을 것이다.

 

그렇다면, 자체적인 API Server의 인증 절차는 기본적으로 Oauth 방식을 따르면 안된다.

어떻게 해결 해야 할까?

 

먼저 Oauth의 동작개념을 간략하게 알아보면서 API Server의 인증은 Oauth 인증을 거치게 되면 생기는 클라이언트 상의 문제점 부터 알아보자.

 

Oauth2.0 의 기본 구성 및 인증 방식

 

🌐 OAuth 2.0 개념 - 그림으로 이해하기 쉽게 설명

OAuth란? 웹 서핑을 하다 보면 Google과 Facebook 등의 외부 소셜 계정을 기반으로 간편히 회원가입 및 로그인할 수 있는 웹 어플리케이션을 쉽게 찾아볼 수 있다. 클릭 한 번으로 간편하게 로그인할

inpa.tistory.com

굉장히 정리가 잘 된 글이 있어서 링크로 달아두었다. 정확한 개념은 위 글을 읽어 보는 것이 좋아 보인다.

(그건 그렇고, tistory 스킨 편집을 저렇게 까지 하다니, 대단하신 분 같다.. 화면을 어떻게 실시간으로 보면서 개발하셨지..)

 

OAuth 2.0 — OAuth

OAuth 2.0 OAuth 2.0 is the industry-standard protocol for authorization. OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices. This

oauth.net

위는 Oauth 2.0에 대한 Document Page.

 

OAuth는 (Open Authorization) 인증을 위한 표준 방식이라고 생각하자.

현재 OAuth2.1이 개발 중이라고 한다.

 

OAuth 인증을 위한 구성 요소는 다음과 같다.

  • Resource Owner (사용자)
  • Client (Application) : 사용자의 권한을 사용하여 인증이 필요한 요청을 하는 클라이언트
  • Authorization Server : 클라이언트에 엑세트 토큰을 발급하는 서버
  • Reserce Server : 리소스를 호스팅 하는 서버. API 서버라고 생각하면 될 것 같다.

OAuth 인증을 위한 방식에는 다음과 같은 방식들이 있다.

  • Authorization code grant
  • Password grant
  • Implicit Grant
  • Client Credentials Grant
  • ...etc

여러가지 방식들이 존재하지만, 실제로 권고되는 방식은 Authorization code grant 방식이다.

Authorization code grant의 flow에 대해 설명 해 보려고 했는데, 아무리 해도 위 포스트인

OAuth 2.0 개념 - 그림으로 이해하기 쉽게 설명 

이 포스트 보다 잘 설명할 자신이 없어서.. 그냥 위 포스트를 확인하자.

정리가 너무 잘 되어 있는걸..

 

일단 third party 에 로그인을 해보자.

 

예제 코드로는 Laravel + Next.js를 사용하였다.

third party 로는 gitlab을 사용했다.

Laravel에서는 Sociallite라는 third party api에 쉽게 인증하도록 도와주는 아주 좋은 도구가 있다.

이게 싫다면, Next.js 에서 third party에 인가코드 발급 부분을 따로 작성해 주면 된다.

Laravel Socialite를 통해서 Access Token 을 발급 받아보자.

 

 

Laravel - The PHP Framework For Web Artisans

Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.

laravel.com

Sociallite에 관한 설정부분과 gitlab Application 등록은 각종 공식문서를 통해 진행하자.

여기서 redirect uri는 client의 uri로 작성한다.
(다른 third party api 이용시에도, application 등록을 진행해줘야 한다.)

php
public function socialLogin(Request $request) { // 인가코드가 없을 경우 인가코드 요청 if(!$request->has('code')) { return Socialite::driver('gitlab') ->scopes(['api', 'read_api']) ->redirect(); } else { // 추후 로그인 오류시에 대한 예외처리 더해줘야 함 $gitlabUser = Socialite::driver('gitlab')->stateless()->user(); return $gitlabUser; } }

위와 같이, socialLogin 함수를 만들고 라우팅 시켜준다.

web.php에 다음과 같이 작성한다.

php
Route::group(['middleware' => ['web']], function () { Route::prefix('gitlab')->group(function() { Route::get('/login', [AuthController::class, 'socialLogin']); }); });

 

이후, next.js 에서 login 버튼을 누르면 다음 함수가 실행되도록 하자.

javascript
export const getGitlabCode = () => { window.location.href = `${process.env.NEXT_PUBLIC_SERVER_URI}/gitlab/login?type=gitlab`; }

Next.js의 로그인 페이지의 코드 전문

javascript
'use client' import {getGitlabCode, getMe, gitlabLogin} from "@/app/api/user"; import {useRouter, useSearchParams} from "next/navigation"; import {useEffect, useState} from "react"; import {getCookie, getCookies, hasCookie, setCookie} from "cookies-next"; export default function Home() { const searchParam = useSearchParams() const router = useRouter() const gitlabLoginTest = () => { getGitlabCode() } useEffect(() => { if(searchParam.get('code')) { if(searchParam.get('type') === "gitlab") { gitlabLogin(searchParam).then(res => { if(res.token) { setCookie('access_token', res.token) window.location.href ='/dashboard' } else { alert('로그인에 실패하였습니다.') router.push('/') } }).catch(err => { alert('로그인에 실패하였습니다.') router.push('/') }) } } else { // login check getMe().then(res => { router.push('/dashboard') }).catch(err => { }) } }, []) return ( <main> <h1>React App</h1> <button onClick={gitlabLoginTest}>Login</button> </main> ) }

다음은 gitlabLogin의 통신 부분.

javascript
export const gitlabLogin = async (searchParam: ReadonlyURLSearchParams) => { try { const {data} = await axiosInstance.get('/gitlab/login', { params: { code: searchParam.get('code'), state: searchParam.get('state') }, headers: { 'Content-Type': 'application/json' } }) return data; } catch (err: any) { throw new Error(err) } }

Login 버튼을 누르면 gitlab의 로그인 페이지가 뜨게 될 것이고, 유저는 gitlab id와 패스워드를 입력하게 된다.

정상적으로 gitlab 계정과 비밀번호를 입력할 시 다시 client로 리디렉션 된다.

리디렉션된 client는 code값과 state 값을 발급받고 gitlabLogin 을 통해 통신하게 된다.

 

이 때, Laravel 에서는 gitlab에서의 access token을 발급받고 유저 정보를 반환해 주게 된다.

이제 API 서버인 Laravel 서버와 Client 둘 다 gitlab의 access token을 발급 받았으니 양쪽 모두 gitlab api를 사용 할 수 있는 상태가 되었다.

 

그런데 문제가 생겼다.

클라이언트에서 로그인 버튼을 눌러서 gitlab의 resource를 사용할 수 있게 되었는데,

정작 API 서버인 Laravel 서버의 resource에 접근하려면 API서버도 마찬가지로 인가코드를 발급 받아야 하기 때문에

사용자에게 API 서버의 resource에 접근하기 위해 한번 더 로그인을 시켜야 한다.

 

즉, 현재 상태에서는 API서버에서 연결된 DB에 저장된 값을 얻어 올 수 있는 방법이 없다.

 

gitlab 로그인만 해서 DB에 접근하고 싶어..

 

API 서버를 비밀번호 없이 인증시키고 회원가입 시키자.

 

그렇다면, gitlab 로그인 시(third party login)에 회원을 생성하고, API 서버의 인증도 거쳐야 한다.

또한 이미 기존에 생성된 회원이 있다면 회원 생성 절차는 패스하고 로그인 절차만 진행 해야 한다.

그런데 API서버의 인증을 Oauth 방식으로 구현하게 된다면 사용자는 API 서버에 대해 한번 더 인증 절차를 밟을 수 밖에 없다.

 

왜냐하면 여태까지 한 인증은 gitlab에 대한 인증이지, Laravel에 대한 인증이 아니기 때문에..

즉, Laravel Passport에서 지원하는 Oauth 형식의 인증을 사용하면 안된다.

SPA 형태의 어플리케이션에서 다음과 같은 인증 절차가 필연적으로 오기 때문일까? Laravel은 Sanctum 이라는 SPA용 인증 방식을 더 구현해 줬다.

 

아래는 Passport에 관한 문서.

 

Laravel - The PHP Framework For Web Artisans

Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.

laravel.com

 

아래는 Sanctum에 관한 문서.

 

Laravel - The PHP Framework For Web Artisans

Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing you to create without sweating the small things.

laravel.com

 

Laravel 쪽에서 Sanctum에 관하여 설정을 진행하고 나서 다음과 같이 코드를 개선한다.

Sanctum에 관한 설정은 공식 문서를 참고하여서 진행하도록 하자.

php
public function socialLogin(Request $request) { // 인가코드가 없을 경우 인가코드 요청 if(!$request->has('code')) { return Socialite::driver('gitlab') ->scopes(['api', 'read_api']) ->redirect(); } else { // 추후 로그인 오류시에 대한 예외처리 더해줘야 함 $gitlabUser = Socialite::driver('gitlab')->stateless()->user(); // 유저를 생성하거나, 이미 있으면 업데이트 한다. // 검색 조건은 gitlab_id 이지만, 이 과정을 이용하여서 다양한 계정을 하나의 계정으로 통합 할 수 있게 된다. // 기존 로그인한 유저의 정보가 있다면, 계정 통합 신청시 user_id 값을 직접 받아 각 third_party_id 값을 갱신하는 형태 $user = User::updateOrCreate([ 'gitlab_id' => $gitlabUser->id, ], [ 'name' => $gitlabUser->name, 'email' => $gitlabUser->email, 'gitlab_access_token' => $gitlabUser->token, 'gitlab_refresh_token' => $gitlabUser->refreshToken, ]); // sanctum Login 구현 Auth::login($user); // 보통 기기명을 적는다 create Token에 $token = $user->createToken('gitlab_token')->plainTextToken; return response()->json(['token'=>$token]); } }

 

자, 이제 gitlab 로그인시 유저정보를 반환하면서 패스워드 없이 api에 관한 access_token값을 발급 받았다.

Login Page의 코드 전문을 보면 res로 받은 token 값을 Cookie에 저장하는 부분이 있다.

 

해당 Cookie값을 이용하여서, Client는 Laravel 의 resource에도 접근이 가능하게 되었다.

api.php 에서 sanctum 인증을 통과해야지만 로그인한 유저 정보를 반환하게 해보고 테스트를 진행 해 보자.

php
Route::middleware('auth:sanctum')->group(function() { Route::get('/user', function (Request $request) { return $request->user(); }); });

 

Next.js 에서는 다음과 같은 통신을 구현하였다.

javascript
export const getMe = async () => { try { const {data} = await axiosInstanceWithToken.get('/user') return data } catch (err: any) { throw new Error(err) } }

axiosInstanceWithToken의 코드는 다음과 같다.

헤더에 인증 토큰값을 심어주고, Content-Type을 application/json으로 처리해 준다.

javascript
import axios from "axios"; import {getCookie} from "cookies-next"; const axiosInstanceWithToken = axios.create({ baseURL: `${process.env.NEXT_PUBLIC_API_SERVER_URI}`, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${getCookie('access_token')}` } })

쿠키값이 있는 상태라면, 로그인된 사용자 정보와 서드파티 api의 인증 정보도 User table에 같이 저장시켰으니 동봉되어서 클라이언트에서 확인 할 수 있다.

잘 가져와 진다~
이제 API 서버에게 가진거 다 내놓으라고 할 수 있다

정리

 

이제 위 코드에서 api server의 refresh token과 third party server의 refresh token에 대해서만 처리해주면 크게 문제가 되는 부분은 없다.

 

또한, socialite의 driver 부분을 변수로 처리하고 여러개의 third party에 대한 정보들을 users table에 추가하게 된다면 계정 통합도 굉장히 간단하게 처리 할 수 있는 상태가 되었다.

아마 이 시점에서는 각 third party의 client id, secret 값을 발급 받기 위한 과정이 더 시간이 오래 걸릴 듯 하다.

 

Web Application으로 개발을 진행할 때는 크게 문제가 되지 않았던 부분이지만 이번에 정리하면서 최대한 보안도 완벽하게 처리해 보기 위해 노력해 보았다.

 

의외로 이쪽으로 웹상에서도 정리된 문서가 굉장히 없어서 처음 진행하게 된다면 분명 많이 헤멜 만한 포인트 일 듯 하다.

 

덧붙히자면, 만약 서비스가 엄청 잘 되어서 Laravel API 서버에서 Oauth2.0 방식을 구현하여 access_token을 발급하여서 API를 공유해야 하는 상황이 온다면 passport를 세팅을 진행하고 

config/auth.php 의 guards 부분에 passport 인증을 관련 데이터를 세팅 해 주고

공유할 API 들만 따로 api.php 에서 auth:api 미들웨어를 두고 라우팅 값만 바꿔 주면 된다. (혹은 공유용 API를 따로 만들고 미들웨어 안쪽에서 라우팅 하던가)

 

API 사용 유저가 일반 유저와 분리된다면, provider 부분도 수정하고 API 사용 유저에 대한  Model, Controller 정도만 추가적으로 작성해 주면된다.

 

 

반응형