Search
Duplicate
🚧

Passport-42 맨틀까지 뜯어보기

간단소개
Passport의 Strategy에 대해 알아보자!
팔만코딩경 컨트리뷰터
ContributorNotionAccount
주제 / 분류
JS
Javascript
라이브러리
Scrap
태그
token
토큰인증
잡지식
9 more properties

Passport.js

passport는 javascript에서 인증처리를 간단하게 해주는 라이브러리입니다. 인증은 다양한 방식으로 이루어지는데 요즘은 인증을 타 서비스로 위임하는 OAuth 방식을 많이 활용하곤 합니다. 저희가 많이 사용하는 구글로그인, 카카오로그인 등이 OAuth방식을 이용하는데요. 구글이나 카카오 등으로 인증 요청을 보내고 해당 인증서버에서 인증을 확인한 후 토큰을 발급받아 다시 사용자의 정보를 가져오는 방식입니다. 이 글의 목적이 로그인방식에 대해 다루는 것이 아닌만큼 이 부분에 대해서는 생략하겠습니다. 이처럼 여런 방법의 인증을 지원하기 위해 Passport는 Strategy라는 인터페이스를 제공합니다. 이 인터페이스의 규격에 맞춰 Strategy를 구현하고 passport에 넘겨주면 passport가 해당 방식에 맞춰 로그인을 처리해줍니다. OAuth2.0의 경우 passport-strategy라이브러리를 상속받은 passport-oauth2가 있고 이 모듈을 상속받아 각 서비스에 맞춰 개조한 passport-facebook, passport-steam 등의 Strategy가 있습니다.

Passport-42

구글이나 카카오처럼 42도 OAuth방식을 지원합니다. 그리고 누군가 만든 passport-42 라이브러리도 있습니다. 저는 사용해본적이 없습니다만, 우연히 슬랙에서 passport-42를 사용하는데 오류가 났다는 글을 발견했습니다. 하지만 질문하신 분께서 이후 같은 문제가 다시 발생하지 않아 어디서 문제가 발생했던 건지를 확인할 수가 없었습니다. 그래서 직접 passport-42를 사용해 간단한 어플리케이션을 만들어보고 코드를 뜯어보며 어디서 문제가 발생했을지를 살펴보았고, passport에 대해 좀 더 이해할 수 있게되어 정리해봤습니다. (근데 42 로그인은 실패함)

코드를 보자!

Passport-42

