Search
▪️

Adding Authentication

What is Authentication?

Application을 이용하는 사용자가 있고, 이 사용자는 Views와 상호작용한다. 이에 따라서 데이터베이스와 서버를 통해서 작업들을 처리한다.
Authentication이라고 함은 Application의 Role에 따라서 사용자들을 분류하고, 이에 맞는 권한을 주어 권한에 따른 작업들을 할 수 있도록 하는 것이다.

How is Authentication Implemented?

사용자가 우선 Sign Up이 되어 있다는 가정 하에, 사용자는 Server에 Login Request를 보내게 된다. 이 때 Server는 Login Request의 정보를 통해서 데이터베이스에 해당 사용자가 있는지 검증을 하게 된다. 만일 사용자가 있다면 사용자의 인증된 정보들을 Session에 담게 된다. (만일 Session에 유지 하지 않으면, 인증된 정보들이 유지되지 않으므로 Logout이 되어서 매 Request마다 인증을 다시 해야 한다. )
성공적으로 Session을 담아내면 Server는 사용자에게 200 Status Code의 Response를 보내게 된다. 사용자는 성공적으로 Response를 받으면, 이 암호화 되어 인증된 정보들을 Cookie에 담게 된다. (Cookie에는 Session의 ID가 저장되어 있고, Session내의 정보가 필요할 때 이 ID 값을 통해서 서버로부터 해당 정보를 받는 것이다.)
위 괄호 내용에 대해서 더 자세히 언급하자면, 사용자가 Cookie까지 저장하게 되었다면, 사용자의 모든 Request마다 Cookie의 정보들이 같이 전송되게 된다. Server가 Request를 받게 되면 사용자의 Cookie를 Session에 연결하게 되고 Session의 정보들을 가져와서 활용할 수 있는 것이다. 따라서 Cookie와 Session을 통해서 사용자는 만일 권한이 있다면, Restricted Resource에 대해서 접근할 수 있게 되는 것이다.
** 즉, Authenticated인 사용자에게만 보여야하는 View와 Route가 Direct한 URL을 통해서 접근할 수 없도록 처리도 해줘야 한다.

Encrypting Passwords

회원가입 후, Password에 대한 정보를 Raw하게 저장해서는 안 된다. 데이터베이스에서 해당 정보를 바로 확인이 가능하기 때문에, 암호화 하는 것이 중요하다.
Password들에 대해서 일반적으로 암호화는 Hash를 통해서 진행된다. Hash의 특징 상, Reversible 하지 않기 때문이다. (Hash는 암호화된 Value로부터 Plain Value값을 도출 할 수 없는 1방향 함수이기 때문이다.)
Node.js에는 Hash Function을 지원하는 'bcryptjs'라는 Package가 있다. 아래 명령어로 설치하고 Import할 수 있다.
npm install —save bcryptjs
const bcrypt = require('bcryptjs')
Package 설치가 끝났다면, 회원가입이나 로그인과 같은 Auth를 담당하는 Controller에서 해당 Package를 사용하면 된다.
Password를 데이터베이스로 넘기기 전에, Password를 bcrypt의 hash() Method로 암호화 하여 데이터베이스에 저장한다. 해당 Method는 String을 인자로 받아 Hash값을 취하게 된다.
String Value외에도 한 가지 인자를 더 받는데, 이는 Salt값이다. Salt 값을 지정한다라고 함은, Hashing을 몇 회를 수행할지 Round를 지정하는 것이다. Round가 높아질수록 소요되는 시간은 늘어나지만 보안성은 올라가게 된다. (일반적으로 12회 정도면 꽤 높은 보안성을 보인다.)
이 때 주의해야 할 점은 hash() Method를 수행하는 것 자체가 비동기 작업이므로 이에 대해서 Promise를 Return하게 된다는 것이다. Promise에 대한 처리를 별도로 해줘야 한다.
** Hash Function의 특성 상, 주어진 동일한 Hash 알고리즘에 같은 값을 취하게 되면 같은 Hash 값을 갖게 된다. 따라서 Password에 대한 검증 자체도 Hash 값 간의 비교로 이어질 뿐, Plain Password 값의 비교를 통해서 검증을 하진 않는다. 이렇게 했을 때 Plain Password값을 보이지 않고도 검증을 할 수 있을 뿐 아니라, 동일한 Hash 알고리즘에 대해서 Password를 다르게 치면 같은 Hash 값이 나오지 않으므로 안전한 검증을 할 수 있게 된다.

