kakasoo

[HTTP] 19. 기본 인증 본문

프로그래밍/HTTP

[HTTP] 19. 기본 인증

카카수(kakasoo) 2021. 1. 31. 17:03
반응형

12.1 인증

인증은 당신이 누구인지 증명하는 것이다. 완벽한 인증은 없다. 다만 인증을 통해 당신이 누군지 판단하는 도움은 될 수 있다.

12.1.1 HTTP의 인증요구/응답 프레임워크

HTTP는 사용자 인증을 하는 데 사용하는 자체 인증요구/응답 프레임워크를 제공한다. 순서는 요청, 인증요구, 인가, 성공의 단계로 이루어진다.

맨 처음 요청은, 클라이언트가 서버 측에 정보를 요청한다는 의미이다. 이 때에는 아무런 헤더도 필요없고, 그저 GET 메서드를 했다는 의미이다. 다음부터가 인증 요구이다.

인증 요구는 서버가 사용자에게 사용자 이름과 비밀번호를 제공하라는 지시다. 이 때, 401 Unauthorized 상태 정보와 함께 요청을 반려한다. 이 때 WWW-Authenticate 헤더에 각 영역에 대한 설명을 덧붙인다.

인증은 클라이언트가 다시 요청을 보내는 것이다. 이 때는 인증 알고리즘과 사용자 이름, 비밀번호를 기술한 Authorization 헤더를 함께 보낸다.

인증 정보가 옳다면 다음은 성공 단계이다. 성공 단계에서는 선택적으로, 서버가 Authentication-Info 헤더를 함께 보낸다.

import express from 'express';
const router = express.Router();

router.get('/', function (req, res, next) {
    // res.append('WWW-Authenticate', 'Basic realm="Access to the staging site."');
    res.append('WWW-Authenticate', 'Basic realm="myRealm"');
    // res.append('Authorization', ' Basic realm="myRealm"');

    console.log('Authorization value : ', res.getHeader('Authorization')); // req와 res를 둘 다 비교하기 위해 작성하였다.
    console.log(req.headers);
    res.status(401).send('Unauthorized.');
});

export default router;

res.append()를 사용하여 myRealm이라는 영역을 설정해주었다. 인증은 basic으로 하였다.

이제 이 값을 사용해서 로그인 페이지로 넘어가는 로직을 만들 수 있다. 코드를 수정한 것은 아래와 같다.

import express from 'express';
const router = express.Router();

router.get('/', function (req, res, next) {
    res.append('WWW-Authenticate', 'Basic realm="myRealm"');
    // kakasoo:123 => base-64
    if (req.headers['authorization'] === 'Basic a2FrYXNvbzoxMjM=') {
        return res.status(200).send('login!');
    }
    res.status(401).send('Unauthorized.');
});

export default router;

if문을 하나 추가하였다. 이것으로 로그인 여부를 체크할 수 있다. 원래는 base-64로 인코딩하여 비교해야 하지만, 그냥 인코딩된 값을 그대로 넣어 비교하게 하였다. 실제로는 이렇게 하면 안된다.

값이 일치하면 로그인에 성공하는 것을 볼 수 있다. 다만 이것을 사용해도 된다는 의미는 아니다.

아래는 MDN에서 가져온 내용이다. 이제는 사용되지 않는다고 한다. ( 우리가 입력한다고 해도 소용 없는 부분이다. )

또한, 이것을 실제로 사용한다고 치더라도 로그인 인증을 위해서 별도의 파일에 아이디와 비밀번호를 저장하여, 일치하는지 여부를 봐야 한다고 한다.

위에 MDN 문서와 같이, 실제로 req.url을 출력해보면 아무것도 나오지 않는다. 그저 경로만이 표현된다. ( req 안에도 남아있지 않았다. )

이상의 내용은 책에도 나와있었는데, 321P의 옮긴이 말을 보면, HTTP 자체 인증은 이제 사용하지 않고 각각의 인증 모듈을 이용해서 직접 구현하는 편이라고 한다.

passport.js

import passport from 'passport';
import GitHub from 'passport-github2';
import dotenv from 'dotenv';
dotenv.config();

const GitHubStrategy = GitHub.Strategy;

let GITHUB_CLIENT_ID: string = process.env.GH_ID;
let GITHUB_CLIENT_SECRET: string = process.env.GH_SECRET;

passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((obj, done) => done(null, obj));

passport.use(
  new GitHubStrategy(
    {
      clientID: GITHUB_CLIENT_ID,
      clientSecret: GITHUB_CLIENT_SECRET,
      callbackURL: 'http://127.0.0.1:3000/auth/github/callback',
    },
    (accessToken, refreshToken, profile, done) => {
      // User.findOrCreate({ githubId: profile.id }, (err, user) => {
      return done;
    },
  ),
);

export default passport;
import express from 'express';
import github from './passport/github';

const app: express.Application = express();

app.get(
  '/',
  (req: express.Request, res: express.Response, next: express.NextFunction) => {
    res.send('hello typescript express!');
  },
);

