앱 등록 및 사용자 인증 구성

이 튜토리얼의 이전 섹션에서는 TypeScript 프로젝트를 설정하고, Express 서버를 추가했으며, 몇 가지 기본 라우팅을 구성했습니다. 이 섹션에서는 이전 섹션에서 만든 프로젝트를 기반으로 작업을 진행합니다. 아직 완료하지 않았다면 이 튜토리얼의 이전 부분을 먼저 완료할 것을 강력히 권장합니다.

Snyk 앱 생성 및 등록

지금까지 TypeScript 애플리케이션으로 좋은 성과를 거두었지만, 현재 상태는 단순한 TypeScript 애플리케이션일 뿐입니다. 이를 진정한 Snyk 앱으로 전환하려면 Snyk API를 사용하여 프로젝트를 새로운 앱으로 등록해야 합니다.

사전 준비 사항

  • API 권한이 있는 Snyk 계정

  • 앱 소유자로 등록될 Snyk 조직의 orgid

orgid 획득 방법

orgid를 검색하는 방법은 두 가지가 있습니다. 첫 번째는 Snyk 계정에 로그인하여 ID를 검색하려는 조직의 조직 설정 페이지를 방문하는 것입니다. 조직 설정 페이지의 경로는 다음과 같습니다.

https://app.snyk.io/org/{your-org-name}/manage/settings

대신, Authorization 헤더에 API 토큰을 포함하여 https://api.snyk.io/v1/orgs API 엔드포인트를 사용해 조직의 orgid를 검색할 수도 있습니다. 이 엔드포인트에 대한 자세한 내용은 참조 문서를 확인하십시오.

Snyk 앱과 Snyk API에 대하여

Snyk Apps는 앱을 설치하는 사용자의 유료 액세스 여부와 관계없이 API에 대한 최우선 액세스 권한(First-class access)을 가집니다. 이 기능을 활용하려면 앱 내에서 API에 액세스할 때 지원 종료된 https://snyk.io/api/ 대신 https://api.snyk.io/ 도메인을 사용하는 API 엔드포인트에 접근해야 합니다.

Snyk에 앱 등록

새로운 Snyk 앱 등록은 Snyk API에 대한 간단한 POST 요청을 통해 수행됩니다. 이 튜토리얼 전체에서 만들고 있는 앱이 요청을 수행하도록 구성할 수도 있지만, 대신 curl을 사용하여 직접 요청을 수행함으로써 단 한 번만 실행되는 함수를 생성하는 것을 피하도록 하겠습니다.

요청 본문에는 다음 세부 정보가 필요합니다.

  • name: Snyk 앱의 이름

  • redirectUris: 최종 사용자 인증 중에 허용되는 콜백 위치

  • scopes: Snyk 앱이 사용자에게 부여를 요청할 계정 권한

범위에 대한 참고 사항: 등록된 Snyk 앱의 범위는 현재 변경할 수 없습니다. 유일한 해결책은 앱 삭제 API 엔드포인트를 사용하여 Snyk 앱을 삭제하고 새로운 Snyk 앱으로 다시 등록하는 것입니다.

이 글을 작성하는 시점에서 Snyk Apps는 아직 베타 단계입니다. 현재 사용 가능한 범위는 apps:beta 하나뿐입니다. 이 범위는 앱이 기존 프로젝트를 테스트하고 모니터링할 수 있게 할 뿐만 아니라 Snyk 조직, 기존 프로젝트, 문제 및 보고서에 대한 정보를 읽을 수 있도록 허용합니다.

Snyk Apps 베타의 제한 사항 중 하나는 Snyk 앱이 등록된 조직에 대해 관리자 권한이 있는 사용자만 Snyk 앱을 승인할 수 있다는 것입니다.

API 토큰과 orgid를 준비하고 터미널에서 다음 명령을 수행하십시오. 필요에 따라 값을 대체하십시오. 이 튜토리얼에서는 redirectUris 값으로 http://localhost:3000/callback을 사용합니다.

circle-info

API 토큰 및 기타 비밀번호를 쉘에 직접 입력하는 대신, 파일에 export 문으로 추가하고 해당 파일을 로드(Source)하여 환경 변수로 설정함으로써 입력을 피할 수 있습니다.

