본문 바로가기
Projects,Activity/Spotify

Spotify API와 자체 DB를 통합한 이중 JWT 인증 시스템

by 그냥하는거지뭐~ 2024. 11. 6.
목차
- 다양한 JWT 전략
- 고민 1: Spotify Login page를 띄우기까지의 과정 
- 고민 2: Next.js의 Route Handler
- 고민 3: 이중 JWT 시스템
- 완성된 Flow 
- 마치면서 

다양한 JWT 전략

지금까지 JWT, OAuth를 이용한 프로젝트를 4-5개 정도 진행하면서, 단 한 번도 동일한 시나리오를 경험한 적이 없다. 각 프로젝트마다 요구사항이 다르다 보니, JWT 전략 역시 매번 새로운 접근이 필요했다. 지금까지 경험한 상황을 간단히 정리해 보면 다음과 같다.

 

1. Third-party 서버에서 유저 정보만 받아온 뒤, 자체 토큰을 발급한다. (TaskStock, Polabo)

초기 인증을 위해 외부 서버로부터 유저 정보를 가져온 후, 자체 서버에서 별도의 JWT를 발급해 관리하는 방식이다. 이를 통해 외부 의존성을 줄이고, 이후 요청은 자체 서버에서 처리할 수 있다.

 

2. 로그인 상태를 지속적으로 유지해야 하는 경우 백그라운드 로그인을 적용하거나, Refresh Token의 유효기간을 늘려준다.  (TaskStock)

유저의 로그인 상태를 오래 유지해야 할 경우, 로그인 시 필요한 정보를 저장해 백그라운드 로그인이나 Refresh Token의 유효기간을 길게 설정하여 끊김 없는 인증 상태를 유지한다. 이를 통해 UX를 개선할 수 있으나, 보안 측면에서 신중하게 관리해야 한다.

https://hwanheejung.tistory.com/15

 

3. 다중 기기 지원이 필요한 경우 각 기기별로 Refresh Token을 발급한다. (TaskStock)

동일한 계정이 여러 기기에서 사용될 수 있는 경우, 각 기기별로 Refresh Token을 관리하여 특정 기기에서 로그아웃하거나 토큰을 무효화할 수 있는 기능을 제공한다. 이를 통해 보안성과 유연성을 동시에 확보할 수 있다.

https://hwanheejung.tistory.com/15

 

4. 웹소켓 업그레이드 시의 인증은 Secondary Token을 사용한다. (DizzyCode)

웹소켓 업그레이드 시 커스텀 헤더를 사용할 수 없는 문제 때문에, 유효기간이 짧은 secondary token을 query parameter로 전달한다. 

https://hwanheejung.tistory.com/43

 

5. Next Auth를 사용하는 경우 프론트가 인증 주도권을 가지게 된다. (Polabo)

백이 사용자 정보를 받는 기존 OAuth 로직과 달리, 프론트가 사용자 정보를 받기까지의 모든 과정을 담당한다. 

 

 

이를 통해 느낀 점은, JWT와 OAuth는 단순한 인증 수단이 아니라 앱의 구조와 요구사항에 따라 매우 유연하게 설계할 수 있는 도구라는 것이다. 특히 보안성, 사용자 경험, 데이터 보호 수준 등을 고려해서 세밀하게 시나리오를 설계할 수 있다.

내가 이번에 만드려고 하는 애플리케이션에서도 또 다른 새로운 전략이 필요하다. Third-party 서버에서 유저 정보를 받아오는 게 끝이 아니라, Spotify에서 제공하는 api를 계속해서 사용해야 하면서, 자체 백엔드 서버 api도 존재한다. 


c.f) 지금부터 소개할 고민들은 다음 섹션인 완성된 Flow를 먼저 읽고 오면 이해가 더 잘될 것 같다. 

고민 1: Spotify Login page를 띄우기까지의 과정 

CASE 1) 클라이언트가 client_id를 직접 사용하여 Authorization URL로 바로 리디렉트하는 방법 
CASE 2) 백엔드에서 Authorization URL을 생성하고 클라이언트로 전달하는 방법

 

CASE 1) 클라이언트가 client_id를 직접 사용하여 Authorization URL로 바로 리디렉트하는 방법 

