큰 꿈은 파편이 크다!!⚡️

Microsoft.IdentityModel 인증 + 리액트 본문

기타 CS

Microsoft.IdentityModel 인증 + 리액트

wood.forest 2023. 7. 30. 17:36

Microsoft.IdentityModel

C#계의 oidc에서 제일 유명한 오픈소스 라이브러리로는 IdentityServer4(ids4)가 있다. 하지만 이번에 하는 작업이 이 라이브러리를 붙일 정도는 아니라고 생각해서 닷넷에서 제공하는 기본 인증(마이크로소프트가 제공하면 기본 이라고 생각하는 나)을 사용해보고 싶었다. 하지만 이 기본 인증 라이브러리도 설치가 필요했다..🫠

  • 스펙: dotnet5
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

Flow

클라이언트(웹, 리액트) ↔ 서버(api 서버이자 인증 서버, ASP.NET) 간의 인증 흐름은 이렇게 구성했다.

  1. ProtectedRoute를 가진 어떤 페이지에 접속한다
  2. 세션 스토리지에 토큰, 유저정보가 있는지 확인한다
    1. 있으면 children page를 반환한다
    2. 없으면,
  3. 로그인 페이지를 반환한다
  4. 로그인한다 ⇒ 서버로 로그인 요청하는 api를 날린다
  5. 서버가 비밀번호를 확인하고
    1. no ⇒ return null (고도화하게 되면 실패 이유까지 반환)
    2. yes ⇒ return access token
  6. 받은 결과에 따라 children page 또는 에러 이유를 표시한다

How to

Startup.cs

  • 인증을 사용할 것이고, jwt방식을 사용할 것이라는 선언을 한다.

🧨 Troubleshoot: 처음에 issuer을 그냥 키값이 될만한 아무 문자열 값으로 넣었더니 오류가 났다.. 검색해보니 꼭 issuer ip/domain을 넣어야하는것같기도?

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("your_secret_key_here")),
        ValidIssuer = "http://127.0.0.1:5000",
        ValidAudience = "your_audience_here"
    };
}).AddCookie();

 

api controller class

  • 클래스에 아래와 같은 Attribute를 추가하면, 인증을 통과한 사용자만 api를 호출할 수 있으며 그 외의 경우에는 401 Unauthorized 오류를 만날 수 있다.
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

 

사용자가 로그인할 AuthController.cs

  • 하지만 로그인을 할 때에는 인증되지 않은 사용자가 api요청을 하는 것이기 때문에 [AllowAnonymous] Attribute를 추가해서 이 api는 인증 안받아도 된다는 예외처리를 해준다.
  • 토큰을 발급할 때의 핵심은 issuer, audience, key 등을 Startup.cs에서 작성했던대로 맞추는 것이다.
[AllowAnonymous]
[HttpPost("Login")]
public async Task<string> Login([FromBody] LoginModel model)
{
  if (LoginSuccess(model)){
        var user = Guid.NewGuid().ToString();
        var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role);
        identity.AddClaim(new Claim(ClaimTypes.Name, user));

        var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("your_secret_key_here")); // Replace with your secret key
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(
            issuer: "http://127.0.0.1:5000", // Replace with your issuer
            audience: "your_audience_here", // Replace with your audience
            claims: identity.Claims,
            expires: DateTime.Now.AddMinutes(30), // Token expiration time (adjust as needed)
            signingCredentials: creds
        );

        // Serialize the token to a string
        var accessToken = new JwtSecurityTokenHandler().WriteToken(token);

        await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

        return accessToken;
  }

  return null;
}

 

LoginPage.tsx

  • 토큰을 반환하면 리액트에서는 어딘가(ex. 세션 스토리지)에 저장한다.
if (result.data){
  setAccessToken(result.data);
}

그리고 그 리액트에서 서버로 api 요청할때 auth header에 bearer로 추가한다

🧨 Troubleshoot: 원래는 axios 인스턴스를 만들어서 인스턴스의 속성에 추가하려고 했는데 레퍼런스가 잘못된 건지 먹히지 않아서 axios default에 직접 추가하게 되었다.

const GetAsync = async (path: string, parameters?: object) => {
  axios.defaults.headers.common["Authorization"] = "Bearer " + getAccessToken();
  return await axios.get(path, {
    params: parameters,
  });
};

 

이제 리프레시 토큰을 해보려다가..(헉헉) 지금 만드는 프로젝트 특성상 이것까지는 필요하지 않을 것 같아서 더이상 진행하지 않았다.

단, 어떻게 할지는 두 가지 방법 정도를 생각했었다.

  1. getAccessToken 할때마다 jwt decode 해서 exp시간이 촉박하거나 초과했으면 access token 요청하는 api를 보낸다.
  2. api 호출할때마다 토큰 확인해서 필요하면 refresh 토큰을 같이 반환한다.

 

 

도움:

반응형