본문 바로가기
프로그래밍

[c# Asp.net] Web Api 보안을 위한 HMAC 인증

by 도장깨기 2021. 6. 22.
728x90
반응형

오늘은 Web Api 보안 구성을 위해 사용했던 HMAC Authentication(인증)에 대해 간략히 샘플을 

보여드릴텐데요.

 

오늘 소개하는 내용은 참고 문서를 참고해 간략히 요약 하여 작성하였습니다.

제가 참고한 문서는 글 가장 하단에 링크 첨부하도록 하겠습니다!

 

먼저, 간단히 HMAC란 무엇인가에 대해 알아보면

데이터 송수신에 관련된 두 당사자(ex. 클라이언트, 서버) 간에 고유 비밀 키와 함께 해시 함수를 사용해 메세지 인증 코드를 생성하는 메커니즘 입니다. 

주로 HMAC 같은 경우 발신자(클라이언트)의 무결성,신뢰성 및 신원 확인을 위한 용도로 사용되고 있습니다.

 

서버는 클라이언트에게 공개 앱ID와 시크릿 KEY를 제공하고, 클라이언트는 앱ID, URL, 요청 내용, HTTP 메서드, 타임스탬프 및 임시값등을 포함한 데이터를 해싱하여 서버로 보냅니다. 그 내용을 서버측에서 비교하여 올바른 요청인지 확인하는 흐름의 방식입니다.

 

이제부터 클라이언트와 서버측 샘플을 보여드리겠습니다.

 

클라이언트 측 

Step 1. 앱 ID 와 API 키 생성 

using (var cryptoProvider = new RNGCryptoServiceProvider())
	{
    	Guid AppID = Guid.NewGuid();
		byte[] secretKeyByteArray = new byte[32]; //256 bit
		cryptoProvider.GetBytes(secretKeyByteArray);
		var APIKey = Convert.ToBase64String(secretKeyByteArray);
	}

먼저 HMAC 인증에 사용할 APP ID(GUID), Api key를 생성합니다. 

 

Step 2. Nuget 패키지 설치

그리고 HTTP Clinet 사용을 위해 Mircosoft.AspNet.WebApi.Client 라는 Nuget 패키지를 설치해 줍니다. 

만약 이미 설치가 되어있다면 Pass

 

 

Step 3. HTTPClient를 이용한 API 호출

이제 HTTPClient를 이용해 API를 호출 해볼텐데요

		private async Task<int> RunAsync()
		{
			Console.WriteLine("Calling the back-end API");
			string apiBaseAddress = "API_ADDRESS";
			HMACDelegatingHandler customDelegatingHandler = new HMACDelegatingHandler();
			HttpClient client = HttpClientFactory.Create(customDelegatingHandler);
			var sample = new Sample()
			{
            	sample = "sample"
			};

			HttpResponseMessage response = await client.PostAsJsonAsync(apiBaseAddress + "api/Sample", sample);
			if (response.IsSuccessStatusCode)
			{
				string responseString = await response.Content.ReadAsStringAsync();
				Console.WriteLine(responseString);
				Console.WriteLine("HTTP Status: {0}, Reason {1}. Press ENTER to exit", response.StatusCode, response.ReasonPhrase);
			}
			else
			{
				Console.WriteLine("Failed to call the API. HTTP Status: {0}, Reason {1}", response.StatusCode, response.ReasonPhrase);
			}

			return 0;
		}

(HMACDelegatingHandler의 경우 아래쪽에 추가)

HMAC 인증을 통한 API 호출 샘플입니다.

apiBaseAddress에는 호출할 api 주소를 입력해주시고 

인증은 이 다음에 설명할 HMACDelegatingHandler에서 인증하게됩니다.

 

그리고 PostAsJsonAsync로 api를 호출을 하시면 됩니다. 

 

4. HTTPClient를 사용한 요청 처리기 구현

HMACDelegatingHandler

public class HMACDelegatingHandler : DelegatingHandler
		{
			private string APPId = "APP_ID";
			private string APIKey = "API_KEY";
			protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
			{
				HttpResponseMessage response = null;
				string requestContentBase64String = string.Empty;

				string requestUri = HttpUtility.UrlEncode(request.RequestUri.AbsoluteUri.ToLower());

				string requestHttpMethod = request.Method.Method;

				DateTime epochStart = new DateTime(1970, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc);
				TimeSpan timeSpan = DateTime.UtcNow - epochStart;
				string requestTimeStamp = Convert.ToUInt64(timeSpan.TotalSeconds).ToString();

				string nonce = Guid.NewGuid().ToString("N");
				
				if (request.Content != null)
				{
					byte[] content = await request.Content.ReadAsByteArrayAsync();
					MD5 md5 = MD5.Create();
					byte[] requestContentHash = md5.ComputeHash(content);
					requestContentBase64String = Convert.ToBase64String(requestContentHash);
				}
			
				string signatureRawData = String.Format("{0}{1}{2}{3}{4}{5}", APPId, requestHttpMethod, requestUri, requestTimeStamp, nonce, requestContentBase64String);

				var secretKeyByteArray = Convert.FromBase64String(APIKey);

				byte[] signature = Encoding.UTF8.GetBytes(signatureRawData);

				using (HMACSHA256 hmac = new HMACSHA256(secretKeyByteArray))
				{
					byte[] signatureBytes = hmac.ComputeHash(signature);
					string requestSignatureBase64String = Convert.ToBase64String(signatureBytes);
					
					request.Headers.Authorization = new AuthenticationHeaderValue("amx", string.Format("{0}:{1}:{2}:{3}", APPId, requestSignatureBase64String, nonce, requestTimeStamp));
				}
				response = await base.SendAsync(request, cancellationToken);
				return response;
			}
		}

위의 코드를 이용해 APPID, HttpMethod, RequestUri, RequsetTimeStamp, nonce, requestContentBase64Strimg을 연결해 서명 데이터를 구축했고, 이 데이터는 SHA256 알고리즘으로 해시됩니다.

 

마지막으로 API키를 이용해 해싱알고리즘을 적용한 후 BASE64로 설정하여 결합하고 

앞쪽에 amx(변경가능)라는 사용자 지정체계를 사용해 요청에 대한 Authorization  문자열을 설정합니다. 

 

참고로 nonce와 타임스태프는 API 재생공격(Replay Attack)으로 부터 보호가 된다고하네요.

(실제로 테스트하여 보호가 되는것을 확인 했습니다.)

 

이와 같이 클라이언트 호출 방법을 알았다면

 

이제 API 서버측을 살펴볼텐데요. 

 

서버측

Step1. HMAC 인증 필터 구축 & isValidRequest 매서드 구현

public class HMACAuthenticationAttribute : Attribute, IAuthenticationFilter
	{
		private static Dictionary<string, string> allowedApps = new Dictionary<string, string>();
		private readonly UInt64 requestMaxAgeInSeconds = 300; //Means 5 min
		private readonly string authenticationScheme = "amx";
		public HMACAuthenticationAttribute()
		{
			if (allowedApps.Count == 0)
			{
				allowedApps.Add("APP_ID", "API_KEY");				
			}
		}
		public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
		{
			var req = context.Request;
			if (req.Headers.Authorization != null && authenticationScheme.Equals(req.Headers.Authorization.Scheme, StringComparison.OrdinalIgnoreCase))
			{
				var rawAuthzHeader = req.Headers.Authorization.Parameter;
				var autherizationHeaderArray = GetAutherizationHeaderValues(rawAuthzHeader);
				if (autherizationHeaderArray != null)
				{
					var APPId = autherizationHeaderArray[0];
					var incomingBase64Signature = autherizationHeaderArray[1];
					var nonce = autherizationHeaderArray[2];
					var requestTimeStamp = autherizationHeaderArray[3];
					var isValid = IsValidRequest(req, APPId, incomingBase64Signature, nonce, requestTimeStamp);
					if (isValid.Result)
					{
						var currentPrincipal = new GenericPrincipal(new GenericIdentity(APPId), null);
						context.Principal = currentPrincipal;
					}
					else
					{
						context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
					}
				}
				else
				{
					context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
				}
			}
			else
			{
				context.ErrorResult = new UnauthorizedResult(new AuthenticationHeaderValue[0], context.Request);
			}
			return Task.FromResult(0);
		}
		public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
		{
			context.Result = new ResultWithChallenge(context.Result);
			return Task.FromResult(0);
		}
		public bool AllowMultiple
		{
			get { return false; }
		}
		private string[] GetAutherizationHeaderValues(string rawAuthzHeader)
		{
			var credArray = rawAuthzHeader.Split(':');
			if (credArray.Length == 4)
			{
				return credArray;
			}
			else
			{
				return null;
			}
		}

		private async Task<bool> IsValidRequest(HttpRequestMessage req, string APPId, string incomingBase64Signature, string nonce, string requestTimeStamp)
		{
			string requestContentBase64String = "";
			string requestUri = HttpUtility.UrlEncode(req.RequestUri.AbsoluteUri.ToLower());
			string requestHttpMethod = req.Method.Method;
			if (!allowedApps.ContainsKey(APPId))
			{
				return false;
			}
			var sharedKey = allowedApps[APPId];
			if (isReplayRequest(nonce, requestTimeStamp))
			{
				return false;
			}
			byte[] hash = await ComputeHash(req.Content);
			if (hash != null)
			{
				requestContentBase64String = Convert.ToBase64String(hash);
			}
			string data = String.Format("{0}{1}{2}{3}{4}{5}", APPId, requestHttpMethod, requestUri, requestTimeStamp, nonce, requestContentBase64String);
			var secretKeyBytes = Convert.FromBase64String(sharedKey);
			byte[] signature = Encoding.UTF8.GetBytes(data);
			using (HMACSHA256 hmac = new HMACSHA256(secretKeyBytes))
			{
				byte[] signatureBytes = hmac.ComputeHash(signature);
				return (incomingBase64Signature.Equals(Convert.ToBase64String(signatureBytes), StringComparison.Ordinal));
			}
		}
		private bool isReplayRequest(string nonce, string requestTimeStamp)
		{
			if (System.Runtime.Caching.MemoryCache.Default.Contains(nonce))
			{
				return true;
			}
			DateTime epochStart = new DateTime(1970, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc);
			TimeSpan currentTs = DateTime.UtcNow - epochStart;
			var serverTotalSeconds = Convert.ToUInt64(currentTs.TotalSeconds);
			var requestTotalSeconds = Convert.ToUInt64(requestTimeStamp);
			if ((serverTotalSeconds - requestTotalSeconds) > requestMaxAgeInSeconds)
			{
				return true;
			}
			System.Runtime.Caching.MemoryCache.Default.Add(nonce, requestTimeStamp, DateTimeOffset.UtcNow.AddSeconds(requestMaxAgeInSeconds));
			return false;
		}
		private static async Task<byte[]> ComputeHash(HttpContent httpContent)
		{
			using (MD5 md5 = MD5.Create())
			{
				byte[] hash = null;
				var content = await httpContent.ReadAsByteArrayAsync();
				if (content.Length != 0)
				{
					hash = md5.ComputeHash(content);
				}
				return hash;
			}
		}
	}

 

Attribute에서 상속되고 IAuthenticationFilter인터페이스를 구현하는 HMACAuthenticationAttribute 라는 새 클래스를 추가 해주고, allowedApps라는 Dictionary에 APP_ID와 API_KEY값을 저장합니다.

 

그 후 isValidRequest라는 매서드를 구현합니다.

이름 그대로 해당 요청이 유효한지를 체크해 주는 부분입니다.

 

requestMaxAgeInSeconds(ex.300 => 5분) 설정으로 클라이언트 요청을 5분간 캐시 메모리에 저장합니다.

그렇게 되면 5분동안의 replay 요청은 replay attack으로 판단하여 거부합니다.

설정한 5분 이후에 대한 요청은 replay attack으로 간주 되지 않습니다. 

 

하지만 타임스탬프를 사용하였기때문에 서버시간과 요청시간을 비교합니다. 그래서 5분 이후에도 동일한 값으로는 요청이 허락되지 않는것으로 보입니다.

 

Step 2. 응답 

	public class ResultWithChallenge : IHttpActionResult
	{
		private readonly string authenticationScheme = "amx";
		private readonly IHttpActionResult next;
		public ResultWithChallenge(IHttpActionResult next)
		{
			this.next = next;
		}
		public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
		{
			var response = await next.ExecuteAsync(cancellationToken);
			if (response.StatusCode == HttpStatusCode.Unauthorized)
			{
				response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(authenticationScheme));
			}
			return response;
		}
	}

 