app.get(
  '/auth/github',
  github.authenticate('github', { scope: ['user:email'] }),
);

app.get(
  '/auth/github/callback',
  github.authenticate('github', { failureRedirect: '/login' }),
  (req, res) => res.redirect('/'),
);

export default app;

책에서 말한 각각의 인증 모듈에, 아마도 이런 passport.js가 포함되어 있지 않을까 싶어서 가져왔다. 이것 역시 요청, 인증요구, 인가, 성공의 단계를 따르고 있다.

아래의 보안 영역과 그 흐름을 보고 passport와 비교해보면 어떨까?

12.1.2 인증 프로토콜과 헤더

HTTP는 필요에 따라 고쳐 쓸 수 있는 제어 헤더를 통해 다른 인증 프로토콜에 맞추어 확장할 수 있는 프레임워크를 제공한 다. OAuth도 HTTP의 인증 프레임워크를 확장한 것이라고 한다.

⇒ 확장은 맞겠지만, HTTP의 기능을 사용한다기보다, 그 개념을 차용했다고 보는 것이 옳은 거 같다. HTTP 자체 프레임워크에서 사용하는 사용자 이름과 비밀번호는 더 이상 쓰이지 않기 때문.

⇒ 따라서 이걸 사용해서 인증을 한다기보다, 이 인증 흐름을 이해하는 것이 더 좋을 거 같다.

아래를 보고 더 디테일하게 이해해보자.

12.1.3 보안 영역

웹 서버는 기밀 문서를 보안 영역으로 나눈다. WWW-Authenticate에 realm 지시자로 기술이 되는데, 상대방에게 어떤 자격을 인증해야 하는지에 대한 힌트를 제공한다.

⇒ 아마도 이 부분이, 전략에 해당하는 부분이 아닌가 싶다. 아래는 전체적인 흐름을 보여준다. 여기서 기본 인증이라는 게, basic을 의미한다. 즉 basic realm도, 여기서 말하는 basic 인증이다.

12.2 기본 인증

기본 인증은 가장 잘 알려진 HTTP 인증 규약이다. 모든 주요 클라이언트와 서버에는 기본 인증이 구현되어 있다. 이 기본 인증은, 웹 서버가 클라이언트의 요청을 거부하고 401 상태코드를 반환하게 할 수 있다.

앞에서 설명한 것이 기본 인증이었다.

순서

  1. 사용자가 자신이 찾고자 하는 리소스를 요청한다.
  2. 서버는 WWW-Authenticate 헤더에 리소스에 접근하는 데 필요한 비밀번호를 요구하는 401 응답을 반환한다.
  3. 브라우저는 401 응답을 받고, 이 영역에 관한 사용자 이름과 비밀번호를 요구하는 대화 상자를 띄운다. 이 부분에 대한 이미지는 맨 위에 있다.
  4. 사용자가 사용자 이름과 비밀번호를 입력하면 그것을 콜론으로 이어붙인 후, Base-64 방식으로 인코딩하여 Authorization 헤더에 담아 보낸다.
  5. 서버는 다시 디코딩하여 값이 일치하면 200 응답으로 처음 요청 받은 문서를 보낸다.

Passport 동작 순서는?

  1. 사용자가 자신이 찾고자 하는 리소스를 요청한다.
  2. 서버는 지정한 로그인 방식으로 사용자를 리다이렉션시킨다. ( 리다이렉션 상태 코드나 401 상태 코드를 보내주면 된다, 이 부분이 위에서는 2에 해당한다. )
  3. 사용자는 로그인을 하면, 그 정보가 일치하는지를 해당 로그인 사이트에서 대신 판단해준다. ( 이 부분이 위에서는 4에 해당한다. )
  4. 일치할 경우 사용자는 다시 원래 페이지로 리다이렉션된다. ( 이 부분이 5에 해당한다. )

12.2.2 Base-64 사용자 이름/비밀번호 인코딩

Base-64가 어떻게 암호화를 하는지에 대한 내용은 다루지 않는다.

Base-64는 원래 시스템에 문제를 일으킬 수 있는 문자열을 받아 전송할 수 있도록 하기 위해 만들어졌다. 즉, 암호화의 목적으로 만들어진 게 아니다. 예컨대 URL에 한글이 있으면 시스템에 문제가 될 수 있다.

이런 상황에 바로 Base-64를 사용하는 것이다.

노출을 방지할 수는 있지만, 디코딩하는 것도 매우 간단하기 때문에 이를 보안 목적으로 사용하는 것은 부적절하다. 뒤에 더 자세한 설명이 나온다.

12.2.3 프락시 인증

어떤 서버는 무조건 프락시를 거치게 하고, 또 그 프락시에서 인증을 하게 한다. 프락시 서버에서 접근 정책을 중앙관리할 수 있고, 회사 리소스에 대한 통합적인 접근을 제어하기 위해서도 프락시 서버는 유용하다.