Adding the Signin Functionality

사용자에 추가에 대한 작업에서도 Dummy를 쓰다가 Generalize한 것처럼, 사용자의 로그인 및 Session 저장에도 Login한 사용자를 기준으로 Generalize 해줘야 한다.
즉 Dummy로 부터 단순히 사용자를 받는 것이 아니라, Input Form에서 계정에 대한 입력 정보를 받아와 해당 정보들을 데이터베이스와 비교하여 사용자를 찾아야하는 것이다.
Encrypting Passwords에서 Password 비교를 Plain Password가 아닌 Hash 된 값을 통해서 비교한다고 했는데, 이에 대해서 bcryptjs에서는 compare()라는 특별한 Method를 제공한다.
동일한 Hash 알고리즘에 대해서 동일한 값을 제공 시 동일한 Hash 값을 갖기 때문에, 데이터베이스 Password에 기록된 Hash값이 어떤 알고리즘에 의해서 Hash된 것인지 알아야 한다. 이런 부분에 대해서 compare() Method는 Hash된 값이 사용한 알고리즘을 알 수 있고, 해당 알고리즘으로 주어진 String 인자를 Hash취하여 비교하고자 하는 두 대상의 Hash값이 동일한지 알 수 있다. (따라서 단순히 hash() Method로 만들어서 두 값을 비교하면 다른 알고리즘을 사용한 것이므로 다른 값이 나오기 때문에 compare() Method가 절대적으로 필요하다.)
compare() Method는 첫 번째 인자는 Plain Texet, 두 번째 인자는 Hash Value를 인자로 갖는다. Return 값에 맞춰서 Session 저장 로직을 구현하면 된다.

Working on Route Protection

작업하고 있는 것과 같이, Authentication을 얻은 사용자들만 접근 가능한 View와 Route들이 있는 와중에, 임의로 Direct하게 URL을 통해서 해당 View와 Route로 접근 가능한 일은 없어야 한다.
이에 대해서 처리를 따로 해줘야 하며, Route시에 Authentication을 갖는 사용자인지 판별해야 한다. 이를 Routing Protection이라 한다.
이는 Session에 로그인 정보를 보유하고 있지 않으면 다른 Page로 Routing 시키면 된다.
위와 같이 처리를 할 수도 있지만, 보호하고자 하는 매 Route마다 반복되는 코드로 Route Protection을 구현하는 것은 Scalability 면에서 유리하지 않다. 따라서 다른 방법이 요구된다.
Route Protection을 수행하는 또 다른 방법은 다음과 같다. 모든 Route들이 Protection 될 수 있도록 모든 Route들이 거치는 Middleware를 두는 것이다. (프로젝트 구조는 Root에 middleware라는 디렉토리를 두는 것이 일반적이다.) 이 Middleware 안에는 이전에 일일이 작성해둔 코드가 들어가게 된다. 따라서 Route Protection 할 것인지에 대한 코드는 Middleware 한 군데만 들어가게 되고, 해당 Middleware는 Route Protection을 수행해야 하는 Router에 포함 시키면 되는 것이다.

Understanding CSRF Attacks