Snyk의 응답에는 Snyk 앱 통합을 완료하는 데 필요한 두 가지 중요한 값인 clientIdclientSecret이 포함되어 있습니다. 이 값들을 안전한 곳에 저장하십시오. Snyk에서 clientSecret을 볼 수 있는 유일한 기회입니다. 주의사항으로, clientSecret을 절대 공개적으로 공유하지 마십시오. 이는 Snyk에서 앱을 인증하는 데 사용됩니다.

이제 앱을 Snyk 앱으로 등록했으므로, 사용자가 앱을 승인할 수 있도록 TypeScript 프로젝트를 조정하기 시작할 수 있습니다.

Snyk 앱을 사용한 사용자 인증

Snyk 앱에 대한 사용자 인증은 Snyk 앱의 데이터와 일치하는 쿼리 매개변수가 포함된 웹페이지 URL을 통해 수행됩니다. 이 URL의 쿼리 매개변수 값을 교체하고 웹 브라우저에서 사용자에게 최종 링크를 보내야 합니다. 사용자는 거기서 Snyk 앱에 대한 계정 액세스 권한을 부여할 수 있습니다.

액세스 권한이 부여된 후, 사용자는 http://localhost:3000/callback으로 정의한 앱의 등록된 callbackURL로 다시 돌아오게 됩니다.

기본적으로 앱은 다음과 같은 링크를 생성하고 승인이 필요할 때 사용자를 이 링크로 보내야 합니다.

쿼리 매개변수 중 일부는 명확할 수 있지만, 하나씩 살펴보겠습니다. 사용자를 위해 이 URL을 생성하도록 Snyk 앱을 수정할 것입니다.

  • redirect_uri: 선택적 값으로, Snyk에 앱 등록 섹션의 등록 명령과 함께 보낸 값 중 하나와 일치해야 합니다. 전달되지 않으면 Snyk 앱의 첫 번째 값이 사용되는 것으로 간주됩니다.

  • state: 이 /authorize 호출에서 redirect_uri의 콜백으로 앱 특정 상태(예: 사용자의 ID)를 전달하는 데 사용됩니다. CSRF 공격을 방지arrow-up-right하기 위해 콜백에서 이를 검증해야 합니다.

  • code_challenge: 코드 검증기(Code Verifier)의 SHA256 해시를 URL 안전한 base64로 인코딩한 문자열입니다. 코드 검증기는 /authorize를 호출하기 전에 앱 측에 저장된 고도로 무작위화된 문자열이며, 반환된 인증 코드를 토큰 쌍으로 교환할 때 전송됩니다. 이는 인증 코드 가로채기 공격을 방지하는 데 도움이 되는 PKCE(Proof Key for Code Exchange)의 일부입니다.

연결이 완료되면 사용자는 제공된 리디렉션 URI(이 경우 /callback 라우트)로 리디렉션되며, 다음 인증 단계를 위해 필요한 codestate 쿼리 문자열 매개변수가 추가됩니다.

다음 단계는 이전 단계의 응답에서 쿼리 매개변수로 받은 인증 코드를 사용하여 _액세스 토큰(Access Token)_으로 바꾸는 것입니다. 이를 위해 Snyk 앱은 https://api.snyk.io/oauth2/token 토큰 엔드포인트에 POST 요청을 보냅니다. 이 POST 요청은 인증 코드, client id, client secret, code_verifier 등을 포함한 특정 데이터가 요청 본문에 필요합니다.

성공하면 해당 POST 요청의 응답에는 Snyk 앱이 승인한 사용자를 대신하여 Snyk과 통신하는 데 필요한 모든 것, 즉 refresh_tokenaccess token이 포함됩니다.

액세스 토큰은 향후 API 호출에 사용되며 리프레시 토큰보다 만료 기간이 훨씬 짧습니다. 리프레시 토큰은 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 얻기 위해 단 한 번만 사용할 수 있습니다. 즉, 리프레시 토큰은 자체 만료 시간이 지나거나 액세스 토큰을 갱신하는 데 사용되면 더 이상 사용할 수 없습니다. 결과적으로 이는 Snyk 앱이 refresh_token 교환을 수행하기 위해 빈번하게 API 호출을 해야 함을 의미합니다.