프락시 서버도 인증 방식은 같지만 상태코드와 헤더가 달라지므로 그 부분만 유의하자. ⇒ 이 부분도 이제 안쓰이지 않을까 싶다.

12.3 기본 인증의 보안 결함

  1. bsae-64는 디코딩이 매우 쉽다.
  2. 디코딩을 하지 않고, 암호화된 값을 그대로 서버에 전송하여 인증할 수도 있다. 기본 인증은 이러한 재전송 이슈에 대응하지 않는다.
  3. 설령 해당 사이트가 인증이 크게 중요하지 않다고 하더라도, 사용자의 개인 정보가 다른 사이트에서도 동일한 경우가 있을 수 있으므로, HTTP 기본 인증은 쓰지 않는 것이 좋다.
  4. 프락시나 중개자가 개입하면 기본 인증은 정상적인 동작을 보장하지 않는다.

⇒ 이상으로 기본 인증에 관한 내용은 마친다.

HTTP 2.0

들어가기 앞서서, 이게 정말로 사용되지 않는 내용인가 싶어 작성한다. 우리가 보는 책의 10장, HTTP/2.0에 관한 내용은 저자가 아니라, 옮긴이가 추가로 작성한 내용이었다.

참고로 이 책을 번역한 사람은 NHN(현 네이버) 에서 일하는 두 회사원들로, 이 책이 나올 당시는 2014년 11월, HTTP/2.0 명세가 나온 것은 2015년 2월로, 지금으로부터 최소 6년 전이다. 그래서 현 시점을 비교해보자 한다.

(8년동안 일하고 난 후 비바 리퍼블리카로 이직했다. 아래는 그 분의 블로그로 추정되는 곳. )

eungjun

아래는 네이버를 개발자 모드로 확인한 것이다.

구글은 왜 SPDY를 만들었을까?

NAVER D2

정리 ⇒ 10년 동안 업데이트가 되지 않은 HTTP/1.1, 그러나 사이트는 페이지 당 요청의 수가 20배 이상 증가하였음. 따라서 더 빠른 웹 프로토콜이 필요하게 됨.

SPDY의 특징

  1. TLS 계층 위에서만 동작
  2. HTTP 헤더 압축 ( 10 ~ 35 %의 압축을 보장하고, 여러 번의 요청이 오갈 경우에는 최대 97% 까지 압축이 된다. )
    • 원래 헤더의 크기는 별 다른 문제가 없었지만, 웹의 발전에 따라서 하나의 페이지를 위해 수백 개의 트랜잭션이 오가기 때문에 헤더의 크기도 무시할 수 없게 되었다고 한다.
  3. 텍스트가 아닌 바이너리로 구성되기 때문에 파싱이 빠르고 오류가 적다.
  4. 멀티플렉싱 방식이라 FIFO인 기존의 프로토콜보다 더 빠르고 효율적이다. ( 위의 이미지이다. )
    • 하나의 커넥션 위에서 여러 개의 스트림이 동시에 만들어질 수 있고, 여러 개의 요청과 응답을 동시에 처리할 수도 있다.
  5. 서버 푸시 (server push)
    • 서버가 필요하다고 생각되면 (!?) 능동적으로 리소스를 보내줄 수 있다. 설령 클라이언트가 요청하지 않았어도!
    • 이걸 받은 클라이언트는 반드시 동일 출처 정책에 따라 검사해야 한다. ( CORs )

사실 문제라고 한다면, 그냥 HTTP/1.1의 성능만 개선한 거기 때문에, 보안 상의 문제는 그대로 가지고 있다. 어플리케이션 레벨에서 해결해야 할 문제로 보인다. (커넥션 유지로 인한 개인정보 유출 문제)

실제 사용

실제 사용을 하지 않고 코드를 가져왔다.

사용 방법은, openssl을 통해서 key와 인증서를 다운로드해야 하고, 그것을 fs 모듈을 통해 읽어낸 다음, spdy에 옵션으로 주면 된다고 한다.

const spdy = require('spdy');
const fs = require('fs');
const express = require("express");
const cors = require('cors')

const options = {
  key: fs.readFileSync('./server.key'),
  cert:  fs.readFileSync('./server.crt'),
  passphrase: '개인키를 만들때 생성한 비밀번호'
};

const app = express(); 
app.use(express.static('images'))  
app.use(cors());
app.get("/test", (req,res)=>{ 
  const images = fs.readdirSync('./images') 
  res.send(images);
})
const port = 3000;

spdy
  .createServer(options, app)
  .listen(port, () => {
    console.log(`App started listening on PORT ${port}`);
  });
반응형

'프로그래밍 > HTTP' 카테고리의 다른 글

[HTTP] 21. 엔터티와 인코딩  (0) 2021.01.31
[HTTP] 20. 보안 HTTP  (0) 2021.01.31
[HTTP] 18. 클라이언트 식별과 쿠키  (0) 2020.12.20
[HTTP] 17. 캐시(2)  (0) 2020.12.06
[HTTP] 16. 캐시(1)  (0) 2020.12.05