CSRF라고 함은 Cross-Site Request Forgery의 약자이고, 이는 사용자의 Session에 대한 정보를 오용 및 남용하여 사용자가 이용하고 있는 Application을 통해서 Malicious Code를 실행하게 하는 것이다.
CSRF Attack의 예를 들면 다음 상황과 같다. 사용자는 Frontend인 View를 통해 Backend인 Server와 상호작용 하고 있으며 Server는 Session에 대한 정보를 Client는 Cookie에 대한 정보를 갖고 있다고 하자. 사용자는 다른 사용자에게 돈을 보내려고 Server에 Request를 넣은 상황이라고 한다.
이 때 사용자가 접속해 있는 사이트가 Original Service처럼 생겼지만 실제로는 SPAM으로 받은 위조된 사이트에 접속한 것이라고 가정해보자.
Malicious Code가 들어가 있지 않은 원래의 상황이라면, 사용자는 다른 사용자 A에게 돈을 보내라고 Request를 넣었을 때, Server는 정상적으로 A에게 돈을 보내게 될 것이다.
하지만 위조된 사이트에서 Maliciouse Code가 들어가 있다고 하면, 사용자는 A에게 돈을 보내라고 Request를 보냈지만 Malicious Code에 의해 원래 보냈던 Request은 사라지고 다른 사용자 B에게 돈을 보내는 Request가 Server에 전달되는 것이다. (사용자가 정당한 Auth를 갖고 있기 때문에 위조된 Request임에도 Server는 Accept를 하게 된다.)
즉, 이와 같은 공격은 Original Service를 기반으로 하고 있기 때문에 Authenticaation 정보를 Session과 Cookie로 잘 갖고 있다고 하더라도 발생할 수 있는 것이다.
이를 막으려면, Original Service의 View만으로 작업하고 있을 때만 Session에 대한 정보를 활용할 수 있게 해야한다. 즉, 다른 View가 끼어들었을 때는 Session에 대한 정보를 활용할 수 없는 것이다. (따라서 Fake Page에서는 Session의 정보 이용이 불가능하다는 것)
그렇다면, 보이는 View가 Original Service인지 그리고 문제는 없는 View인지 판별하려면 어떻게 해야하는가? → CSRF Token을 두는 것이다.

Using a CSRF Token & Adding CSRF Protection

CSRF Token을 이용하기 위해서 Third Party Package의 설치가 필요하고, 아래의 명령어를 통해서 수행할 수 있다.
npm install —save csurf
일반적으로 Token이라고 하는 String은 매 Request마다 우리가 사용하는 Form에 내재시킬 수 있다. 이렇게 Form에 내재시킨 String들을 통해서 Backend에서 사용자의 State를 변화시키는 작업을 처리하도록 한다.
즉, Token을 View에 포함시키고 View에서 Server로 Request를 보낼 때, Server에서는 Request를 날린 View가 Valid한 Token을 갖고 있는지 확인 후 작업을 수행하게 된다. 만일 위조된 View라면 Valid한 Token을 갖지 않으므로 해당 View로부터의 Request는 폐기한다. 따라서 위조된 View로부터 Session을 활요하게 되는 경우는 없어지는 것이다. (Token의 Validation은 Random Hash Value가 보장해준다. 심지어 Valid한 Token의 경우 View의 매 Rendering이 되고 나면, 새로운 Token을 내재시킨다.)
아래와 같이 Third Party Package를 Import하여 상수에 담는다.
const csurf = require('csurf')
상수에 담았다면, 아래와 같이 해당 상수를 Initialize하여 상수에 담는다. Initialize시 인자를 안 주어도 이용 가능하고 별도의 Object 형태의 인자를 주어 Configuration도 할 수 있다. (Hash될 Token을 할당할 때 쓰는 Secret값은 기본 옵션으로 Session에 담기지만, 이를 Cookie에 담게할 수 도 있다.)
const csurfProtection = csurf()
해당 상수에 대해서는 app.use() Middleware를 통해서 실행한다.
일단 Middleware가 실행되면, 어떤 것이든 Non-Get Request에 대해서 csurf Package는 View에 Valid한 Token이 있는지 확인하게 된다. 따라서 기존 View들이 Valid한 Token을 가질 수 있도록 처리해야 한다.
View가 Valid Token 값을 가질 수 있도록 데이터를 View에 밀어 넣어야 한다. Post 요청 시에 Token에 대한 검증을 하게 되므로 그 전에 Token 값을 갖게 해야할 뿐 아니라, View가 데이터를 갖게 하는 부분이 Render 할 때이므로 Page를 Get 요청을 보낼 때 Token을 밀어 넣는다.
render() Method에서 Key와 Value Pair를 매칭 시켜서 데이터를 View로 밀어 넣을 때, Token은 request.csrfToken()을 통해서 설정이 가능하다. csrfToken() Method가 가능한 이유는, Middleware에서 csurfProtection을 use했기 때문이다. csrfToken() Method를 통해서 Token 생성이 가능하고 이 Token을 View로 밀어 넣을 수 있는 것이다.
Package Import도 하고, Package를 Initialized도 했고, Initialize된 Package를 Middleware에서 설정도 했고, Get 요청으로 Render시 View에 Token 값도 갖게 했다. 마지막으로 Post를 보내게 될 때, 해당 Token값을 hidden 타입의 input으로 보내게 되면 자동으로 View의 Validation이 검증된다. (단, Package가 View의 Validation을 위해 Token을 인식하기 위해서는, hidden input의 name을 반드시 '_csrf'로 해야만 설정한 Token을 인식할 수 있다.)
매 Render마다 isLoggedIn과 csrfToken을 일일이 render() Method에 넣는 것 역시 Scalability에 좋지 않다.
따라서 Express.js에게 매 render() Method 호출마다 반드시 포함해야 하는 데이터가 있다고 알려야 한다.
이 역시도 app.js에서 Middleware를 통해서 설정을 하게 되고, 설정을 하는 부분은 반드시 Routing을 시작하기 전에 둬야 한다. (그래야 Get을 통해 Routing을 하더라도 해당 데이터를 갖고 있을 것이기 때문이다.)
Middleware 설정 시, response.locals라는 Express.js가 제공하는 Special Field를 이용한다. locals라고 하는 Local Variable들은 View들에게 전달된다. 이 locals로 설정된 값들은 오로지 View들이 Render되는 동안에만 살아 있다.
hidden input에서 csrfToken값을 활용해야 하므로 이 Partial들도 따로 빼두어 include로 쓰는 것이 좋다.