circle-info

이 두 토큰은 저장하기 전에 반드시 암호화되어야 합니다.

사용자 인증 처리를 위한 Snyk 앱 업데이트

위의 정보에 따라 Snyk 앱에는 새로운 요구 사항이 생겼습니다. Snyk 앱으로 Snyk 사용자 계정을 성공적으로 인증하려면 TypeScript 앱 내에서 몇 가지 작업을 수행해야 합니다.

  1. API 요청 전송 및 응답 처리

  2. 토큰 만료와 같은 데이터 추적

  3. 비밀 데이터 암호화 및 복호화

  4. 인증 _코드_를 인증 _토큰_으로 변환

  5. 인증 토큰 갱신(Refresh)

  6. 오류 처리 및 사용자에게 인증 성공 또는 실패 알림

이후부터 Snyk 앱에서 많은 리팩토링을 수행하고 여러 파일을 오가게 될 것입니다. 과정을 더 쉽게 따라올 수 있도록, 이 튜토리얼에서는 코드 스니펫의 첫 번째 줄에 해당 코드가 속한 파일 경로를 주석으로 추가하는 관례를 사용합니다. 실제 코드에서는 이러한 주석이 필요하지 않습니다.

또한 새로운 요구 사항을 해결하기 위해 여러 개의 새로운 패키지를 추가할 것입니다. 편의를 위해 프로젝트 루트에서 다음 명령을 실행하십시오.

Snyk 앱에 데이터 및 구성 저장

애플리케이션 구성

애플리케이션 구성(예: 클라이언트 시크릿, API 토큰, 기타 구성 등)은 일반적으로 안전하게 저장되어야 하며 앱 자체의 외부에 보관되어야 합니다. 그러나 간결함을 위해 이 튜토리얼에서는 App.ts 파일에 내보낼 수 있는 상수로 구성 정보를 추가하고 실제 구현 세부 사항은 여러분의 몫으로 남겨둡니다. 이 값들은 Snyk 앱이 여러 곳에서 참조하는 값들입니다.

데이터 저장

수행하고 싶은 작업 중 하나는 Snyk 앱을 승인한 사용자에 대한 일부 정보를 캡처하는 것입니다. 다시 한 번 말씀드리지만, 실제 구현은 여러분의 몫입니다. 이 튜토리얼의 목적을 위해 오버헤드가 적은 소규모 로컬 JSON 데이터베이스인 훌륭한 lowdb를 사용할 것입니다.

먼저 ./db/lowdb 데이터베이스 파일을 초기화하고 App 생성자에서 이를 호출하도록 app.ts에 새로운 미들웨어 함수를 생성합니다.

데이터베이스 초기화가 처리되었으므로, 데이터베이스 항목을 읽고 쓰고 업데이트하는 것을 더 간단하게 만들기 위한 몇 가지 새로운 헬퍼 메서드를 생성하겠습니다. 이것은 TypeScript 프로젝트이므로 저장할 데이터 구조에 대한 인터페이스 또는 유형을 생성할 것입니다. 따라서 ./src/interfaces/DB.ts./src/util/DB.ts 두 파일을 생성합니다.

인터페이스 파일에 저장할 인증 데이터의 각 조각을 설명하는 인터페이스와 전체 데이터베이스에 적용할 수 있는 래핑 인터페이스를 채웁니다.

이 튜토리얼에서는 데이터베이스와 세 가지 기본 상호 작용(읽기, 쓰기, 업데이트)을 수행해야 합니다.

./src/util에 생성한 파일에 각각에 대한 함수를 생성합니다. 읽기 함수는 데이터베이스 내용이 포함된 Promise를 반환하고, 쓰기 함수는 방금 설명한 AuthData 인터페이스와 일치하는 객체를 받으며, 업데이트 함수는 항목을 다시 쓰려고 시도하고 성공 또는 실패를 나타내는 불리언(Boolean) 값을 반환합니다.

API 호출 준비

