이전 포스트에서 Cognito로 부터 토큰을 받아오는 방법에 대해 설명했었습니다.
- Amplify 초기화
- Cognito 소셜 로그인 추가(이론)
- 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'
]
},
});
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 토큰을 사용해 사용자 정보까지 가져오더군요.
그래도 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개의 값을 반환합니다.
[인스타그램 클론코딩] 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 |