Providing User Feedback

Login이나 상품 추가 등으로 Error가 발생 했을 때, 이를 사용자가 인식할 수 있도록 해야 한다. 따라서 Redirect 전에 View에 Error가 발생 했다는 데이터를 보내줘야 한다.
즉, Redirect 전에 Error 내용에 대해서 저장하고 난 후, Redirect로 인해 새로운 Request가 생성 되었을 때, 데이터에 저장된 Error 내용을 사용할 수 있어야 한다. (일단, response.locals는 View 내에서만 사용이 가능하기 때문에 올바르지 않은 접근이다.)
Request를 처리 도중에 Redirect라는 새로운 Request가 발생했고, 동일 사용자 간 모든 Request에서 공유할 수 있는 데이터는 Session으로 처리한다 했으므로 Session을 이용하는 것이 맞다. 하지만, 단순히 Session에 이 Error을 저장한다고 했을 때, Error에 대한 내용을 오랜 시간 Session에 녹일 것이 아니라 Error을 Notify한 후 삭제할 것이기 때문에 일반적인 Session보다는 다른 기법을 요한다.
이와 같은 것들을 제공해주는 'connect-flash'라는 Third Party Package가 존재한다. 이어지는 Request가 들어오면, Notify한 Error는 Session에서 삭제하게 된다. 아래 명령어를 통해서 설치할 수 있다.
npm install —save connect-flash
Package를 설치 했다면, app.js에 Initialize가 필요하다. 아래 명령어로 Import하고, Initialize할 수 있다. 다만, Session을 이용하는 것이기 때문에 Initialize는 Session 설정 후에 하도록 한다.
const flash = require('connect-flash')
app.use(flash())
Session에 저장할 때는, request.flash() Method로 사용하며, Method의 인자로 String Key와 String Value를 받는다. 이 Key는 저장하고자 하는 Message의 이름을, Value는 그 내용을 담으면 된다.
Session에 담은 내용을 사용할 때는 똑같은 Method로 사용하되 Key값 인자만 사용하면 된다. 이렇게 한 번 사용된 Flash는 Session에서 사라지게 된다.