Last active
June 27, 2019 07:23
-
-
Save tompetko/8169b161375cd23618280dbcd00210d4 to your computer and use it in GitHub Desktop.
Portos API - NWS4 Authentication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.ObjectModel; | |
using System.Diagnostics; | |
using System.Globalization; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Security.Cryptography; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
using System.Web; | |
namespace NineDigit.mPOS.API.Client.Examples | |
{ | |
internal class ComputeSignatureResult | |
{ | |
public string Scheme { get; internal set; } | |
public string PublicApiKey { get; internal set; } | |
public string Signature { get; internal set; } | |
public string SignedHeaderNames { get; internal set; } | |
public string Nonce { get; internal set; } | |
public string Timestamp { get; internal set; } | |
} | |
public static class IEnumerableExtensions | |
{ | |
public static IReadOnlyDictionary<string, string> ToReadOnlyDictionary(this IEnumerable<KeyValuePair<string, string>> self) | |
{ | |
var result = self | |
.ToDictionary(i => i.Key, i => i.Value) | |
.ToReadOnlyDictionary(); | |
return result; | |
} | |
public static IReadOnlyDictionary<string, string> ToReadOnlyDictionary(this IDictionary<string, string> self) | |
{ | |
var result = new ReadOnlyDictionary<string, string>(self); | |
return result; | |
} | |
} | |
public static class HttpHeadersExtensions | |
{ | |
/// <summary> | |
/// Converts HTTP headers to dictionary with ordinal, ignore-case string key comparision. | |
/// </summary> | |
/// <param name="self"></param> | |
/// <returns></returns> | |
public static IDictionary<string, string> ToDictionary(this HttpHeaders self) | |
{ | |
var headers = self | |
.Select(i => CanonicalizeHttpHeader(i)) | |
.ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase); | |
return headers; | |
} | |
public static IReadOnlyDictionary<string, string> ToReadOnlyDictionary(this HttpHeaders self) | |
{ | |
var headers = self.ToDictionary(); | |
var result = headers.ToReadOnlyDictionary(); | |
return result; | |
} | |
private static KeyValuePair<string, string> CanonicalizeHttpHeader(KeyValuePair<string, IEnumerable<string>> httpHeader) | |
{ | |
var value = CanonicalizeHttpHeaderValue(httpHeader.Value); | |
var result = new KeyValuePair<string, string>(httpHeader.Key, value); | |
return result; | |
} | |
private static string CanonicalizeHttpHeaderValue(IEnumerable<string> httpHeaderValue) | |
{ | |
var result = string.Join(",", httpHeaderValue); | |
return result; | |
} | |
} | |
public static class ByteArrayExtensions | |
{ | |
/// <summary> | |
/// Helper to format a byte array into string | |
/// </summary> | |
/// <param name="data">The data blob to process</param> | |
/// <param name="lowercase">If true, returns hex digits in lower case form</param> | |
/// <returns>String version of the data</returns> | |
public static string ToHexString(this byte[] data, bool lowercase) | |
{ | |
StringBuilder sb = new StringBuilder(); | |
string result; | |
for (var i = 0; i < data.Length; i++) | |
{ | |
sb.Append(data[i].ToString(lowercase ? "x2" : "X2")); | |
} | |
result = sb.ToString(); | |
return result; | |
} | |
} | |
internal static class AuthenticationHeaderContextSerializer | |
{ | |
private const string AuthStringFormat = "{0} {1}"; | |
private const string AuthParamStringFormat = "Credential={0},SignedHeaders={1},Timestamp={2},Signature={3}"; | |
public static AuthenticationHeaderContext Deserialize(string content) | |
{ | |
var authParts = ParseFormat(AuthStringFormat, content); | |
if (authParts.Length != 2) | |
throw new FormatException(nameof(content)); | |
var authParamParts = ParseFormat( | |
AuthParamStringFormat, HttpUtility.UrlDecode(authParts[1])); | |
if (authParamParts.Length != 4) | |
throw new FormatException(nameof(content)); | |
var timestamp = HttpUtility.UrlDecode(authParamParts[2]); | |
var signedHeaders = HttpUtility.UrlDecode(authParamParts[1]); | |
var data = new AuthenticationHeaderContext() | |
{ | |
Scheme = authParts[0], | |
Credential = authParamParts[0], | |
SignedHeaders = signedHeaders, | |
Timestamp = timestamp, | |
Signature = authParamParts[3], | |
}; | |
return data; | |
} | |
public static string Serialize(AuthenticationHeaderContext data) | |
{ | |
if (data == null) | |
throw new ArgumentNullException(nameof(data)); | |
var timestamp = HttpUtility.UrlEncode(data.Timestamp); | |
var signedHeaders = HttpUtility.UrlEncode(data.SignedHeaders); | |
var authorizationParam = HttpUtility.UrlEncode( | |
string.Format(AuthParamStringFormat, | |
data.Credential, | |
signedHeaders, | |
timestamp, | |
data.Signature)); | |
var authorization = string.Format(AuthStringFormat, data.Scheme, authorizationParam); | |
return authorization; | |
} | |
private static string[] ParseFormat(string template, string str) | |
{ | |
string pattern = "^" + Regex.Replace(template, @"\{[0-9]+\}", "(.*?)") + "$"; | |
Regex r = new Regex(pattern); | |
Match m = r.Match(str); | |
List<string> ret = new List<string>(); | |
for (int i = 1; i < m.Groups.Count; i++) | |
ret.Add(m.Groups[i].Value); | |
return ret.ToArray(); | |
} | |
} | |
/// <summary> | |
/// Various Http helper routines | |
/// </summary> | |
public static class HttpHelper | |
{ | |
/// <summary> | |
/// Helper routine to url encode canonicalized header names and values for safe | |
/// inclusion in the presigned url. | |
/// </summary> | |
/// <param name="data">The string to encode</param> | |
/// <param name="isPath">Whether the string is a URL path or not</param> | |
/// <returns>The encoded string</returns> | |
public static string UrlEncode(string data, bool isPath = false) | |
{ | |
// The Set of accepted and valid Url characters per RFC3986. Characters outside of this set will be encoded. | |
const string validUrlCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; | |
var encoded = new StringBuilder(data.Length * 2); | |
string unreservedChars = String.Concat(validUrlCharacters, (isPath ? "/:" : "")); | |
foreach (char symbol in Encoding.UTF8.GetBytes(data)) | |
{ | |
if (unreservedChars.IndexOf(symbol) != -1) | |
encoded.Append(symbol); | |
else | |
encoded.Append("%").Append(String.Format("{0:X2}", (int)symbol)); | |
} | |
return encoded.ToString(); | |
} | |
} | |
public sealed class AuthenticationHeaderContext | |
{ | |
public AuthenticationHeaderContext() | |
{ } | |
public string Scheme { get; internal set; } | |
public string Credential { get; internal set; } | |
public string Signature { get; internal set; } | |
public string SignedHeaders { get; internal set; } | |
/// <summary> | |
/// ISO format date time stamp | |
/// </summary> | |
public string Timestamp { get; internal set; } | |
public DateTime GetUtcDateTime() | |
{ | |
return SignerForAuthorizationHeader.ParseUtcDateTime(this.Timestamp); | |
} | |
public override string ToString() | |
{ | |
return AuthenticationHeaderContextSerializer.Serialize(this); | |
} | |
public static AuthenticationHeaderContext Parse(string authHeader) | |
{ | |
return AuthenticationHeaderContextSerializer.Deserialize(authHeader); | |
} | |
} | |
public abstract class SignerBase | |
{ | |
protected const string SCHEME = "NWS4"; | |
protected const string ALGORITHM = "HMAC-SHA256"; | |
// SHA256 hash of an empty request body | |
protected const string EmptyBodySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; | |
// the name of the keyed hash algorithm used in signing | |
protected const string HMACSHA256 = "HMACSHA256"; | |
// algorithm used to hash the canonical request that is supplied to | |
// the signature computation | |
protected static readonly HashAlgorithm CanonicalRequestHashAlgorithm = HashAlgorithm.Create("SHA-256"); | |
// format strings for the date/time and date stamps required during signing | |
protected const string ISO8601Format = "yyyy-MM-ddTHH:mm:ss.fffffffK"; | |
// format strings for the date/time and date stamps required during signing | |
protected const string DateFormat = "yyyy-MM-dd"; | |
// request canonicalization requires multiple whitespace compression | |
private static readonly Regex CompressWhitespaceRegex = new Regex("\\s+"); | |
protected static string FormatDateTime(DateTime dateTimeStamp) | |
{ | |
return dateTimeStamp.ToString(ISO8601Format, CultureInfo.InvariantCulture); | |
} | |
protected static string FormatDate(DateTime dateTimeStamp) | |
{ | |
return dateTimeStamp.ToString(DateFormat, CultureInfo.InvariantCulture); | |
} | |
protected static DateTime ParseDateTime(string dateTime) | |
{ | |
return DateTime.ParseExact(dateTime, ISO8601Format, CultureInfo.InvariantCulture); | |
} | |
public static string GetSchemeName() | |
{ | |
return $"{SCHEME}-{ALGORITHM}"; | |
} | |
/// <summary> | |
/// Returns the canonical collection of header names that will be included in | |
/// the signature. For NWS4, all header names must be included in the process | |
/// in sorted canonicalized order. | |
/// </summary> | |
/// <param name="headers"> | |
/// The set of header names and values that will be sent with the request | |
/// </param> | |
/// <returns> | |
/// The set of header names canonicalized to a flattened, ;-delimited string | |
/// </returns> | |
protected static string CanonicalizeHeaderNames(IReadOnlyDictionary<string, string> headers) | |
{ | |
var headersToSign = GetHeaderNamesToSign(headers); | |
var result = CanonicalizeHeaderNames(headersToSign); | |
return result; | |
} | |
protected static string[] GetHeaderNamesToSign(IReadOnlyDictionary<string, string> headers) | |
{ | |
if (headers == null) | |
throw new ArgumentNullException(nameof(headers)); | |
var headersToSign = new List<string>(headers.Keys); | |
headersToSign.Sort(StringComparer.OrdinalIgnoreCase); | |
var result = headersToSign.ToArray(); | |
return result; | |
} | |
/// <summary> | |
/// Returns the canonical collection of header names that will be included in | |
/// the signature. For NWS1, all header names must be included in the process | |
/// in sorted canonicalized order. | |
/// </summary> | |
/// <param name="headers"> | |
/// The set of header names and values that will be sent with the request | |
/// </param> | |
/// <returns> | |
/// The set of header names canonicalized to a flattened, ;-delimited string | |
/// </returns> | |
protected static string CanonicalizeHeaderNames(string[] headerNames) | |
{ | |
if (headerNames == null) | |
throw new ArgumentNullException(nameof(headerNames)); | |
var names = headerNames.ToList(); | |
names.Sort(StringComparer.OrdinalIgnoreCase); | |
var sb = new StringBuilder(); | |
foreach (var header in names) | |
{ | |
if (sb.Length > 0) | |
sb.Append(";"); | |
sb.Append(header.ToLower()); | |
} | |
return sb.ToString(); | |
} | |
protected static string[] ParseHeaderNames(string canonicalizedHeaderNames) | |
{ | |
if (canonicalizedHeaderNames == null) | |
throw new ArgumentNullException(nameof(canonicalizedHeaderNames)); | |
return canonicalizedHeaderNames.Length == 0 ? | |
Array.Empty<string>() : | |
canonicalizedHeaderNames.Split(';'); | |
} | |
protected static string CanonicalizeQueryParameters(string queryParameters) | |
{ | |
var canonicalizedQueryParameters = string.Empty; | |
if (!string.IsNullOrEmpty(queryParameters)) | |
{ | |
var paramDictionary = queryParameters.Split('&').Select(p => p.Split('=')) | |
.ToDictionary(nameval => nameval[0], | |
nameval => nameval.Length > 1 | |
? nameval[1] : ""); | |
canonicalizedQueryParameters = CanonicalizeQueryParameters(paramDictionary); | |
} | |
return canonicalizedQueryParameters; | |
} | |
protected static string CanonicalizeQueryParameters(IReadOnlyDictionary<string, string> queryParameters) | |
{ | |
if (queryParameters == null) | |
throw new ArgumentNullException(nameof(queryParameters)); | |
var sb = new StringBuilder(); | |
var paramKeys = new List<string>(queryParameters.Keys); | |
paramKeys.Sort(StringComparer.Ordinal); | |
foreach (var p in paramKeys) | |
{ | |
if (sb.Length > 0) | |
sb.Append("&"); | |
sb.AppendFormat("{0}={1}", p, queryParameters[p]); | |
} | |
var canonicalizedQueryParameters = sb.ToString(); | |
return canonicalizedQueryParameters; | |
} | |
/// <summary> | |
/// Computes the canonical headers with values for the request. | |
/// For NWS4, all headers must be included in the signing process. | |
/// </summary> | |
/// <param name="headers">The set of headers to be encoded</param> | |
/// <returns>Canonicalized string of headers with values</returns> | |
public static string CanonicalizeHeaders(IReadOnlyDictionary<string, string> headers) | |
{ | |
if (headers == null || headers.Count == 0) | |
return string.Empty; | |
// step1: sort the headers into lower-case format; we create a new | |
// map to ensure we can do a subsequent key lookup using a lower-case | |
// key regardless of how 'headers' was created. | |
var sortedHeaderMap = new SortedDictionary<string, string>(); | |
foreach (var header in headers.Keys) | |
{ | |
sortedHeaderMap.Add(header.ToLower(), headers[header]); | |
} | |
// step2: form the canonical header:value entries in sorted order. | |
// Multiple white spaces in the values should be compressed to a single | |
// space. | |
var sb = new StringBuilder(); | |
foreach (var header in sortedHeaderMap.Keys) | |
{ | |
var headerValue = CompressWhitespaceRegex.Replace(sortedHeaderMap[header], " "); | |
sb.AppendFormat("{0}:{1}\n", header, headerValue.Trim()); | |
} | |
return sb.ToString(); | |
} | |
/// <summary> | |
/// Returns the canonical request string to go into the signer process; this | |
/// consists of several canonical sub-parts. | |
/// </summary> | |
/// <param name="endpointUri"></param> | |
/// <param name="httpMethod"></param> | |
/// <param name="queryParameters"></param> | |
/// <param name="canonicalizedHeaderNames"> | |
/// The set of header names to be included in the signature, formatted as a flattened, ;-delimited string | |
/// </param> | |
/// <param name="canonicalizedHeaders"> | |
/// </param> | |
/// <param name="bodyHash"> | |
/// Precomputed SHA256 hash of the request body content. For chunked encoding this | |
/// should be the fixed string ''. | |
/// </param> | |
/// <returns>String representing the canonicalized request for signing</returns> | |
public static string CanonicalizeRequest(Uri endpointUri, | |
string httpMethod, | |
string queryParameters, | |
string canonicalizedHeaderNames, | |
string canonicalizedHeaders, | |
string bodyHash) | |
{ | |
var canonicalRequest = new StringBuilder(); | |
canonicalRequest.AppendFormat("{0}\n", httpMethod); | |
canonicalRequest.AppendFormat("{0}\n", CanonicalResourcePath(endpointUri)); | |
canonicalRequest.AppendFormat("{0}\n", queryParameters); | |
canonicalRequest.AppendFormat("{0}\n", canonicalizedHeaders); | |
canonicalRequest.AppendFormat("{0}\n", canonicalizedHeaderNames); | |
canonicalRequest.Append(bodyHash); | |
return canonicalRequest.ToString(); | |
} | |
/// <summary> | |
/// Returns the canonicalized resource path for the service endpoint | |
/// </summary> | |
/// <param name="endpointUri">Endpoint to the service/resource</param> | |
/// <returns>Canonicalized resource path for the endpoint</returns> | |
private static string CanonicalResourcePath(Uri endpointUri) | |
{ | |
if (string.IsNullOrEmpty(endpointUri.AbsolutePath)) | |
return "/"; | |
// encode the path per RFC3986 | |
//HttpUtility.UrlEncode(endpointUri.AbsolutePath, ) | |
return HttpHelper.UrlEncode(endpointUri.AbsolutePath, true); | |
} | |
/// <summary> | |
/// Compute and return the multi-stage signing key for the request. | |
/// </summary> | |
/// <param name="algorithm">Hashing algorithm to use</param> | |
/// <param name="secretAccessKey">The clear-text NWS secret key</param> | |
/// <param name="date">Date of the request, in ISO format</param> | |
/// <returns>Computed signing key</returns> | |
protected static byte[] DeriveSigningKey(string algorithm, string secretAccessKey, string date) | |
{ | |
const string ksecretPrefix = SCHEME; | |
char[] ksecret = null; | |
byte[] result; | |
ksecret = (ksecretPrefix + secretAccessKey).ToCharArray(); | |
result = ComputeKeyedHash(algorithm, Encoding.UTF8.GetBytes(ksecret), Encoding.UTF8.GetBytes(date)); | |
return result; | |
} | |
/// <summary> | |
/// Compute and return the hash of a data blob using the specified algorithm | |
/// and key | |
/// </summary> | |
/// <param name="algorithm">Algorithm to use for hashing</param> | |
/// <param name="key">Hash key</param> | |
/// <param name="data">Data blob</param> | |
/// <returns>Hash of the data</returns> | |
private static byte[] ComputeKeyedHash(string algorithm, byte[] key, byte[] data) | |
{ | |
var kha = KeyedHashAlgorithm.Create(algorithm); | |
kha.Key = key; | |
return kha.ComputeHash(data); | |
} | |
protected static string ComputeBodyHash(byte[] content) | |
{ | |
var bodyHash = EmptyBodySHA256; | |
var isContentEmpty = content == null || content.Length == 0; | |
if (!isContentEmpty) | |
{ | |
// precompute hash of the body content | |
var contentHash = CanonicalRequestHashAlgorithm.ComputeHash(content); | |
bodyHash = contentHash.ToHexString(true); | |
} | |
return bodyHash; | |
} | |
protected static IDictionary<string, string> GetHeaders(HttpHeaders headers, string[] headerNames) | |
{ | |
if (headers == null) | |
throw new ArgumentNullException(nameof(headers)); | |
var httpHeaders = headers.ToDictionary(); | |
var result = GetHeaders(httpHeaders, headerNames); | |
return result; | |
} | |
protected static IDictionary<string, string> GetHeaders(IDictionary<string, string> headers, string[] headerNames) | |
{ | |
if (headers == null) | |
throw new ArgumentNullException(nameof(headers)); | |
if (headerNames == null) | |
throw new ArgumentNullException(nameof(headerNames)); | |
headerNames = headerNames.Distinct().ToArray(); | |
var result = new Dictionary<string, string>(); | |
for (var i = 0; i < headerNames.Length; i++) | |
{ | |
var headerName = headerNames[i]; | |
var header = headers.FirstOrDefault(h => string.Compare(h.Key, headerName, ignoreCase: true) == 0); | |
if (header.Equals(new KeyValuePair<string, string>())) | |
{ | |
throw new InvalidOperationException($"Header key '{headerName}' does not exist."); | |
} | |
result.Add(headerName, header.Value); | |
} | |
return result; | |
} | |
} | |
public class SignerForAuthorizationHeader : SignerBase | |
{ | |
const string X_ND_HEADER_KEY_NAME = "x-nd-date"; | |
const string X_ND_CONTENT_SHA256_KEY_NAME = "x-nd-content-sha256"; | |
public async Task SignRequestAsync( | |
HttpRequestMessage request, | |
string accessKey, | |
string privateKey) | |
{ | |
if (request == null) | |
throw new ArgumentNullException(nameof(request)); | |
DateTime dateTime = DateTime.UtcNow; | |
String dateTimeStamp = FormatDateTime(dateTime); | |
HttpContent content = request.Content; | |
byte[] body = content != null ? | |
await content.ReadAsByteArrayAsync() : Array.Empty<byte>(); | |
String bodyHash = ComputeBodyHash(body); | |
// update the headers with required 'x-nd-content-sha256', 'x-nd-date' and 'host' values | |
request.Headers.TryAddWithoutValidation(X_ND_HEADER_KEY_NAME, dateTimeStamp); | |
request.Headers.TryAddWithoutValidation(X_ND_CONTENT_SHA256_KEY_NAME, bodyHash); | |
if (string.IsNullOrEmpty(request.Headers.Host)) | |
{ | |
var hostHeader = request.RequestUri.Host; | |
if (!request.RequestUri.IsDefaultPort) | |
hostHeader += ":" + request.RequestUri.Port; | |
request.Headers.Add(nameof(HttpRequestHeaders.Host), hostHeader); | |
} | |
Uri requestUri = request.RequestUri; | |
string httpMethod = request.Method.Method; | |
IReadOnlyDictionary<string, string> headers = request.Headers.ToReadOnlyDictionary(); | |
string header = GetAuthenticationHeader( | |
requestUri, httpMethod, headers, bodyHash, accessKey, privateKey, dateTime); | |
AuthenticationHeaderValue authHeader = AuthenticationHeaderValue.Parse(header); | |
request.Headers.Authorization = authHeader; | |
} | |
private string GetAuthenticationHeader( | |
Uri requestUri, | |
string httpMethod, | |
IReadOnlyDictionary<string, string> headers, | |
string bodyHash, | |
string accessKey, | |
string privateKey, | |
DateTime dateTime) | |
{ | |
var result = ComputeSignatureInternal( | |
requestUri, httpMethod, headers, | |
bodyHash, accessKey, privateKey, dateTime); | |
var context = new AuthenticationHeaderContext() | |
{ | |
Scheme = result.Scheme, | |
Credential = result.PublicApiKey, | |
Signature = result.Signature, | |
SignedHeaders = result.SignedHeaderNames, | |
Timestamp = result.Timestamp, | |
}; | |
return context.ToString(); | |
} | |
private ComputeSignatureResult ComputeSignatureInternal( | |
Uri requestUri, | |
string httpMethod, | |
IReadOnlyDictionary<string, string> headers, | |
string bodyHash, | |
string accessKey, | |
string privateKey, | |
DateTime dateTime) | |
{ | |
if (requestUri == null) | |
throw new ArgumentNullException(nameof(requestUri)); | |
string queryParameters = requestUri.Query; | |
// TODO: Validate required Headers like Host | |
// canonicalize the headers; we need the set of header names as well as the | |
// names and values to go into the signature process | |
string canonicalizedHeaderNames = CanonicalizeHeaderNames(headers); | |
string canonicalizedHeaders = CanonicalizeHeaders(headers); | |
// if any query string parameters have been supplied, canonicalize them | |
string canonicalizedQueryParameters = CanonicalizeQueryParameters(queryParameters); | |
// canonicalize the various components of the request | |
string canonicalRequest = CanonicalizeRequest(requestUri, httpMethod, | |
canonicalizedQueryParameters, canonicalizedHeaderNames, | |
canonicalizedHeaders, bodyHash); | |
// Compute signature | |
// first get the date and time for the subsequent request, and convert | |
// to ISO 8601 format for use in signature generation | |
string dateTimeStamp = FormatDateTime(dateTime); | |
string dateStamp = FormatDate(dateTime); | |
// generate a hash of the canonical request, to go into signature computation | |
var canonicalRequestHashBytes = CanonicalRequestHashAlgorithm | |
.ComputeHash(Encoding.UTF8.GetBytes(canonicalRequest)); | |
// construct the string to be signed | |
var stringToSign = new StringBuilder(); | |
stringToSign.AppendFormat("{0}-{1}\n{2}\n{3}\n{4}", | |
SCHEME, ALGORITHM, dateTimeStamp, accessKey, canonicalRequestHashBytes.ToHexString(true)); | |
// compute the signing key | |
var kha = KeyedHashAlgorithm.Create(HMACSHA256); | |
kha.Key = DeriveSigningKey(HMACSHA256, privateKey, dateStamp); | |
// compute the NWS4 signature and return it | |
var signature = kha.ComputeHash(Encoding.UTF8.GetBytes(stringToSign.ToString())); | |
var signatureString = signature.ToHexString(true); | |
Debug.WriteLine("\nCanonicalRequest:\n{0}", canonicalRequest); | |
Debug.WriteLine("\nStringToSign:\n{0}", stringToSign); | |
Debug.WriteLine("\nSignature:\n{0}", signatureString); | |
// | |
var schemeName = GetSchemeName(); | |
var result = new ComputeSignatureResult() | |
{ | |
Scheme = schemeName, | |
PublicApiKey = accessKey, | |
Signature = signatureString, | |
SignedHeaderNames = canonicalizedHeaderNames, | |
Timestamp = dateTimeStamp | |
}; | |
return result; | |
} | |
public bool CanValidateSignature(string authHeaderValue) | |
{ | |
var canParse = AuthenticationHeaderValue.TryParse( | |
authHeaderValue, out AuthenticationHeaderValue header); | |
return canParse ? | |
this.CanValidateSignature(header) : | |
false; | |
} | |
public bool CanValidateSignature(AuthenticationHeaderValue header) | |
{ | |
if (header == null) | |
throw new ArgumentNullException(nameof(header)); | |
var nws2Scheme = GetSchemeName(); | |
var scheme = header.Scheme; | |
return scheme == nws2Scheme; | |
} | |
public Task ValidateSignatureAsync( | |
HttpRequestMessage request, string privateKey) | |
{ | |
if (request == null) | |
throw new ArgumentNullException(nameof(request)); | |
var authHeaderValue = request.Headers.Authorization?.ToString(); | |
if (string.IsNullOrEmpty(authHeaderValue)) | |
throw new ArgumentException("No authorization header.", nameof(request)); | |
var headerContext = AuthenticationHeaderContext.Parse(authHeaderValue); | |
var result = this.ValidateSignatureAsync(request, headerContext, privateKey); | |
return result; | |
} | |
public async Task ValidateSignatureAsync( | |
HttpRequestMessage request, AuthenticationHeaderContext authHeaderContext, string privateKey) | |
{ | |
if (request == null) | |
throw new ArgumentNullException(nameof(request)); | |
if (authHeaderContext == null) | |
throw new ArgumentNullException(nameof(authHeaderContext)); | |
string scheme = GetSchemeName(); | |
if (authHeaderContext.Scheme != scheme) | |
throw new ArgumentException("Authorization header is not of NWS2 header type.", nameof(request)); | |
Uri requestUri = request.RequestUri; | |
string httpMethod = request.Method.Method; | |
string[] signedHeaderNames = ParseHeaderNames(authHeaderContext.SignedHeaders); | |
IReadOnlyDictionary<string, string> signedHeaders = | |
GetHeaders(request.Headers, signedHeaderNames).ToReadOnlyDictionary(); | |
byte[] content = request.Content != null ? | |
await request.Content.ReadAsByteArrayAsync() : null; | |
String bodyHash = ComputeBodyHash(content); | |
DateTime dateTime = ParseUtcDateTime(authHeaderContext.Timestamp); | |
ComputeSignatureResult result = ComputeSignatureInternal( | |
request.RequestUri, httpMethod, signedHeaders, | |
bodyHash, authHeaderContext.Credential, privateKey, dateTime); | |
if (result.Signature != authHeaderContext.Signature) | |
throw new InvalidOperationException("Invalid signature."); | |
} | |
internal static DateTime ParseUtcDateTime(string dateTimeStamp) | |
{ | |
return ParseDateTime(dateTimeStamp).ToUniversalTime(); | |
} | |
} | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
var publicApiKey = "61d6b0acdcf45ea5cf0fcf9c1ce3e82fb8c06b91"; | |
var privateApiKey = "2b643d8aace5a6ba16669d36778df2ad540f7ac9616a1f3d4b6a6ffbf90b7c39"; | |
var url = "http://localhost:3000/api/stocks"; | |
var request = new HttpRequestMessage(HttpMethod.Get, url); | |
var nws4Signer = new SignerForAuthorizationHeader(); | |
nws4Signer.SignRequestAsync(request, publicApiKey, privateApiKey).Wait(); | |
var httpClient = new HttpClient(); | |
var response = httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead).Result; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment