Cookie
HTTP는 기본적으로 서버가 클라이언트의 상태를 확인하지 않는 stateless(무상태성)을 가진다. 하지만 실제로 웹 어플리케이션을 제작할 때 로그인 상태 유지나 테마, 로그인과 관련한 인증 정보 등 클라이언트의 상태와 연계되어 동작해야 되는 경우가 많다. 이를 해결하는 방법 중 하나가 Cookie이다.
쿠키는 서버에서 클라이언트에 영속성(persistence, 프로그램이 종료되어도 사라지지 않는 데이터 특성)있는 데이터를 저장하는 방법이다. 서버는 쿠키를 이용해 웹 브라우저(클라이언트)에 특정한 데이터를 저장할 수 있으며, 해당 도메인에 대해 쿠키가 존재하면 웹 브라우저는 도메인에게 http 요청 시 쿠키를 함께 전달하게 된다. 서버가 원한다면 서버는 클라이언트의 쿠키를 이용해 데이터를 가져올 수 있다. 즉, 쿠키를 이용하는 것은 단순히 서버에서 클라이언트에 쿠키를 전송하는 것을 넘어 클라이언트에서 서버로 쿠키를 다시 전송하는 것을 포함한다.
쿠키 내에는 다양한 데이터를 담을 수 있지만, 인증 정보나 사용자 개인 정보와 같은 보안이 중요한 내용들도 포함될 수 있기 때문에 일반적으로는 해싱(암호화)처리를 하여 저장한다.
쿠키 옵션
서버는 쿠키를 이용해 데이터를 저장하고 다시 불러와 사용할 수 있지만, 데이터를 저장한 후 아무 때나 데이터를 가져올 수는 없다. 데이터를 저장한 이후 특정 조건들이 만족되어야 다시 가져올 수 있고, 이런 조건들은 쿠키 옵션으로 표현할 수 있다.
'Set-Cookie':[
'cookie=yummy',
'Secure=Secure; Secure',
'HttpOnly=HttpOnly; HttpOnly',
'Path=Path; Path=/cookie',
'Doamin=Domain; Domain=codestates.com'
]
서버에서 위와 같이 옵션을 지정한 다음 서버에서 클라이언트로 쿠키를 처음 전송하게 된다면 헤더에 Set-Cookie
라는 프로퍼티로 쿠키를 담아 전송한다. 이후 클라이언트에서 서버에게 쿠키를 전송해야 한다면 클라이언트는 헤더에 Cookie
라는 프로퍼티에 쿠키를 담아 서버에 쿠키를 전송하게 된다.
Domain
서버 주소. 쿠키 옵션에서 Domain은 포트 및 서브 도메인 정보(
www
와 같이 도메인 앞에 추가로 작성되는 부분), 세부 경로를 포함하지 않는다. 만약 쿠키 옵션에 Domain 정보가 존재한다면 클라이언트는 쿠키의 Domain 옵션과 서버의 Domain이 일치해야만 쿠키를 전송할 수 있고, 이를 통해 다른 사이트에서 받은 쿠키를 현재 사이트에 전송하는 경우를 막을 수 있다.- ex.
Domain=localhost.com
- ex.
Path
세부 경로. 서버가 라우팅할 때 사용하는 경로를 의미하며,
http://도메인:포트/
이후의 내용이 명시된다. 이를 명시하지 않으면 기본적으로/
로 설정되어 있다. 설정된 경로를 포함하는 하위 경로로 요청해도 쿠키를 서버에 전송 할 수 있다.- ex.
Path=/user
⇒ 요청 경로가/user/codestates
여도 쿠키 전송 가능
- ex.
MaxAge
orExpires
쿠키가 유효한 기간을 정하는 옵션.
MaxAge
는 쿠키가 유효한 시간을 초 단위로 설정하는 옵션이고Expires
는 쿠키가 유효한 날짜를 클라이언트의 시간을 기준으로 설정하는 옵션이다. 설정한 유효 시간(날짜)를 초과하면 쿠키는 자동으로 파괴된다. 쿠키가 영원히 남는다면 그만큼 탈취 역시 쉬워지기 때문에 이런 유효기간을 설정하는 것은 보안 측면에서 중요하다.쿠키는 위의 옵션의 여부를 기준으로 세션 쿠키(Session Cookie)와 영속성 쿠키(Persistent Cookie)로 나뉜다.
- 세션 쿠키 :
MaxAge
나Expires
옵션이 없는 쿠키로 브라우저가 실행 중일 때 사용하는 임시 쿠키. 브라우저 종료 시 해당 쿠키는 삭제된다.
- 영속성 쿠키 : 브라우저의 종료 여부와 관계 없이
MaxAge
나Expires
옵션으로 지정된 유효시간만큼 사용 가능한 쿠키.
- 세션 쿠키 :
Secure
사용하는 프로토콜에 따른 쿠키 전송 여부를 결정하는 옵션이다.
Secure
옵션이true
로 설정되어 있다면 HTTPS를 이용하는 경우에만 쿠키 전송이 가능하며, 생략 시 프로토콜에 관계 없이 쿠키 전송이 가능하다.
HttpOnly
JavaScript에서 브라우저의 쿠키에 접근 가능 여부를 결정하는 옵션. 해당 옵션이
true
로 설정되어 있다면 JavaScript로 쿠키에 접근할 수 없다. 생략 시 기본 설정인false
이며, 이 경우document.cookie
를 이용해 JavaScript에서 쿠키 접근이 가능해 XSS 공격에 취약하다.
SameSite
Cross-Origin 요청을 받은 경우 요청에서 사용한 메서드와 해당 옵션(
GET
,POST
,PUT
,PATCH
등)의 조합을 기준으로 서버의 쿠키 전송 여부를 결정하게 된다. 사용 가능한 옵션은 아래와 같다.Lax
: Cross-Origin 요청이라면GET
메서드에 대해서만 쿠키를 전송할 수 있다.
Strict
: 단어 그대로 가장 엄격한 옵션으로 Cross-Origin이 아닌same-site(서버 도메인, 프로토콜, 포트가 같은 경우)
인 경우에만 쿠키를 전송 할 수 있다.
None
: Cross-Origin에 대해 가장 관대한 옵션으로 항상 쿠키를 보내줄 수 있지만,Secure
옵션이 설정되어야 사용 가능하다.
쿠키를 이용한 상태 유지
이런 쿠키의 특성을 이용해 서버는 클라이언트에 인증 정보를 담은 쿠키를 전송하고, 클라이언트는 전달받은 쿠키를 서버에 요청과 함께 전송하여 Stateless한 인터넷 연결을 Stateful하게 유지할 수 있다.
하지만 기본적으로 쿠키는 오랜 시간 유지가 가능하고, HttpOnly 옵션 미설정 시 JavaScript로 접근할 수 있기 때문에 쿠키에 민감한 정보를 담는 것은 위험하다. 인증정보를 이용해 공격자가 유저인 척 서버에 요청을 보내면 서버는 요청에 대해 의심하지 않고 인증된 유저의 요청으로 취급하여 2차 피해가 일어날 수 있기 때문이다.
Session
Session은 Cookie와 마찬가지로 유지되어야 하는 상태를 저장하기 위해 사용하는 기술이다. 하지만 Cookie가 클라이언트(브라우저)에 상태를 저장하여 서버와 소통하는 방식이라면 Session은 서버에 상태를 저장하는 대신 Client에 유일하고 암호화된 ID 부여해 이를 통해서 소통하는 방식이다. 이 때 Session ID를 쿠키를 이용해서 소통하게 된다. 중요 데이터는 서버에서 관리하고 실제로 서버와 클라이언트가 소통하는 것은 쿠키를 통한 Session ID일 뿐이기 때문에 보안 측면에서 훨씬 안전하다는 장점을 가진다.
세션 기반 인증 (Session-based Authentication)
로그인
세션 기반 인증을 이용해 로그인을 구현한다면 위의 그림과 같은 과정을 통하도록 구현할 수 있다.
- 사용자가 웹 사이트에서 아이디 및 비밀번호를 이용해 로그인 시도
- 사용자가 인증에 성공하면 세션이라는 상태를 만들어 세션 스토어에 저장
- 만들어진 세션을 구분할 세션 아이디를 반환
- 이 과정을 통해 유저의 인증이 완료되었다는 접속 상태를 유지함
- 만들어진 세션 아이디를 쿠키를 통해 사용자에게 전달
- 받은 세션 아이디를 이용해 쿠키에 세션 아이디와 필요한 작업을 요청
- 세션 아이디를 통해 사용자가 접속 중이라는 것을 인증
- 서버는 세션 아이디를 확인해 세션 스토어에 해당 세션이 존재하는지 확인
- 세션 아이디에 맞는 세션이 존재한다면 접속 중인 것을 확인 가능
- 세션 아이디 확인 후 요청 수행
- 요청을 수행한 결과 전달
로그아웃
세션 아이디가 담긴 쿠키는 클라이언트에 저장되어 있고, 서버는 세션을 저장하고 있으며 세션 아이디로만 인증 여부를 판단한다. 그러므로 로그아웃을 구현할 때는 서버에서는 세션 정보를 삭제하고 클라이언트의 쿠키를 set-cookie
를 이용해 세션 아이디의 키값을 무효한 값으로 갱신해야 한다.
express-session을 이용한 Session 구현
Node.js에는 세션을 대신 관리해주는 express-session이라는 모듈이 존재한다. 이 모듈은 세션을 위한 미들웨어로, express 서버에서 쉽게 세션을 위한 공간을 다룰 수 있도록 만들어준다.
const express = require('express');
// 미들웨어 express-session 불러오기
const session = require('express-session');
const app = express();
app.use(
// 세션 생성
session({
// 세션 옵션 설정
secret: '@codestates', // 비밀키를 이용해 암호화하여 세션 ID 생성
resave: false,
saveUninitialized: true,
// 세션 아이디를 전송할 쿠키 설정
cookie: {
domain: 'localhost',
path: '/',
maxAge: 24 * 6 * 60 * 10000,
sameSite: 'none',
httpOnly: false,
secure: true,
},
})
);
express-session을 사용해 위와 같이 세션의 옵션을 지정할 수 있다. 이 때 세션은 secret
옵션의 비밀키를 이용해 암호화하여 세션 ID를 생성하고, 이를 클라이언트에게 쿠키로 전송한다. 쿠키로 전송된 세션 ID는 이에 종속되는 고유한 세션 객체를 가지고 있으며 고유한 세션 객체는 서버에 저장된다. 세션 객체는 유저별로 독립적으로 생성된 객체이므로 유저별로 각각 다른 데이터를 저장할 수 있으며, 클라이언트에는 유저의 개인정보가 아닌 세션 ID를 보관하여 이를 통해 유저의 인증 여부를 판단할 수 있다.
세션 객체는 req.session
으로 접근할 수 있으며 이를 이용해 세션에 임의의 데이터를 저장하거나 불러올 수 있다. 만약 세션을 파괴하려면 req.session.destroy()
를 사용하면 된다.
Cookie vs. Session
Cookie | Session | |
---|---|---|
설명 | HTTP의 Stateless한 점을 보완해주는 도구 | 접속 상태를 서버가 가짐(Stateful) 접속 상태와 권한 부여를 위해 세션 아이디를 쿠키로 전송 |
접속 상태 저장 경로 | 클라이언트 | 서버 |
장점 | 서버의 부담을 덜어줌 | 신뢰할 수 있는 유저인지 서버에서 추가로 확인 가능 |
단점 | 쿠키 그 자체는 인증이 아님 | 하나의 서버에서만 접속 상태를 가지므로 분산에 불리 |
Hashing
Hashing은 가장 많이 쓰이는 암호화 방식 중 하나로, 복호화가 가능한 다른 방식과 달리 암호화만 가능하다. 해싱은 해시 함수(Hash Function)를 사용해 암호화를 진행하고, 해시 함수는 다음과 같은 특징을 가진다.
- 항상 같은 길이의 문자열을 리턴한다.
- 서로 다른 문자열에 동일한 해시 함수를 사용하면 반드시 다른 결과값이 나온다.
- 동일한 문자열에 동일한 해시 함수를 사용하면 항상 같은 결과값이 나온다.
비밀번호 | 해시 함수(SHA1) 리턴 값 |
---|---|
‘password’ | ‘5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8’ |
‘Password’ | ‘8BE3C943B1609FFFBFC51AAD666D0A04ADF83C9D’ |
‘kimcoding’ | ‘61D17C8312E8BC24D126BE182BC674704F954C5A’ |
레인보우 테이블과 솔트(Salt)
반대로, 항상 같은 결과값이 나온다는 특성을 이용해 해시 함수를 거치기 이전의 값을 알아낼 수 있도록 기록한 레인보우 테이블이 존재한다. 레인보우 테이블에 기록된 값의 경우, 유출이 되었을 때 해싱을 했더라도 해싱 이전의 값을 알아낼 수 있어 보안상 위협이 될 수 있다.
이 때 활용 가능한 것이 솔트(Salt)이다. 음식에 소금 간을 하는 것 처럼, 해싱 이전 값에 임의의 특정 값을 더해 데이터가 유출되어도 해싱 이전의 값을 알아내기 더욱 어렵게 만드는 방법이다. 솔트를 사용하게 되면 해싱 값이 유출되더라도 솔트가 함께 유출된 것이 아니라면 암호화 이전의 값을 알아내는 것은 불가능에 가깝다.
비밀번호 + 솔트 | 해시 함수(SHA1) 리턴 값 |
---|---|
‘password’ + ‘salt’ | ‘C88E9C67041A74E0357BEFDFF93F87DDE0904214’ |
‘Password’ + ‘salt’ | ‘38A8FDE622C0CF723934BA7138A72BEACCFC69D4’ |
‘kimcoding’ + ‘salt’ | ‘8607976121653D418DDA5F6379EB0324CA8618E6’ |
해싱의 목적
해싱은 복호화가 불가능한데도 사용되는 이유는 해싱의 목적이 데이터 그 자체의 사용이 아닌 동일한 값의 데이터를 사용하는지만 확인하는 것이 목적이기 때문이다.
예를 들어, 사이트 관리자는 사용자의 비밀번호를 알고 있을 필요도, 알고 있어서도 안되기 때문에 보통 비밀번호를 DB에 저장할 때 복호화가 불가능하도록 해싱하여 저장한다. 해싱은 복호화가 불가능하기 때문에 사이트 관리자도 비밀번호를 알 수 없다. 서버측에서는 로그인 요청을 처리할 때 사용자의 비밀번호 원본 값을 몰라도 비밀번호의 해싱 값과 DB의 값과 일치하는지 확인하여 인증할 수 있게 된다.
이처럼 해싱은 민감한 데이터를 다루어야 하는 상황에서 데이터 유출의 위험성은 줄이고 유효성을 검증하기 위해 사용되는 단방향 암호화 방식이다.
Token
토큰을 정의하자면, 특정 공간이나 서비스를 이용할 수 있는 일종의 인증 수단이자 화폐라 할 수 있다. 테마파크의 티켓이나, (지금은 사라진) 버스 토큰을 떠올려 보면 이해가 쉽다. 이런 토큰은 단순한 인증 수단을 넘어 토큰의 종류에 따라 어떤 서비스를 이용할 수 있는지, 즉 권한 부여도 가능할 것이다.
토큰 기반 인증 (Token-based Authentication)
토큰 기반 인증은 서버에 유저 정보를 담는 세션 기반 인증 방식의 단점인 ‘Stateful로 인한 리소스 소모’와 ‘인증 서버 분산의 어려움’을 해결하고자 고안되었다. 토큰 인증 방식은 클라이언트에 인증과 관련한 정보를 담되, 그런 민감한 유저 정보를 암호화하여 처리하는 방식이다. 가장 대표적인 방식으로는 JWT(JSON Web Token)가 있다.
토큰 기반 인증의 장점
- Statelessness & Scalability (무상태성 & 확장성)
- 서버는 클라이언트에 대한 정보를 저장할 필요 없이 토큰이 해독되는지만 판단한다.
- 클라이언트는 새로운 요청을 보낼때마다 토큰을 헤더에 포함시키면 된다.
- 서버를 여러개 가지고 있는 서비스라면 같은 토큰으로 여러 서버에서 인증이 가능하기 때문에 모든 서버가 해당 유저의 정보를 공유해야 하는 세션 방식에 비해 더더욱 효율성이 높아진다.
- 안전하다
- 암호화한 토큰을 사용하고 암호화 키(salt)를 노출 할 필요가 없기 때문에 안전하다.
- 어디서나 생성 가능
- 토큰을 확인하는 서버가 토큰을 만들지 않아도 된다.
- 토큰 생성용 서버를 만들거나 다른 회사에 토큰 관련한 작업을 맡기는 등 다양한 활용이 가능하다.
- 권한 부여에 용이
- 토큰의 Payload 안에 어떤 정보에 접근 가능한지 정할 수 있다.
JWT(JSON Web Token)
JWT의 종류
JWT는 보통 다음과 같이 두 가지 종류의 토큰을 이용해 인증을 구현한다. 클라이언트가 처음 인증을 받으면(로그인) 액세스 토큰과 리프레시 토큰 모두 클라이언트에 저장된다.
- 액세스 토큰 (Access Token)
액세스 토큰은 보호된 정보들(유저의 이메일, 연락처, 사진 등)에 접근할 수 있는 권한 부여에 사용된다. 실질적으로 권한을 얻는 데 사용하는 토큰은 액세스 토큰이지만, 악의적인 유저가 다른 사용자의 토큰을 탈취해 악용할 가능성을 방지하기 위해 비교적 짧은 유효기간을 주어 토큰을 탈취하더라도 오랫동안 사용할 수 없게 해야한다.
- 리프레시 토큰 (Refresh Token)
액세스 토큰의 유효기간이 만료되었을 때 사용하는 토큰으로, 리프레시 토큰을 이용해 새로운 액세스 토큰을 발급받을 수 있다. 이 때 유저는 다시 로그인 할 필요가 없다. 단, 리프레시 토큰이 탈취 된다면 새로운 액세스 토큰을 발급할 수 있어 보안에 큰 위협이 될 수 있기 때문에 정보 보안이 더 중요한 서비스는 리프레시 토큰을 사용하지 않는 경우도 많다.
JWT의 구조
JWT는 위와 같이 .
을 기준으로 세 부분으로 나뉘며 각각 Header, Payload, Signature라 부른다.
- Header
Header는 해당 토큰이 어떤 종류인지, 어떤 알고리즘으로 시그니처를 sign(암호화)할지 JSON 형태로 작성된다. 이 JSON 객체를 base64 방식으로 인코딩하면 Header가 완성된다.
{ "alg": "HS256", "typ": "JWT" }
- Payload
Payload에는 단어 그대로 서버에서 활용할 유저의 정보가 담긴다. 여기에는 어떤 정보에 접근이 가능한지에 대한 권한 이나 유저의 이름과 같은 개인정보 등을 담을 수 있다. 페이로드는 시그니처를 통해 유효성이 검증될 정보이지만, 디코딩이 쉬운 base64 방식으로 인코딩 되기 때문에 민감한 정보는 담지 않는 것이 좋다. Header와 마찬가지로 JSON 형태로 작성하고 base64 방식으로 인코딩하면 된다.
{ "sub": "someInformation", "name": "phillip", "iat": 151623391 }
- Signature
Signature는 위의 정보들에 서버의 비밀키(salt)를 더해 헤더에서 지정한 알고리즘을 사용하여 해싱한다. 누군가 권한을 속이기 위해 알아낸 헤더와 페이로드를 이용해 토큰을 위조하더라도 서버의 비밀 키까지 정확히 알지 못한다면 전혀 다른 시그니처가 만들어지기 때문에 서버가 해당 토큰이 올바르지 않음을 확인할 수 있다. 아래의 예시는 암호화 방법 중 하나인 HMAC SHA256 알고리즘을 사용해 시그니처를 생성한다.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);
JWT 사용 예시
JWT는 권한 부여에 굉장히 유용하다. 새로 다운받은 A라는 어플이 Gmail과 연동해 이메일을 읽어와야 한다면, A어플은 다음과 같은 절차를 밟아
- Gmail 인증서버에 로그인 정보(아이디, 비밀번호)를 제공한다.
- 인증이 성공되면 JWT를 발급받는다.
- A어플은 JWT를 사용해 해당 유저의 Gmail의 이메일을 읽거나 사용할 수 있다.
토큰 기반 인증 절차
- 클라이언트가 서버에 아이디/비밀번호를 담아 로그인 요청을 보낸다.
- 아이디/비밀번호가 일치하는지 확인하고, 클라이언트에게 보낼 암호화된 코튼을 생성한다.
- 이 때, Access/Refresh 토큰을 모두 생성하며, 토큰에 담길 정보(Payload)는 유저를 식별할 정보, 권한이 부여된 카테고리가 될 수 있다.
- 두 종류의 토큰이 같은 정보를 담을 필요는 없다.
- 서버가 토큰을 클라이언트에 보내주면, 클라이언트는 토큰을 저장한다.
- 저장하는 위치는 Local Storage, Session Storage, Cookie 등 다양하다.
- 클라이언트가 HTTP 헤더(
Authorization
) 또는 쿠키에 토큰을 담아 보낸다. 쿠키에는 리프레시 토큰을, 헤더나 바디에는 액세스 토큰을 담는 이원화 등 다양한 방법으로 구현 가능핟.Authorization: <type> <credentials>
헤더를 사용할 시type
에는 JWT 혹은 OAuth를 뜻하는bearer
를 사용한다.
- 서버는 토큰을 확인하여 인증이 완료되었다면 클라이언트의 요청을 처리한 후 응답을 보낸다.
OAuth
OAuth는 직접 작성한 서버에서 인증을 처리하는 것이 아닌, 인증을 중개해주는 메커니즘이다. 웹이나 어플리케이션에서 흔히 볼 수 있는 소셜 로그인 인증 방식은 OAuth 2.0이라는 기술을 바탕으로 구현된다. 보안된 리소스에 액세스하기 위해 클라이언트에게 권하는 제공하는 프로세스를 단순화하는 프로토콜로, 이미 사용자 정보를 가지고 있는 웹 서비스에서 사용자의 인증을 대신해주고 접근 권한에 대한 토큰을 발급한 후 이를 이용해 내 서버에서 인증이 가능해진다.
OAuth를 쓰는 이유
유저 입장에서 서비스마다 회원가입을 하는 것은 그 과정 자체도 번거로울 뿐 더러 각각의 서비스별로 아이디와 비밀번호를 다 기억해야 하는 일 역시 매우 귀찮은 일이다. OAuth를 활용한다면 자주 사용하고 중요한 서비스(Facebook, Google, Github 등)의 아이디와 비밀번호만 기억해놓고 해당 서비스를 통해 외부 서비스로 소셜 로그인이 가능해 사용자 경험 측면에서 유리한 이점이 있다.
또한 보안상의 이점도 있는데, 검증되지 않은 App에서 OAuth를 사용한다면 직접적인 유저의 민감한 정보가 App에 노출될 일이 없고 인증 권한에 대한 허가를 미리 유저에게 구해야 하기 때문에 더 안전하게 사용이 가능하다.
OAuth에서 꼭 알아야 할 용어
Resource Owner
: 사용자이자 정보 제공자.
Client
:Resource Owner
를 대신해 보호된 리소스에 액세스하는 어플리케이션.
Local Server
:Client
의 요청을 수락하고 응답할 수 있는 서버.
Resource Server
: 사용자의 정보를 저장하고 있는 서버.
Authorization Server
: 인증을 담당하고 있는 서버이자 Access Token을 발급하는 인증 서버.
Authorization Grant
:Client
가Access Token
을 얻는 방법. 다음과 같은 방법을 주로 사용한다.- Authorization Code Grant Type
- Refresh Token Grant Type
Authorization Code
:Authorization Grant
의 한 타입으로 Access Token을 발급 받기 위한 코드.
Access Token
: 보호된 리소스에 액세스하는데 사용되는 인증 토큰. 이 토큰으로Resouce Server
에 접근할 수 있다.
Refresh Token
: 발급받은Access Token
이 만료될 시Refresh Token
을 통해 새로운 Access Token을 발급받을 수 있다.
OAuth 인증 흐름
Authorization Code Grant Type
Authorization Code를 받아 이를 통해 Access Token을 받는 방식이다. 이 유형은 Access Token이 사용자나 브라우저에 표시되지 않는다는 것을 의미하므로 Access Token이 다른 사람에게 누출 될 위험이 줄어든다.
Refresh Token Grant Type
Authorization Code Grant Type으로 Access Token을 발급받은 후 Access Token이 만료된 경우 Refresh Token을 활용해 새로운 Access Token으로 교환하는데 사용된다. 이를 통해 사용자와의 추가 상호작용 없이 계속 유효한 액세스 토큰을 가질 수 있다.
Uploaded by N2T