이 방식은 백엔드를 거치지 않고 클라이언트에서 직접 리디렉트하기 때문에 구현이 간단하다. 백엔드 API 호출이 필요 없어 클라이언트 측에서 바로 처리할 수 있다. 하지만 client_id와 같은 정보가 클라이언트 코드에 노출될 수 있다는 점과, 배포까지 생각했을 때 설정에 변경이 생기면 클라이언트 코드를 수정하고 재배포해야 하므로 번거로울 수 있다. 

 

CASE 2) 백엔드에서 Authorization URL을 생성하고 클라이언트로 전달하는 방법 ✅

이 방식은 민감한 OAuth 설정이 백엔드에만 저장되기 때문에 좀더 안전하다. 또한 이 설정들을 백엔드에서만 관리하기 때문에 여러 클라이언트(ex. 웹, 앱)가 동일한 백엔드 API를 통해 인증 절차를 공유하기 때문에 중앙 집중화된 설정 관리가 가능해진다. 만약에 웹뿐만 아니라 앱도 만들었을 때를 가정했을 때 만약 설정이 변경된다면 백엔드 코드만 수정하면 모든 클라이언트에 적용되기 때문에 유지보수하기가 용이하다. 

 

=> 보안성, 유지보수성, 확장성을 고려해서 CASE 2로 진행했다. 

// Node.js

// authController.ts
export const requestSpotifyAuthUrl = (req: Request, res: Response) => {
  const url = getSpotifyAuthUrl();
  res.json({ url });
};

// spotifyService.ts
export const getSpotifyAuthUrl = (): string => {
  const scope = "user-read-private user-read-email";
  const authUrl = new URL("https://accounts.spotify.com/authorize");

  const config = {
    response_type: "code",
    client_id: process.env.SPOTIFY_CLIENT_ID!,
    scope,
    redirect_uri: process.env.SPOTIFY_REDIRECT_URI!,
  };

  authUrl.search = new URLSearchParams(config).toString();
  return authUrl.toString();
};

고민 2: Next.js의 Route Handler 

https://nextjs.org/docs/app/building-your-application/routing/route-handlers

 

Routing: Route Handlers | Next.js

Create custom request handlers for a given route using the Web's Request and Response APIs.

nextjs.org

 

저번에 Next Auth를 사용해보면서 접한 특이한 파일이 하나 있다. app/api/auth/[... nextauth]/route.ts인데, Next에서 제공하는 Route Handler이다. 한마디로 서버 사이드에서 API 엔드포인트를 쉽게 생성할 수 있게 해주는 기능이다. app 디렉토리 내에서 route.ts 파일을 생성해서 사용할 수 있다. 

Polabo 폴더 (Next Auth)

 

Spotify 인증 흐름에서 route handler의 역할은 다음과 같다. 

- Redirect URI: Spotify for Developers에서 설정한 redirect URI가 바로 이 Route Handler의 경로이다.

- Authorization Code 수신: 유저가 Spotify에 로그인하면, 유저는 이 URI로 리다이렉트되며, Authorization Code가 쿼리 파라미터로 전달된다. 

- 받은 인증 코드를 사용해서 Spotify API에 access token을 요청하게 되는데, 이 부분은 백엔드가 담당하므로 프론트는 백에 Authorization code를 전달한다. 

- 인증이 완료되면 메인 페이지로 리다이렉트한다. 

// api/auth/spotify/route.ts
import { login } from '@/lib/api/auth'
import { NextResponse, type NextRequest } from 'next/server'

export async function GET(request: NextRequest) {
  const { searchParams } = request.nextUrl
  const code = searchParams.get('code') as string

  if (!code)
    return NextResponse.json({ error: 'No code provided' }, { status: 400 })

  try {
    await login(code)

    return NextResponse.redirect(new URL('/', request.url))
  } catch (error) {
    console.error('Login error:', error)
    return NextResponse.json({ error: 'Login failed' }, { status: 500 })
  }
}

 


고민 3: 이중 JWT 시스템

