본문 바로가기
프로그래밍/C#

C# 웹 인증(Authentication) & 권한(Authorization) 코드 작성하기

by bantomak 2024. 2. 19.

C#으로 웹 인증 코드 작성하기

C# 프로젝트를 진행할 때 인증 관련된 코드를 작성하는 일이 생긴다면 C#이 제공하는 기능을 사용해서 좀 더 쉽게 인증 관련 코드를 작성하고 처리하는 것이 가능하다.

 

웹 인증을 위해서는 아래의 두 가지를 구현해야 한다.

 

인증(Authentication) vs 권한(Authorization)

  • 인증(authentication) : 유저가 누구인지에 대해서 검증한다. 보통 토큰이 유효(valid)한 지 검증한다.
  • 권한(authorization) : 유저가 해당 행동을 수행하는 것이 가능한지를 결정하는(determining) 프로세스이다.

 

사용자 정의 인증 스키마(Custom authentication schemes)

인증 스키마는 반드시 프레임워크 startup 파일에 등록되어야 한다. 예를 들어 쿠키 스키마를 작성한다고 한다면 이렇게 작성해야 한다.

builder.Services.AddAuthentication()
    .AddCookie();

 

만약 사용자 지정 스키마를 등록하고 싶다면 AddScheme() 메서드를 사용해야 한다.

builder.Services.AddAuthentication()
    .AddScheme<SessionTokenAuthSchemeOptions, SessionTokenAuthSchemeHandler>(
        "SessionTokens",
        opts => {}
    );

 

새로운 스키마를 정의할 때 생성해야 할 것들이 있다.

 

  • 옵션 클래스 SessionTokenAuthSchemeOptions은 AuthenticationSchmeOptions를 상속받아서 작성되었다. 만약 마땅한 옵션이 없다면 해당 클래스는 빈 상태여도 상관없다.
  • 인증 요청을 처리하는 핸들러(SessionTokenAuthSchemeHandler)는 반드시 AuthenticationHandler를 상속받아야 한다. 혹은 IAuthenticationHandler 인터페이스를 구현해야 한다.

AddScheme<TOptions, THandler>() 메서드는 첫 번째 인자로 스키마의 이름을 인자로 받는다.

하드코딩을 피하기 위해서 보통은 static 클래스를 인자로 넣지만 일단은 쉽게 가도록 하자.

 

아니면 이런식으로 클래스를 생성해서 인자로 넘겨주면 된다.

public class KeyDefault
{
    public const string AuthenticationScheme = "Key";
}
builder.Services.AddAuthentication()
    .AddScheme<SessionTokenAuthSchemeOptions, SessionTokenAuthSchemeHandler>(
        KeyDefault.AuthenticationScheme,
        opts => {}
    );

 

두 번째 인자로 스키마의 옵션을 할당할 수 있다. (options 클래스의 프로퍼티로 정의된) 옵션이 없다면 null을 넘긴다.

 

오버라이딩한 HandleAuthenticateAsync 메서드는 엔드 포인트를 향하는 모든 요청에 대해서 미들웨어를 통해서 자동적으로 호출된다.

 

public class SessionTokenAuthSchemeHandler : AuthenticationHandler<SessionTokenAuthSchemeOptions>
{
    public SessionTokenAuthSchemeHandler(
        IOptionsMonitor<SessionTokenAuthSchemeOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder) : base(options, logger, encoder)
    {
    }

    protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        // Read the token from request headers/cookies
        // Check that it's a valid session, depending on your implementation

        // If the session is valid, return success:
        var claims = new[] { new Claim(ClaimTypes.Name, "Test") };
        var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Tokens"));
        var ticket = new AuthenticationTicket(principal, this.Scheme.Name);
        return AuthenticateResult.Success(ticket);

        // If the token is missing or the session is invalid, return failure:
        // return AuthenticateResult.Fail("Authentication failed");
    }
}

 

해당 코드를 실행하고 핸들러에 중단점을 설정한 다음 애플리케이션의 컨트롤러/엔드포인트에 HTTP 요청을 보내면 모든 요청에서 처리기가 자동으로 호출되는 것을 알 수 있다.

 

필요한 클래스

  • 스키마 이름을 정의하는 클래스
  • AuthenticationSchemeOptions을 상속받은 클래스
  • AuthenticationHandler를 상속받은 클래스

 

인증 스키마를 추가하는 것만으로는 사실 충분하지 않다. 이제 컨트롤러와 액션에 권한에 대한 정의를 해줘야 한다.

 

권한(Authorization)

인증 메커니즘은 구현되었다. 이어서 인증된 유저가 접근이 가능한 엔드포인트를 정의하는 규칙을 프레임워크에게 알려야 한다.

 

권한에 대해서는 아래 예제와 같이 직접 구현이 가능하다.

[HttpGet]
public IActionResult GetActuallyNothing()
{
    if (!HttpContext.User.Identity.IsAuthenticated) // <--
    {
        return StatusCode(401);
    }

    return Ok();
}

 

사실 가장 간단한 방법은 전역적으로 권한을 요구하는 것이다. 모든 컨트롤러와 액션에 말이다. 이는 startup 코드에 해당 예제를 추가하면 가능하다.

app.MapControllers().RequireAuthorization();

 

특정한 컨트롤러나 액션에 대해서 인증을 하지 않는 화이트리스트를 작성하고 싶다면 아래와 같이 작성하면 된다.

[ApiController]
public class AuthController : ControllerBase
{
    [HttpGet("/login")]
    [AllowAnonymous] // <--
    public IActionResult Login()
    {
        // Create your token and store the associated session somewhere
        // Probably return the token in the response...
        return Ok();
    }
}

 

특정 컨트롤러나 액션에 대해서 인증을 요구하고 싶다면 아래와 같이 작성하면 된다.

[HttpGet("/secret")]
[Authorize] // <--
public IActionResult KindaSecret()
{
    return Ok();
}

 

기본 인증 스키마를 지정하지 않은 상태에서도 [Authorize] 속성을 사용할 수 있다. 특정 인증 스키마를 지정하면 가능하다.

[HttpGet("/secret")]
[Authorize(AuthenticationSchemes = "SessionTokens")] // <--
public IActionResult Secret()
{
    return Ok();
}

 

게다가 동시에 여러 개의 인증 스키마를 검증하는 것 또한 가능하다.(, 콤마로 구분한다.)

 

매번 특정한 인증 스키마를 작성하는 것을 피하고 싶다면 위에서 작성했던 것과 같이 기본 스키마 정책으로 작성하면 편한다.

builder.Services.AddAuthorization(options =>
{
    options.DefaultPolicy = new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes("SessionTokens")
        .RequireAuthenticatedUser()
        .Build();
});

 

그리고 [Authorize]를 컨트롤러와 액션에 추가하면 된다.

 

정리하자면

  • 인증 처리기(HandleAuthenticateAsync)는 모든 요청(request)에 대해서 무슨 일이 있어도 호출된다.
  • RequireAuthorization 또는 [Require]를 통해서 컨트롤러나 액션에서 인증을 진행할 수 있다.
  • [AllowAnonymous]를 사용하면 선별적으로 컨트롤러나 액션에서 인증을 진행하지 않을 수 있다.
  • 특정 인증 스키마를 지정해서 인증을 진행할 수 있다.
  • 인증 처리기는 인증이 활성화된 상태에서만 실행된다.

 

참고 사이트

 

Working with custom authentication schemes in ASP.NET Core 8.0

How to define custom authentication schemes in ASP.NET Core 8.0, and why they’re not enough to actually enforce authentication for your web application.

matteosonoio.it

댓글