.NET6 C# 으로 구현한 기본 인증 Basic Authentication

이전 글에서 Basic Authentication이 어떻게 구현 되는지 설명은 했는데, 사실 코드가 어떻게 구동 되는지 보지 않으면 이해가 되지 않을 수도 있을 것 같아서 이렇게 github gist 도 써보고자 한 번 코드로 기본 인증을 구현해보자

그리고 2021. 11월에 새로 나온 .NET6 의 새 기능 Minimal API 을 써보도록 하자

Minimal API 소회
이전의 .NET5, .NET Core 2~3.1 등등 많이 사용하면서 지내왔는데,
Minimal API 이거 진짜 nodejs로 개발 하는 느낌이다
(빠르게 몇 줄 만들면 express 촥~ 기능  촥~ 만들어지는 그런 느낌이랄까?  
nodejs로 개발해보신 분들이라면 이해하는 감정일꺼임)

Minimal API vs Use Controller 사이에 다른 점을 확실히 이해하고 진행해보도록 한다

  • BasicAuthenticationHandler.cs - 기본 인증 핸들러
  • BasicAuthenticationOptions.cs - 기본 인증 옵션
  • IdentityBasicAuthenticationHandler.cs - 비지니스만 추상화 시킨 인증 핸들러
  • Program.cs -  .NET6 Minimal API 의 실행 파일 (모든 구현은 여기서)

총 4개의 파일로 구성되어져 있는데, 사실 나머지는 내 의도일 뿐이고, Program.cs 하나로 다 몰아버리면 모든게 되는게 신기한 Minimal API (짝짝짝)

총 24 lines 의 코드로 기본 인증이 가능한 서버를 만들어낸다 (물론 진짜 구현된 class 파일은 따로 있는게 함정)

중요한 설명들은 아래 코드에서 모든게 다 설명이 되고 구현을 한다

services
    .AddAuthentication("BasicAuthenticaion")
        .AddScheme<BasicAuthenticationOptions, IdentityBasicAuthenticationHandler>("BasicAuthenticaion", null);
Program.cs line 4 ~ 6
app.UseEndpoints(endpoints =>
{
    endpoints.MapGet("/", context => context.Response.WriteAsync("Hello World!")); // 첫 진입점 (인증 없음)
    endpoints.MapGet("/api/data", context => context.Response.WriteAsync("get authorization data"))
            .RequireAuthorization(); // 인증이 성공해야 볼 수 있는 지점
});
Program.cs line 17 ~ 21

몇 줄 되지 않는 Program.cs 로 간단하고 빠른 개발이 가능한 느낌이라 정말 Micro Service 가 많아지겠거니 하는 생각은 들지만... (실무자는 더이상의 말은 생략한다ㅋㅋㅋ)

다시 돌아가서 코드를 만들어보도록 하자
이 곳은 기본 인증을 구현하는게 목적이니, 그것에 포커스를 맞춰서 진행한다

기존에 .NET Core나 .NET5 에서 Startup.cs 에서 있던 코드를 그대로 따라왔다고 생각하자
ISerivceCollection service.AddAuthentication() 를  정의(Go to define)로 보게 되면 AuthenticationBuilder 를 return 하는 것을 볼 수 있다. 따라가자.

AuthenticationBuilder 빌더의 정의를 또 보게 되면 AddScheme<TOption, THandler>() 을 확인할 수 있다.  AddScheme<TOptions, THandler> 는 무엇인지는 클래스에 이미 나와있다.

where TOptions : AuthenticationSchemeOptions, new() 
where THandler : AuthenticationHandler<TOptions>

아하! AuthenticationSchemeOptions,  그리고 AuthenticationHandler<TOptions> 구나!  이게 무엇인지 자세히 모르겠으니 MSDN 을 활용한다.

All authentication schemes that use derived AuthenticationSchemeOptions and the associated AuthenticationHandler:

모든 인증 스킴들은 that 이하다 (갑분 영어 독해??)
- AuthenticationSchemeOptions으로부터 파생되고,
- AuthenticationHandler 와 관련이 있다

결론은 저녀석들을 사용해서 만들어야 한다는 소리다

뭔가 석연치가 않다. 단지 기본 인증 Basic Authentication 하나 만들려고 하는데 이미 방법도 다 알고 있는데 - base64-encoded(A:B) - 왜 이렇게 어려울 것인가? 를 생각하면서 이 곳에서는 저 듣보들을 사용한다고 하는데 도대체 내가 왜 이런걸 알아야 하는거지? 하는 생각이 들 것이다. 그저 이런 원리를 통해 만들어진다 라고 설명하는 중이다.  이 글을 보는 C# .NET 개발자라면, [Authorize] 필터가 제 역할을 하기 위해서 밑밥을 까는게 현재 AuthenticationHanlder 의 구현이다. (라고 생각하면 이해하기가 좀 더 쉬울려나??)

다시 돌아가서 BasicAuthenticationHandler.cs 파일을 살펴보자

추상(abstract) 클래스 BasicAuthenticationHandlerAuthenticationHandler<BasicAuthenticationOptions> 를 상속(inherit) 한다

어??? 아까 걔다. AuthenticationHandler<TOptions> 잠깐! 그럼 TOptions 에 있는 BasicAuthenticationOptions 는 뭐지?
BasicAuthenticationOptions.cs 파일을 보면 BasicAuthenticationOptionsAuthenticationSchemeOptions를 상속 받고 있다