이번 프로젝트에서는 이전부터 사용해보고 싶었던 GraphQL을 도입하게 되었다. Spotify API가 제공하는 정보가 필요 이상으로 많아 overfetching 문제가 발생할 수 있기 때문에, 프론트에서 실제로 필요한 데이터만 선별하여 효과적으로 전달하는 방식이 필요했다. 방대한 Spotify API의 응답을 백엔드에서 가공하여 필요한 부분만 프론트로 보내는 구조를 채택함으로써, 프론트는 Spotify API와 직접 통신할 필요가 없어졌다. 즉, 프론트는 Spotify가 제공한 Access Token(SAT)과 Refresh Token(SRT)을 사용할 일이 없으며, Spotify API와의 통신은 백엔드에서만 이루어진다.

 

Spotify API와의 통신을 백엔드가 처리하게 되면서, 두 가지 종류의 토큰을 별도로 관리하는 방식을 채택했다. Spotify Access Token(SAT)과 Refresh Token(SRT)은 백엔드와 Spotify API 간의 통신에서만 사용하고, 백엔드와 프론트엔드 간의 통신에서는 자체적으로 발급한 JWT 토큰(AT)을 사용한다. 이로 인해 클라이언트는 Spotify의 SAT에 접근할 필요 없이 필요한 기능을 모두 이용할 수 있다. 이로 인한 장점을 정리해 보면 다음과 같다. 

- 명확한 책임 분리
- 보안 강화
- Third-Party 서비스(Spotify) 의존성 감소
- 유연한 유저 관리
- 확장성

 

 


완성된 Flow

 

1. [User] 로그인 버튼 클릭

2. [Client > Server] Spotify Authorization URL 요청

3. [Server > Client] 서버는 https://accounts.spotify.com/authorize 에 필요한 client_id, redirect_uri, scope, response_type=code 등의 config를 search parameter로 달아서 프론트에 전달

4. [Client] 해당 URL로 유저를 redirect

5. [User > Spotify] 유저가 Spotify 로그인 페이지에서 권한 부여

6. [Spotify > Client] Spotify for Developers 사이트에 등록한 redirect uri에 Authorization Code를 포함해 클라이언트로 redirect

7. [Client > Server] 클라이언트는 Authorization Code를 서버에 전달

8. [Server > Spotify] Authorization Code를 이용해 SAT, SRT 요청 (POST https://accounts.spotify.com/api/token)

9. [Spotify > Server] access token(SAT), refresh token(SRT) 전송

10. [Server > Spotify] SAT로 유저 정보 요청

11. [Spotify > Server] 유저 정보 전송

12. [Server > DB] 서버는 유저의 이메일을 기준으로 신규 유저인지 기존 유저인지 DB에서 확인

13. [DB] 해당 이메일이 존재하면 true, 존재하지 않으면 false로 응답

14-1. [Server > DB] 신규 유저이면 email, SAT, SRT, AT, RT, atExpiry를 저장한다.

14-2. [Server > DB] 기존 유저이면 SAT, SRT, AT, RT, atExpiry를 갱신한다. 

15. [Server > Client] 서버는 클라이언트가 필요한 데이터를 가공해서 전달, AT, RT도 전달한다.  


마치면서

처음 JWT와 OAuth를 공부하고 적용할 때는 하나의 정답만이 존재한다고 생각했다. 하지만 앱의 요구사항에 따라 세부적인 시나리오를 정말 다양하게 설계할 수 있다는 점을 점점 더 깨닫게 되었다. 이번 프로젝트에서 두 가지 토큰을 사용하는 것도 그냥 내가 생각한 거라 이게 정답이 아닐 수도 있고 더 좋은 방법이 있을 수도 있다. 앞으로 다양한 상황들을 경험해 보면서 최적의 시나리오를 설계하기 위해 다양한 케이스를 접해보고 싶다. 

 

또한 지금까지는 프론트 코드에만 집중했고 제대로 된 백엔드 코드를 작성해 본 적이 많지 않은데, 이번 토이 프로젝트를 통해 프론트, 서버, Third party 서버, DB 간의 전체적인 흐름을 자세하게 이해하고, 각 부분이 어떻게 연결되는지 구체적으로 경험해볼 수 있었다. 앞으로 구현할 GraphQL도 기대가 된다. 

 

 

spotify가고싶다