보통 저는 타입스크립트를 쓰다보니 vim에서 커맨드를 입력해 인터페이스나 타입이 어떻게 정의되었는지를 열어보는데, passport-42는 types가 정의되어있지 않아 직접 구현 코드를 열어보는 수 밖에 없었습니다.(이래서 동적타입 언어란..) passport-42는 다음과 같은 구조로 이루어져있습니다.
passport-42 ├── lib │ ├── index.js │ ├── profile.js │ └── strategy.js ├── test ├── package.json └── 기타 등등
Bash
복사
대부분의 passport strategy 라이브러리들은 이런식으로 lib폴더 안에 strategy.js파일이 있습니다. 파일을 열어보겠습니다.
// lib/strategy.js // Load modules. var OAuth2Strategy = require('passport-oauth2'); var util = require('util'); var Profile = require('./profile'); var InternalOAuthError = require('passport-oauth2').InternalOAuthError;
JavaScript
복사
맨 위에 varrequire를 쓰는 쉰내나는 import문이 반겨주네요. 보시는 것 처럼 passport-oauth2 Strategy를 불러와 사용하고 있습니다. 40줄 가량의 친절한 주석 밑에 Strategy가 정의되어있습니다.
function Strategy(options, verify) { options = options || {}; options.authorizationURL = options.authorizationURL || 'https://api.intra.42.fr/oauth/authorize'; options.tokenURL = options.tokenURL || 'https://api.intra.42.fr/oauth/token'; options.customHeaders = options.customHeaders || {}; if (!options.customHeaders['User-Agent']) { options.customHeaders['User-Agent'] = options.userAgent || 'passport-42'; } OAuth2Strategy.call(this, options, verify); this.name = '42'; this._profileURL = options.profileURL || 'https://api.intra.42.fr/v2/me'; this._profileFields = options.profileFields || null; this._oauth2.useAuthorizationHeaderforGET(true); } // Inherit from `OAuth2Strategy`. util.inherits(Strategy, OAuth2Strategy);
JavaScript
복사
option을 받아서 값이 없으면 42api URL을 기본값으로 넣어주고 call을 이용해 OAuth2Strategy를 상속받습니다. javascript의 this나 call, apply, bind 등의 개념에 익숙하지 않으시면 낯설게 느껴지실 수 있지만, 쉽게 말해 OAuth2Strategy라는 객체의 속성을 불러와 사용하겠다는 이야기로 보시면 됩니다. 그 아래부터는 this를 활용해 OAuth2Strategy의 속성을 가져와 변조하고 있습니다. 이것만 봐서는 어떻게 동작하는지 알기가 어렵습니다. require로 불러온 passport-oauth2 라이브러리를 살펴봐야겠습니다.
이름에서 볼 수 있듯 passport에서 OAuth2.0 방식의 로그인을 도와주는 Strategy입니다. 앞에서 처럼 lib/strategy.js파일을 열어보겠습니다.
// Load modules. var passport = require('passport-strategy') , url = require('url') , util = require('util') , utils = require('./utils') , OAuth2 = require('oauth').OAuth2 , NullStateStore = require('./state/null') , SessionStateStore = require('./state/session') , AuthorizationError = require('./errors/authorizationerror') , TokenError = require('./errors/tokenerror') , InternalOAuthError = require('./errors/internaloautherror');
JavaScript
복사
이 친구는 passport-strategy라이브러리를 가져와 사용하네요. OAuth로그인에는 oauth라는 라이브러리를 가져와 사용하고 있습니다. 아래로 내려가보면 이 passport의 Strategy객체를 불러오는걸 볼 수 있습니다.
function OAuth2Strategy(options, verify) { if (typeof options == 'function') { verify = options; options = undefined; } options = options || {}; if (!verify) { throw new TypeError('OAuth2Strategy requires a verify callback'); } if (!options.authorizationURL) { throw new TypeError('OAuth2Strategy requires a authorizationURL option'); } if (!options.tokenURL) { throw new TypeError('OAuth2Strategy requires a tokenURL option'); } if (!options.clientID) { throw new TypeError('OAuth2Strategy requires a clientID option'); } passport.Strategy.call(this); //...
JavaScript
복사
앞서 말씀드렸던 것 처럼, passport는 strategy를 인터페이스로 제공하고 있습니다. passport-strategy라이브러리의 strategy.js 파일을 열어보면 아래의 코드밖에 없습니다.
/** * Creates an instance of `Strategy`. * * @constructor * @api public */ function Strategy() { } /** * Authenticate request. * * This function must be overridden by subclasses. In abstract form, it always * throws an exception. * * @param {Object} req The request to authenticate. * @param {Object} [options] Strategy-specific options. * @api public */ Strategy.prototype.authenticate = function(req, options) { throw new Error('Strategy#authenticate must be overridden by subclass'); }; /** * Expose `Strategy`. */ module.exports = Strategy;
JavaScript
복사
JSDoc으로 기능을 설명하고 아무것도 구현돼있지 않습니다. 저도 이 코드를 보면서 js의 독특한 인터페이스 제공방식을 알 게 됐습니다. Strategy객체는 이처럼 authenticate라는 메소드를 공통적으로 갖고 있기 때문에 passport가 등록된 Strategy를 이용해 유저를 인증할 수 있습니다. 다시 passport-oauth2로 돌아가서 authenicate를 어떻게 구현했는지 살펴보겠습니다. passport 인증의 흐름을 보기 위해 임의로 코드를 요약했습니다.
OAuth2Strategy.prototype.authenticate = function(req, options) { options = options || {}; var self = this; //... if (req.query && req.query.code) { function loaded(err, ok, state) { //... self._oauth2.getOAuthAccessToken(code, params, function(err, accessToken, refreshToken, params) { //...
JavaScript
복사
req는 서버로 들어온 요청을 받는 인자입니다. 서버url/auth?code=인증코드와 같이 get요청이 들어오면 loaded 함수로 보냅니다. 이후 _oauth2라는 private 객체의 getOAuthAccessToken메소드에 인증코드를 담아 실행시킵니다. AccessToken을 받아오는 함수일텐데 이 _oauth2 객체는 무엇일까요? 다시 위로 조금 올라가 보면 쉽게 발견할 수 있습니다.
// ... this._oauth2 = new OAuth2(options.clientID, options.clientSecret, '', options.authorizationURL, options.tokenURL, options.customHeaders); // ...
JavaScript
복사
앞서 불러온 모듈 중에 oauth 모듈이 있었습니다. 입력받은 인자를 활용해 OAuth2객체를 생성해 활용합니다. AccessToken을 어떻게 가져오는지 보기 위해 oauth 라이브러리 코드를 봐야겠습니다.
깃허브 레포지토리 이름은 node-oauth입니다. lib폴더의 oauth2.js파일에 OAuth2함수가 정의돼있습니다. 182줄에 getOAuthAccessToken메소드의 코드가 있습니다.
exports.OAuth2.prototype.getOAuthAccessToken= function(code, params, callback) { var params= params || {}; //... this._request("POST", this._getAccessTokenUrl(), post_headers, post_data, null, function(error, data, response) { if( error ) callback(error); else { //... var access_token= results["access_token"]; var refresh_token= results["refresh_token"]; delete results["refresh_token"]; callback(null, access_token, refresh_token, results); // callback results =-= } }); }
JavaScript
복사
_request메소드로 토큰 url에 post요청을 보내 AccessToken과 RefreshToken을 받은 뒤 콜백함수로 전달해줍니다. 코드를 좀 더 살펴보면 _request메소드는 _executeRequest를 실행하고 _executeRequesthttps모듈을 사용해 네트워크 요청을 보냅니다.

프로필 가져오기

passport-42는 42유저의 프로필정보도 함께 가져오는데요, passport-oauth2라이브러리는 AccessToken을 받아온 뒤 _loadUserProfile함수를 실행해 유저의 프로필정보를 가져오도록 하는데 실제로 프로필을 가져오는 userProfile함수는 딱히 구현돼있지 않고 인터페이스만 제공하고 있습니다.
// passport-oauth2/lib/strategy.js OAuth2Strategy.prototype.userProfile = function(accessToken, done) { return done(null, {}); }; //... OAuth2Strategy.prototype._loadUserProfile = function(accessToken, done) { var self = this; function loadIt() { return self.userProfile(accessToken, done); } function skipIt() { return done(null); } //...
JavaScript
복사
구글이나 페이스북 등 passport로 각 서비스의 oauth2로그인을 지원하는 라이브러리들은 이 userProfile함수를 구현해 AccessToken을 받아옴과 동시에 profile도 같이 불러옵니다. 아래는 passport-42의 userProfile메소드입니다. Profile객체의 코드는 profile.js파일에 있습니다. 그냥 받아온 값을 파싱하는 함수이기 때문에 따로 다루지 않겠습니다.
// passport-42/lib/strategy.js Strategy.prototype.userProfile = function(accessToken, done) { var fields = this._profileFields; this._oauth2.get(this._profileURL, accessToken, function (err, body) { var json; if (err) { if (err.data) { try { json = JSON.parse(err.data); } catch (_) { // nothing } } if (json && json.message) { return done(new InternalOAuthError(json.message, err)); } return done(new InternalOAuthError('Failed to fetch user profile', err)); } try { json = JSON.parse(body); } catch (ex) { return done(new Error('Failed to parse user profile')); } var profile = Profile.parse(json, fields); profile.provider = '42'; profile._raw = body; profile._json = json; done(null, profile); }); };
JavaScript
복사

결론

passport-42라이브러리도 써보고 passport-oauth2라이브러리도 써보면서 42로그인을 시도해봤는데, 네트워크 오류가 발생하며 계속해서 실패했습니다. 한참 헤매다가 passport-oauth2라이브러리를 사용해 카카오 로그인을 시도해봤는데 바로 성공해서 좀 당황스러웠습니다.
질문하신 분께서 겪었던 문제는 profile을 받아와 이메일을 사용해 데이터베이스 조회를 하는데 이메일이 null값이 들어온다는 문제였습니다. profile값을 따로 validate하지 않고 바로 profile.email로 값을 조회했음에도 에러가 발생하지 않았다는 건 profile은 값이 들어왔다는 것으로 판단해 passport-42라이브러리 문제라고 생각했는데, 코드상에 큰 문제는 없어보입니다.
다른 strategy 라이브러리들도 찾아봤는데 대체로 만들어진지 7년 정도 됐고, 이후로 큰 업데이트는 없는 것 같습니다. 더불어 strategy들이 typescript를 지원하지 않아 별도로 @types 라이브러리를 설치해줘야하는데, passport-42는 그마저도 없어 불편했습니다. 제가 쓰게된다면 passport-oauth2를 활용해 구현할 것 같습니다.
이번에 passport를 뜯어보면서 js가 어떻게 인터페이스를 제공하고, prototype을 사용해 객체를 사용하는지, 무엇보다 콜백지옥이 뭔지 어렴풋이 알 수 있었습니다.