옴마! 다 쓰고 있네?? 위에 영어(해석 본)를 다시 보면

- AuthenticationSchemeOptions으로부터 파생되고, => 얘 꼭 써야해!
- AuthenticationHandler 와 관련이 있다  => 관련이면 일단 참고해둬!

에 딱인 상황이다

AuthenticationSchemeOptions 부터 살펴보자

public class BasicAuthenticationOptions : AuthenticationSchemeOptions
{
    public string Realm => "Basic Auth Server";
}

BasicAuthenticationOptions 클래스를 만들고 Realm 프로퍼티를 만들어서 기본 값을 넣어준 상태다

Realm  : authentication parameter is reserved for use by authentication schemes that wish to indicate a scope of protection.
[참고 설명 : Realm (stackoverflow)]

사실 이 부분은 굳이 BasicAuthenticationOptions 를 사용하지 않고, AuthenticationSchemeOptions만 사용해도 무방하다.

예를 들면 아래와 같이

services
    .AddAuthentication("BasicAuthenticaion")
        .AddScheme<AuthenticationSchemeOptions, IdentityBasicAuthenticationHandler>("BasicAuthenticaion", null);

public class ThisIsExampleClassName : AuthenticationHandler<AuthenticationSchemeOptions>
{
  ..
}

이런 형태로 AuthenticationSchemeOptions만 사용해도 가능하다
단지 개념적으로 Realm 을 추가 했던 과거의 나놈이 있어서 코드에는 들어간 내용일 뿐이다
(전혀 개의치 않고 AuthenticationSchemeOptions만 사용해도 된다)

우리가 상속해야 할 AuthenticationHandler 클래스를 살펴보자.

public abstract class AuthenticationHandler<TOptions> : IAuthenticationHandler where TOptions : AuthenticationSchemeOptions, new()
{
.... 뭐 이것저것 써 있음 
....
....
    protected abstract Task<AuthenticateResult> HandleAuthenticateAsync();
....
....뭐 이것저것 써 있음 
....

}

보는 바와 같이 추상(abstract) 클래스이다 - 추상 클래스면 필수로 구현 해야 하는 키워드 abstract 를 찾아야 한다.  클래스 중간에 Task<AuthenticateResult> HandleAuthenticateAsync();를 필수로 구현해야 하게 되어있다

자 이제 드디어 만든 코드 설명을 할 수 있게 되었다 (감격)

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>

class BasicAuthenticationHandler 는 AuthenticationHandler<TOptions> 를 상속 받고, 그에 따라 필수로 HandlerAuthentcateAsync() 메소드를 구현해야만 했다

어렵게 생각하지 말고 우선 새로운 클래스 하나를 만들고, AuthenticationHandler<TOptions> 를 상속해보자

갓갓(God) Visual Studio 님께서는 바로 빨간줄을 그려주시고, 해당 구현을 자동으로 해주신다
감사한 마음으로 구현을 누르고, 또 한 번 감사하는 마음으로 생성자를 생성한다

단박에 후다닥 코드가 만들어진다

public class ThisIsExampleClassName : AuthenticationHandler<BasicAuthenticationOptions>
{
    public ThisIsExampleClassName(IOptionsMonitor<BasicAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        throw new NotImplementedException();
    }
}
class 이름을 ThisIsExampleClassName 으로 만든 예시코드

사실 여기 까지 만들면 다 한거나 다름 없다.

이후 부터는 진짜 구현이다. Basic Authentication 의 Spec 을 구현하면 된다.

인증 미들웨어가 실행 되었을 때 DefaultAuthenticateScheme을 호출하면 HandleAuthenticateAsync() 메소드가 실행된다.

결과가 리턴 되는 값으로 AuthenticateResult 가 보이는데, 이 클래스의 정의로 들어가면 아래와 같이 3가지의 조건이 나온다

  • AuthenticateResult.NoResult() 아무 결과 없음을 생성 (null도 가능)
  • AuthenticateResult.Fail(Exception or string)
    실패를 나타내며, 예외(Exception) 또는 오류 메시지(failureMessage)를 제공가능
  • AuthenticateResult.Success(ticket)
    성공을 나타내며 ticket 은 사용자 정보(UserInfo)가 포함된 AuthenticationTicket

위의 Github Gist 의 코드를 참고 하면서 보면 Request Header 의 Authorization : Basic <credentials> 로 들어오는 것을 검증하고, credentials 분리 하고, 해당 내용을 userid , password 로 나눠서 Biz Logic Process를 따로 만들 수 있도록 추상 메소드를 하나 더 만들었다.
사실 회사에서 그냥 단일로 쓰다가, 다른 곳을 봐야 하는 요구사항이 생겨서 Biz Logic 만 따로 구현하도록 만들었다

뭔가 더 설명할 내용이 있을 것 같지만, 이미 많은 말들을 남겨놨고, 이 부분에서 얻어갈 사람들은 알아서 얻을 수 있을 것이라 생각하며 이만

References

Tutorial: Create a minimal web API with ASP.NET Core
Learn how to build a minimal web API with ASP.NET Core.
Overview of ASP.NET Core Authentication
Learn about authentication in ASP.NET Core.