보안: 기본 인증

등록일시: 2014-01-22 08:00,  수정일시: 2014-06-06 13:16
조회수: 10,124
이 문서는 ASP.NET Web API 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

기본 인증은 RFC 2617, HTTP Authentication: Basic and Digest Access Authentication 문서에 정의되어 있습니다.

장점 단점
  • 인터넷 표준
  • 모든 주요 브라우저에서 지원됨
  • 비교적 단순한 프로토콜
  • 요청과 함께 사용자의 자격 증명(Credential)이 전송됨
  • 자격 증명이 평문으로 전송됨
  • 모든 요청에 자격 증명이 함께 전송됨
  • 브라우저의 세션을 종료하는 것 외에, 로그아웃 할 수 있는 방법이 없음
  • 크로스 사이트 요청 위조(CSRF, Cross-Site Request Forgery)에 취약해서 CSRF에 대한 별도 대응 방안이 필요함

기본 인증은 다음과 같이 동작합니다:

  1. 인증이 필요한 요청인 경우, 서버가 상태 코드 401(인증되지 않음)을 반환합니다. 이 응답에는 서버가 기본 인증을 지원함을 의미하는 WWW-Authenticate 헤더가 포함됩니다.
  2. 클라이언트가 Authorization 헤더에 클라이언트의 자격 증명을 설정한 새로운 요청을 전송합니다. 이 자격 증명은 Base64로 인코딩 된, "이름:비밀번호" 포멧의 문자열입니다. 즉, 자격 증명은 암호화되지 않습니다.

기본 인증은 "영역(Realm)"의 문맥에서 수행됩니다. 이 영역의 이름은 서버가 WWW-Authenticate 헤더를 통해서 지정합니다. 그리고, 사용자의 자격 증명은 바로 이 영역 안에서만 유효합니다. 이 영역의 정확한 범위는 서버에 의해서 정의됩니다. 가령, 자원들을 분할하기 위한 목적으로 여러 영역을 정의할 수도 있습니다.

그리고, 기본 인증에서는 자격 증명이 암호화되지 않은 상태로 전송되기 때문에 HTTPS를 사용하는 경우에만 안전합니다. 이에 관한 보다 자세한 정보는 보안: Web API에서 SSL 사용하기 문서를 참고하시기 바랍니다.

또한, 기본 인증은 CSRF(크로스 사이트 요청 위조) 공격에도 매우 취약합니다. 일단 사용자가 자격 증명을 입력하고 나면, 세션이 유지되는 동안, 같은 도메인에 대한 요청들을 전송할 때, 브라우저가 자동으로 자격 증명을 함께 전송합니다. 이 점은 AJAX 요청의 경우도 마찬가지 입니다. 이 문제에 관해서는 보안: 크로스 사이트 요청 위조(Cross-Site Request Forgery) 공격 방지하기 문서를 참고하시기 바랍니다.

IIS와 기본 인증

IIS도 기본 인증을 지원하기는 하지만, 한 가지 주의해야 할 점이 있습니다. 바로, 사용자가 자신의 Windows 자격 증명을 통해서 인증된다는 점입니다. 결국, 이 얘기는 사용자가 반드시 서버의 도메인에 속한 계정을 갖고 있어야만 한다는 뜻입니다. 그러나, 공개된 웹 사이트인 경우, 대부분 ASP.NET 멤버십 공급자를 통해서 인증 받는 것이 일반적일 것입니다.

IIS를 이용한 기본 인증을 활성화시키려면, 먼저 ASP.NET 프로젝트의 Web.config에서 인증 모드를 "Windows"로 설정합니다:

<system.web>
    <authentication mode="Windows" />
</system.web>

이 모드에서 IIS는 인증을 위해서 Windows 자격 증명을 사용합니다. 그리고, IIS에서 기본 인증도 활성화시켜야 합니다. IIS 관리자에서 기능 보기(Features View)로 이동한 다음, 인증(Authentication)을 선택하고 기본 인증(Basic authentication)을 활성화시킵니다.

마지막으로, Web API 프로젝트에서는 인증이 필요한 모든 컨트롤러 액션에 [Authorize] 어트리뷰트를 추가합니다.

