본문 바로가기

AWS/Cognito

Cognito ID 토큰 복호화

이전 포스트에서 Cognito로 부터 토큰을 받아오는 방법에 대해 설명했었습니다.

그리고 이렇게 받아온 토큰을 백앤드로 전송할 수 있었습니다.

이렇게 전송된 토큰은 express 서버라면 콜백 함수의 첫 번째 매개변수, AWS 람다 핸들러라면 event 매개변수headers.authorization 속성을 통해서 얻을 수 있습니다.

하지만 apollo-server(또는 apollo-server-lambda 또는 graphql-yoga) 모듈을 사용해서 GraphQL 서버를 운영할 때는 GraphQL 서버 모듈에 API 요청이 입력되기 전에 요청을 가로채는 방법을 사용해야 합니다. ApolloServer 생성자의 context 속성을 통해서 해당 작업을 수행할 수 있습니다.

handler.js

context는 기본적인 형태의 AWS 람다 핸들러와 같은 매개변수(event, context)를 입력받을 수 있는 함수입니다.

따라서 event.headers.authorization 속성에서 클라이언트로 부터 전송된 토큰을 추출할 수 있습니다.

decode-verify-jwt 모듈에 이 토큰을 입력하여 실행시키면 userName, 이메일, 토큰 유효성을 얻어올 수 있습니다.

decode-verify-jwt 모듈은 AWS Labs에서 제공하는 모듈입니다.

저는 이름을 verifier로 바꿔서 사용했습니다. 그리고 모듈 내부에 약간의 수정이 필요합니다.

'use strict';

const { ApolloServer } = require('apollo-server-lambda');
const { schema } = require('./src/schema');
const { prisma } = require('./src/context'); 
const { verifier } = require('./src/verifier');


const server = new ApolloServer({
  schema,
  context: async ({ event, context }) => {
    const authorization = event.headers.authorization || event.headers.Authorization;
    const {userName, isValid} = await verifier({token: authorization}); // 토큰 유효성 확인
    ... // 다음에 계속
  },
  playground: {
    endpoint: "/dev/apollo"
  }
});

exports.apollo = server.createHandler({
  cors: {
    origin: '*',
    methods: [
      'POST',
      'GET'
    ], 
    allowedHeaders: [
      'Content-Type',
      'Origin',
      'Accept'
    ]
  },
});

비교: 서버리스 GraphQL 백앤드 구축

verifier.js

타입스크립트 파일을 자바스크립트 파일로 변환했습니다. serverless-plugin-typescript를 사용할 수도 있었지만, 서버리스 프로젝트를 생성할 때 타입스크립트 설정을 따로 안해서 해당 파일만 자바스크립트로 변환 후 사용했습니다.

일단 변경해야할 부분은 주석으로 수정1, 수정2, 수정3으로 표시해 두었습니다. 나머지 부분은 신경쓸 필요 없습니다.

"use strict";

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.verifier = void 0;

var _util = require("util");

var Axios = _interopRequireWildcard(require("axios"));

var jsonwebtoken = _interopRequireWildcard(require("jsonwebtoken"));

function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }

var jwkToPem = require('jwk-to-pem');

var cognitoPoolId = 'COGNITO-POOL-ID'; // 수정1

if (!cognitoPoolId) {
  throw new Error('env var required for cognito pool');
}

// 수정2
var cognitoIssuer = "https://cognito-idp.ap-northeast-2.amazonaws.com/".concat(cognitoPoolId);
var cacheKeys;

var getPublicKeys = async function getPublicKeys() {
  if (!cacheKeys) {
    var url = "".concat(cognitoIssuer, "/.well-known/jwks.json");
    var publicKeys = await Axios.default.get(url);
    cacheKeys = publicKeys.data.keys.reduce(function (agg, current) {
      var pem = jwkToPem(current);
      agg[current.kid] = {
        instance: current,
        pem: pem
      };
      return agg;
    }, {});
    return cacheKeys;
  } else {
    return cacheKeys;
  }
};