앞서 API 호출을 처리하기 위해 널리 쓰이는 axios 패키지를 설치했습니다. 동일한 API에 대해 반복적인 호출을 해야 한다는 것을 알고 있으므로, 코드를 프로젝트 전체에서 쉽게 재사용할 수 있도록 일부 헬퍼 함수를 추상화하겠습니다. util 디렉토리에 APIHelpers.ts 파일을 생성합니다.

내용을 채우기 전에, Snyk API를 일관되게 호출하더라도 Snyk API v1에서 Snyk REST API로의 마이그레이션 상태에 따라 여러 버전의 API에 대해 요청을 해야 할 수도 있다는 점에 유의하십시오. 이를 처리하는 한 가지 방법은 TypeScript Enum을 정의하고 함수 내에서 인수를 Enum의 가능한 값과 비교하여 필요한 쿼리 매개변수를 교체하는 것입니다.

새 파일에 다음 내용을 추가하거나, 원한다면 APIHelpers.ts에 추가하십시오. 나중에 사용할 수 있도록 export 해야 합니다.

앱이 Snyk API를 호출하는 것을 단순화하기 위해 단일 함수를 추가하는 것으로 시작합니다. 이 함수는 tokenType(bearer 또는 token), token 자체, 그리고 APIVersion(방금 정의한 Enum에 편리하게 대응됨)을 인수로 받습니다.

이 함수는 AxiosInstance이므로, 해당 객체에서 일반적으로 사용할 수 있는 .get(), .post() 또는 기타 메서드를 호출하여 API의 다양한 엔드포인트와 쉽게 통신할 수 있습니다.

Snyk Apps의 조직 ID를 검색하는 두 번째 비동기 함수를 정의하여 실제 작동 방식을 확인해 보겠습니다.

암호화 / 복호화 용이화

API에서 가져올 데이터를 암호화하는 것이 좋습니다. 이를 위한 작은 클래스를 정의하겠습니다. 클래스에는 두 개의 멤버가 있습니다.

  1. secret: 데이터를 암호화하는 데 사용되는 키

  2. cryptr: Cryptr 라이브러리의 인스턴스

Passport.js 및 Snyk-OAuth2 전략 구성

기반을 마련했으니 이제 본격적으로 작업을 시작할 때입니다.

이전 섹션에서 논의한 것처럼, 앱은 승인을 원하는 사용자를 특정 토큰 URL로 보내야 합니다. Snyk 앱에 /auth 라우트를 추가하고 Express에 인증 미들웨어를 추가하겠습니다. 이를 위해 훌륭한 passportjsarrow-up-right, passport-oauth2arrow-up-right 인증 전략과 Snyk의 @snyk/passport-snyk-oauth2arrow-up-right. passport와 그 관련 패키지들은 길고 복잡할 수 있는 인증 프로세스의 상당 부분을 처리해 줍니다.

passport는 캡슐화 철학을 중요하게 여기므로, 인증 프로세스의 나머지 모든 부분을 우리가 처리해야 합니다. 사용할 passport 전략의 인스턴스를 설정해야 합니다. 앞서 만든 데이터베이스 헬퍼를 여기서도 사용하여 승인이 성공하면 데이터베이스에 항목을 추가하겠습니다.

이 파일을 시간을 들여 살펴보고 수행하는 모든 작업을 이해하는 것이 중요합니다.

Express 미들웨어 업데이트

passport 전략이 구현되었으므로, 다음 코드 블록과 같이 passport 미들웨어를 설정하도록 app.ts를 수정합니다. 직접 호출하는 대신 initGlobalMiddlewares()라는 함수를 생성하여 몇 가지 다른 미들웨어를 동시에 설정하겠습니다.

  • express.json(): JSON 요청 처리를 위한 Express 미들웨어

  • express.urlencoded(): URL 인코딩된 호출을 처리하기 위한 Express 미들웨어

  • expressSession: passport에 의해 확장되는 express-session 미들웨어 패키지

  • setupPassport: passport 설정을 초기화하기 위함

인증 및 콜백 라우트 처리

인증 및 콜백 컨트롤러는 비교적 간단합니다. 두 개의 새로운 컨트롤러 파일을 생성합니다.

AuthController는 앞서 설명한 인증 흐름을 통해 앱의 인증을 처리합니다. 이는 passport 설정의 세 번째 단계입니다. 모든 컨트롤러 클래스는 경로와 라우터라는 두 개의 멤버를 가진 컨트롤러 인터페이스를 구현합니다.

이 컨트롤러는 사용자를 (passport를 통해) 승인 승인을 위해 Snyk 웹사이트로 보내는 데 사용할 /auth 라우트를 처리합니다.

사용자가 Snyk 웹사이트를 통해 Snyk 앱에 대한 승인을 완료하면, 사용자는 우리의 콜백 URI인 /callback으로 돌아오게 됩니다. 이 라우트를 유사하게 처리하여 passport를 다시 호출하겠습니다. 이것이 사용자 인증의 마지막 단계입니다.

CallbackController/callback에 대한 요청을 수락하지만, Snyk으로부터 받을 수 있는 다양한 결과를 처리하기 위해 /callback/success/callback/failure라는 두 개의 하위 라우트도 생성합니다.

완료하기 전에 index.ts에 새로운 컨트롤러에 대한 참조를 추가해야 합니다.

리프레시 토큰 관리

이 시점에서 Snyk 앱을 빌드하고 실행하면, /auth 라우트에 접속했을 때 성공적으로 Snyk 인증 포털로 이동하며 승인을 확인하면 로컬 앱의 콜백 라우트인 /callback으로 돌아오게 됩니다. 매우 단순한 일회성 사용 사례라면 여기서 끝낼 수 있습니다. 하지만 사용자 인증을 최신 상태로 유지하려면 해결해야 할 퍼즐 조각이 하나 더 남아 있습니다. 바로 토큰 만료입니다.

앱을 실행하여 테스트했다면 데이터베이스 항목을 살펴보십시오. 과정을 잘 따라왔다면 다음과 같은 내용을 볼 수 있을 것입니다.

해당 expires_in 값은 0이 될 때까지 계속 줄어듭니다. 0이 되면 사용자는 다시 인증해야 합니다.

액세스 토큰이 만료되는 것을 방지하려면 refresh_token을 사용하여 업데이트된 access_token을 얻기 위한 POST 요청을 해야 합니다. 리프레시 토큰 교환 설정을 참조하십시오.

Axios 인터셉터(Interceptors)arrow-up-right를 활용하여 요청을 가로채고 최신 access_token을 보유하고 있는지 확인함으로써 Snyk 앱에서 이 프로세스를 자동화할 수 있습니다.

./src/util/interceptors.ts 파일을 생성하고 상단에 필요한 모든 패키지, 클래스 등을 가져옵니다.

총 세 개의 인터셉터를 추가하겠습니다.

첫 번째인 refreshTokenReqInterceptorauth_token이 만료되었을 때 refresh_token을 사용하여 auth_token을 갱신합니다. 이 함수는 인터셉터에서 추가 확인을 위해 사용할 수 있는 AxiosRequestConfig 요청을 인수로 받습니다.

refreshTokenRespInterceptor는 요청 응답 중에 사용됩니다. 수신되는 응답이 401 Unauthorized인 경우에만 토큰을 갱신/재시도합니다. 이는 문제가 발생했을 때 Passport가 반환하는 값입니다.

마지막으로 refreshAndUpdateDb는 지정된 데이터베이스 레코드에 대해 액세스 토큰을 갱신하고 데이터베이스를 다시 업데이트한 후 새로 갱신된 토큰을 반환합니다.

인터셉터 정의가 완료되었으므로, callSnykApi 함수가 이를 사용하도록 업데이트하기만 하면 됩니다. 인터셉터는 axiosInstance 객체의 메서드이므로, axios.create() 호출 이후와 함수의 return 이전에 추가합니다.

마무리

여기까지 오셨다면 축하합니다! Snyk에 Snyk 앱을 등록하고, 인증 흐름을 구성하고, auth_token이 만료되지 않도록 유지하며, TypeScript를 사용한 훌륭한 시작점을 설정하는 방법을 배웠습니다.

이 튜토리얼의 다음 모듈에서는 템플릿 시스템을 추가하고 앱에서 사용자의 모든 Snyk 프로젝트를 보여주도록 앱을 구성해 보겠습니다.

Last updated