클라이언트는 요청에 Authorization 헤더를 설정해서 직접 인증을 수행하게 됩니다. 클라이언트가 브라우저인 경우에는 자동으로 이 과정이 수행됩니다. 반면, 클라이언트가 브라우저가 아니라면, 직접 이 헤더를 설정해줘야 합니다. 다음 C# 예제에서는 사용자의 기본 자격 증명을 전송하기 위해서 HttpClient를 사용하고 있습니다:

HttpClientHandler handler = new HttpClientHandler()
{
    UseDefaultCredentials = true
};
HttpClient client = new HttpClient(handler);
client.BaseAddress = new Uri("https://localhost");
var response = client.GetAsync("api/values").Result;

또는, HttpClientHandlerCredentials 속성을 이용해서 자격 증명을 설정할 수도 있습니다:

HttpClientHandler handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential("username", "password");
HttpClient client = new HttpClient(handler);

사용자 지정 멤버십을 이용한 기본 인증

이미 살펴본 것처럼 IIS 내장 기본 인증은 Windows 자격 증명을 사용합니다. 결국 이 말은, 호스팅 서버에 사용자의 계정을 생성해야 한다는 뜻입니다. 그러나, 인터넷 응용 프로그램에서는 대부분 사용자 계정을 외부 데이터베이스에 저장합니다.

다음 코드는 기본 인증을 수행하는 HTTP 모듈을 보여주고 있습니다. 이 예제 코드에 임시로 구현되어 있는 CheckPassword 더미 메서드만 수정하면 ASP.NET 멤버십 공급자를 이용하도록 간단하게 변경할 수 있습니다

namespace WebHostBasicAuth.Modules
{
    public class BasicAuthHttpModule : IHttpModule
    {
        private const string Realm = "My Realm";

        public void Init(HttpApplication context)
        {
            // Register event handlers
            context.AuthenticateRequest += OnApplicationAuthenticateRequest;
            context.EndRequest += OnApplicationEndRequest;
        }

        private static void SetPrincipal(IPrincipal principal)
        {
            Thread.CurrentPrincipal = principal;
            if (HttpContext.Current != null)
            {
                HttpContext.Current.User = principal;
            }
        }

        // TODO: Here is where you would validate the username and password.
        private static bool CheckPassword(string username, string password)
        {
            return username == "user" && password == "password";
        }

        private static bool AuthenticateUser(string credentials)
        {
            bool validated = false;
            try
            {
                var encoding = Encoding.GetEncoding("iso-8859-1");
                credentials = encoding.GetString(Convert.FromBase64String(credentials));

                int separator = credentials.IndexOf(':');
                string name = credentials.Substring(0, separator);
                string password = credentials.Substring(separator + 1);

                validated = CheckPassword(name, password);
                if (validated)
                {
                    var identity = new GenericIdentity(name);
                    SetPrincipal(new GenericPrincipal(identity, null));
                }
            }
            catch (FormatException)
            {
                // Credentials were not formatted correctly.
                validated = false;
            }
            return validated;
        }

        private static void OnApplicationAuthenticateRequest(object sender, EventArgs e)
        {
            var request = HttpContext.Current.Request;
            var authHeader = request.Headers["Authorization"];
            if (authHeader != null)
            {
                var authHeaderVal = AuthenticationHeaderValue.Parse(authHeader);

                // RFC 2617 sec 1.2, "scheme" name is case-insensitive
                if (authHeaderVal.Scheme.Equals("basic", StringComparison.OrdinalIgnoreCase) &&
                    authHeaderVal.Parameter != null)
                {
                    AuthenticateUser(authHeaderVal.Parameter);
                }
            }
        }

        // If the request was unauthorized, add the WWW-Authenticate header 
        // to the response.
        private static void OnApplicationEndRequest(object sender, EventArgs e)
        {
            var response = HttpContext.Current.Response;
            if (response.StatusCode == 401)
            {
                response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", Realm));
            }
        }

        public void Dispose()
        {
        }
    }
}

이 HTTP 모듈을 활성화시키려면 web.config 파일의 system.webServer 섹션에 다음 구성을 추가하면 됩니다:

<system.webServer>
  <modules>
    <add name="BasicAuthHttpModule" type="WebHostBasicAuth.Modules.BasicAuthHttpModule, BasicAuth"/>
  </modules>