var verifyPromised = (0, _util.promisify)(jsonwebtoken.verify.bind(jsonwebtoken));

var verifier = async function verifier(request) {
  var result;

  try {
    var token = request.token;
    var tokenSections = (token || '').split('.');

    if (tokenSections.length < 2) {
      throw new Error('requested token is invalid');
    }

    var headerJSON = Buffer.from(tokenSections[0], 'base64').toString('utf8');
    var header = JSON.parse(headerJSON);
    var keys = await getPublicKeys();
    var key = keys[header.kid];

    if (key === undefined) {
      throw new Error('claim made for unknown kid');
    }

    var claim = await verifyPromised(token, key.pem);
    var currentSeconds = Math.floor(new Date().valueOf() / 1000);

    if (currentSeconds > claim.exp || currentSeconds < claim.auth_time) {
      throw new Error('claim is expired or invalid');
    }

    if (claim.iss !== cognitoIssuer) {
      throw new Error('claim issuer is invalid');
    }
    
    if (claim.token_use !== 'access' && claim.token_use !== 'id') { // 수정3
      throw new Error('claim use is not access');
    }
    
    result = {
      userName: claim['cognito:username'],
      email: claim.email,
      isValid: true
    };
  } catch (error) {
    result = {
      userName: '',
      clientId: '',
      error: error,
      isValid: false
    };
  }

  return result;
};

exports.verifier = verifier;

수정1

일단 해당 모듈이 Cognito의 어떤 User Pool에 접속해야 하는지 알려주어야 합니다.

var cognitoPoolId = 'COGNITO-POOL-ID'; // 수정1

수정2

수정전 decode-verify-jwt 모듈은 us-east-1 리전 Cognito로만 접속합니다. 이것을 원하는 리전의 Cognito로 접속할 수 있도록 변경해 주어야 합니다. 전 ap-northeast-2(서울) 리전으로 변경했습니다. 

 

변경전

// 수정2
var cognitoIssuer = "https://cognito-idp.us-east-1.amazonaws.com/".concat(cognitoPoolId);

변경후

// 수정2
var cognitoIssuer = "https://cognito-idp.ap-northeast-2.amazonaws.com/".concat(cognitoPoolId);

수정3

수정전 decode-verify-jwt 모듈은 Access 토큰만 허용합니다.

처음부터 Access 토큰을 사용했으면 되는데, 저는 ID 토큰으로 사용했기 때문에 ID 토큰도 허용하도록 변경했습니다.

저는 Access 토큰은 권한 범위를 얻어올 때 사용하는 토큰인 줄 알았는데, 해당 모듈에선 어떻게 해서 access 토큰을 사용해 사용자 정보까지 가져오더군요.

참조: https://docs.aws.amazon.com/ko_kr/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html

 

그래도 ID 토큰으로도 작동합니다.

변경전

if (claim.token_use !== 'access') { // 수정2
   throw new Error('claim use is not access');
}

변경후

if (claim.token_use !== 'access' && claim.token_use !== 'id') { // 수정2
   throw new Error('claim use is not access');
}

결과

verifier 모듈은 userName, 이메일, 토큰 유효성 3개의 값을 반환합니다.

decode-verify-jwt 결과

[인스타그램 클론코딩] Cognito Pre-SignUp 람다 함수 포스트에서 사용자가 Cognito에 가입할 때 userName이메일 등의 사용자 정보를 그대로 데이터베이스에 복제했었습니다.

따라서 verifier 모듈에서 반환된 userName 또는 이메일을 사용해 데이터베이스의 사용자 정보에 접근할 수 있습니다.

 

'AWS > Cognito' 카테고리의 다른 글

Cognito Pre-SignUp 트리거  (0) 2020.05.13
Cognito 소셜 로그인 추가 (실전) - 구글  (0) 2020.05.13
Cognito 소셜 로그인 추가 (이론)  (0) 2020.05.13