gRPC - JWT Token 을 통한 인증 처리

2022. 5. 24. 00:00ASPNET/Grpc

반응형

ASP.NET Core 인증과 함께 gRPC를 사용하여 각 호출과 사용자를 연결할 수 있습니다.

 

ASP.NET Core에서 Identity 소개

ASP.NET Core 앱과 함께 Identity를 사용합니다. 암호 요구 사항(RequireDigit, RequiredLength, RequiredUniqueChars 등)을 설정하는 방법을 알아봅니다.

docs.microsoft.com

 

Share project 생성 (c# library)

 

CommonConfiguration/Configuraion.json
(property 에서 build 시 copy 되도록 설정하자)

요런식으로

 

{
  "TokenManagement": {
    "Secret": "99ce883bfb15df8e5422d9f6e987500be6d98fb1e47a13aa12f29e38c3ad0a99",
    "Issuer": "Tester",
    "Audience": "Tester!GrpcService",
    "AccessExpiration": 1440,
    "RefreshExpiration": 10
  }
}

Models/TokenManagement.cs

namespace Share.Models;
public class TokenManagement
{
    public string Secret { get; set; }
    public string Issuer { get; set; }
    public string Audience { get; set; }
    public int AccessExpiration { get; set; }
    public int RefreshExpiration { get; set; }
}

Utility/CustomConfigurationHelper.cs

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using System.Diagnostics;

namespace Share.Utility;
public class CustomConfigurationHelper
{
    private static IConfigurationBuilder _configBuilder;
    private static readonly object _lock = new object();
    public static IConfigurationRoot CreateConfigurationBuilder(string customJsonFile, string[] args = null)
    {

        var result = CreateConfigurationRoot(customJsonFile, args);

        result = result?.Providers.Count() == 0 ? CreateConfigurationRoot(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, customJsonFile), args) : result;

        return result;
    }


    private static IConfigurationRoot CreateConfigurationRoot(string customJsonFile, string[] args)
    {
        lock (_lock)
        {
            try
            {

                if (_configBuilder == null)
                {
                    _configBuilder = new ConfigurationBuilder().AddJsonFile(customJsonFile, true, true);
                }
                else
                {
                    var jsonConfigList = _configBuilder.Sources.Where(source => source is JsonConfigurationSource).OfType<JsonConfigurationSource>().ToList();
                    if (jsonConfigList.Exists(jsonSource => jsonSource.Path.Equals(customJsonFile, StringComparison.OrdinalIgnoreCase)))
                    {
                        return _configBuilder.Build();
                    }

                    _configBuilder = _configBuilder.AddJsonFile(customJsonFile, true, true);
                }

                if (args != null)
                {
                    _configBuilder = _configBuilder.AddCommandLine(args);
                }

                return _configBuilder.Build();
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
                return _configBuilder.Build();
            }
        }
    }
}

Utility/JwtTokenHelper.cs

using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System.Dynamic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Share.Utility;

public class JwtTokenHelper
{
    public string GenerateToken(string userId, string role = "user")
    {
        var config = CustomConfigurationHelper.CreateConfigurationBuilder("./CommonConfiguration/Configuration.json");
        config = CustomConfigurationHelper.CreateConfigurationBuilder("./Configuration/Configuration.json");

        var now = DateTime.UtcNow;
        dynamic token = config.GetSection("TokenManagement").Get<ExpandoObject>();
        int.TryParse(token.AccessExpiration, out int accessExpiration);
        var claims = new[]
            {
                    new Claim("Name", userId),
                    new Claim("Role", role),
                    new Claim("Ticks", now.AddMinutes(accessExpiration).Ticks.ToString())
                };



        var jwtToken = new JwtSecurityToken(
        token.Issuer,
        token.Audience,
        claims,
        expires: now.AddMinutes(accessExpiration),
        signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)), SecurityAlgorithms.HmacSha512Signature));
        return new JwtSecurityTokenHandler().WriteToken(jwtToken);

    }
}

Protos/user.proto

syntax = "proto3";
import "google/protobuf/empty.proto";