API 응답값을 받기 위한 코드 입니다. 

 

 

Step 3. API 엔드 포인트 보안 설정

마지막으로 API의 Controller에 보안 설정을 해야합니다. 

	[HMACAuthentication]
	public class SampleController : ApiController
	{

		/// <summary>
		/// Sample
		/// </summary>
		/// <param name="test"></param>
		/// <returns></returns>
		[Route("api/Sample/Test")]
		[HttpPost]
		public IHttpActionResult Test(Test test) 
		{


			try
			{
				// Sample
				return Ok();
			}
			catch (Exception ex)
			{
				return Ok();
			}
		}


	}

Api에 HMACAuthentication이라는 인증 필터 속성을 상단에 추가 해주면 구성이 완료됩니다.

 

기존에 Oauth 2.0 방식을 사용하고 있었는데 HMAC가 더 보안에 적합하다는 말을 듣고 새로 구성하면서 

공부해본 내용을 정리해보았습니다.

 

저도 공부중인 내용이기때문에 혹여나 잘못된내용이나 질문내용이 있으시다면 댓글로 남겨주시면 감사하겠습니다.

 

아무래도 제 글에서 부족한부분은 아래 링크를 참고 하시면 좋을것 같습니다.

 

감사합니다!

 

 

https://bitoftech.net/2014/12/15/secure-asp-net-web-api-using-api-key-authentication-hmac-authentication/

 

Secure ASP.NET Web API using API Key Authentication - HMAC Authentication - Bit of Technology

Tutorial shows how to secure ASP.NET Web API using API Key Authentication - HMAC Authentication and implement it using IAuthenticationFilter

bitoftech.net

 

 

 

 

 

 

 

 

 

728x90
반응형

댓글