option csharp_namespace = "GrpcInterface";

package user;

// The greeting service definition.
service User {
  // Sends a greeting
  rpc Login (LoginRequest) returns (LoginResponse);
  rpc AuthHello(google.protobuf.Empty) returns (AuthHelloResponse);
}

// The request message containing the user's name.
message LoginRequest {
  string id = 1;
}

// The response message containing the greetings.
message LoginResponse {
  bool result = 1;
  string accessToken = 2;
}

message AuthHelloResponse {
  bool result = 1;
  string message = 2;
}

Services/UserService.cs

using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using GrpcInterface;
using Microsoft.AspNetCore.Authorization;
using Share.Utility;

namespace GrpcServerWithASPNet.Services
{
    [Authorize]
    public class UserService : User.UserBase
    {
        private readonly ILogger<UserService> _logger;
        private readonly JwtTokenHelper _jwtTokenHelper;
        public UserService(ILogger<UserService> logger)
        {
            _logger = logger;
            _jwtTokenHelper  = new JwtTokenHelper();
        }
        [AllowAnonymous]
        public override Task<LoginResponse> Login(LoginRequest request, ServerCallContext context)
        {
            var httpContext = context.GetHttpContext();
            return Task.FromResult(new LoginResponse
            {
                Result = true,
                AccessToken = _jwtTokenHelper.GenerateToken(request.Id)
            });
        }
        public override Task<AuthHelloResponse> AuthHello(Empty request, ServerCallContext context)
        {
            var httpContext = context.GetHttpContext();

            var nameClaim = httpContext.User?.Claims?.SingleOrDefault(c => c.Type == "Name");
            return Task.FromResult(new AuthHelloResponse
            {
                Message = $"Auth Success Hello {nameClaim.Value}",
                Result = true
            });
        }
    }
}

 

Program.cs

using GrpcServerWithASPNet.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Share.Models;
using Share.Utility;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

// Add services to the container.
builder.Services.AddGrpc();

var config = CustomConfigurationHelper.CreateConfigurationBuilder("./CommonConfiguration/Configuration.json");
config = CustomConfigurationHelper.CreateConfigurationBuilder("./Configuration/Configuration.json");
builder.Services.Configure<TokenManagement>(config.GetSection("TokenManagement"));

var token = config.GetSection("TokenManagement").Get<TokenManagement>();
var secret = Encoding.ASCII.GetBytes(token.Secret);

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
   
    x.SaveToken = true;
    x.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
        ValidIssuer = token.Issuer,
        ValidAudience = token.Audience,
        ValidateIssuer = false,
        ValidateAudience = false,
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero,

    };
});

builder.Services.AddAuthorization();

builder.Services.AddGrpcReflection();
builder.Services.AddCors(o => o.AddPolicy("AllowAll", builder =>
{
    builder.AllowAnyOrigin()
           .AllowAnyMethod()
           .AllowAnyHeader()
           .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding");
}));

var app = builder.Build();

if(app.Environment.IsDevelopment())
    app.MapGrpcReflectionService();


app.UseAuthentication();
app.UseAuthorization();

app.UseCors("AllowAll");
// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>().RequireCors("AllowAll");
app.MapGrpcService<UserService>().RequireCors("AllowAll");
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();

 

실행 방법

Login 을 먼저 실행해서 accessToken 을 얻어온다. 

Request Metadata 을 확인 하자

Name 에 Authorization 

Value 에 Bearer {accessToken}   

그리고 invoke 해보자

아래와 같이 실행되었다면 정상 실행 된것이다. 

만약 Request Metadata 에 아무것도 없이 실행 한다면 아래와 같이 나온다.

 

 

관련영상

https://youtu.be/12enD_aew68

 

 

 

반응형

'ASPNET > Grpc' 카테고리의 다른 글

gRPC - 로깅 및 진단  (0) 2022.05.26
gRPC - 인터셉터  (0) 2022.05.25
gRPC - Configuration  (0) 2022.05.23
grpc test - grpcui  (0) 2022.05.20
grpc test - grpcurl  (0) 2022.05.19