• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / HashGate / 21171908792

20 Jan 2026 12:36PM UTC coverage: 75.643%. First build
21171908792

push

github

pwelter34
Add .NET Framework support and improve logging

118 of 188 branches covered (62.77%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 1 file covered. (66.67%)

323 of 395 relevant lines covered (81.77%)

12.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.47
/src/HashGate.AspNetCore/HmacAuthenticationHandler.cs
1
// Ignore Spelling: timestamp Hmac
2

3
using System.Security.Claims;
4
using System.Security.Cryptography;
5
using System.Text;
6
using System.Text.Encodings.Web;
7

8
using Microsoft.AspNetCore.Authentication;
9
using Microsoft.AspNetCore.Http;
10
using Microsoft.Extensions.Logging;
11
using Microsoft.Extensions.Options;
12

13
namespace HashGate.AspNetCore;
14

15
/// <summary>
16
/// Handles HMAC authentication for incoming HTTP requests.
17
/// </summary>
18
/// <remarks>
19
/// This handler validates the HMAC signature in the Authorization header, checks the timestamp for replay protection,
20
/// retrieves the client secret using <see cref="IHmacKeyProvider"/>, and authenticates the request if all checks pass.
21
/// </remarks>
22
public class HmacAuthenticationHandler : AuthenticationHandler<HmacAuthenticationSchemeOptions>
23
{
24
    private static readonly AuthenticateResult InvalidTimestampHeader = AuthenticateResult.Fail("Invalid timestamp header");
1✔
25
    private static readonly AuthenticateResult InvalidContentHashHeader = AuthenticateResult.Fail("Invalid content hash header");
1✔
26
    private static readonly AuthenticateResult InvalidClientName = AuthenticateResult.Fail("Invalid client name");
1✔
27
    private static readonly AuthenticateResult InvalidSignature = AuthenticateResult.Fail("Invalid signature");
1✔
28
    private static readonly AuthenticateResult AuthenticationError = AuthenticateResult.Fail("Authentication error");
1✔
29

30
    private readonly IHmacKeyProvider _keyProvider;
31

32
    /// <summary>
33
    /// Initializes a new instance of the <see cref="HmacAuthenticationHandler"/> class.
34
    /// </summary>
35
    /// <param name="options">The options monitor for <see cref="HmacAuthenticationSchemeOptions"/>.</param>
36
    /// <param name="logger">The logger factory.</param>
37
    /// <param name="encoder">The URL encoder.</param>
38
    /// <param name="keyProvider">The HMAC key provider used to retrieve client secrets.</param>
39
    public HmacAuthenticationHandler(
40
        IOptionsMonitor<HmacAuthenticationSchemeOptions> options,
41
        ILoggerFactory logger,
42
        UrlEncoder encoder,
43
        IHmacKeyProvider keyProvider)
44
        : base(options, logger, encoder)
10✔
45
    {
46
        _keyProvider = keyProvider;
10✔
47
    }
10✔
48

49
    /// <summary>
50
    /// Handles the authentication process for HMAC authentication.
51
    /// </summary>
52
    /// <returns>
53
    /// A <see cref="Task{AuthenticateResult}"/> representing the asynchronous authentication operation.
54
    /// </returns>
55
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
56
    {
57
        try
58
        {
59
            var authorizationHeader = Request.Headers.Authorization.ToString();
10✔
60

61
            // If no Authorization header is present, return no result
62
            if (string.IsNullOrEmpty(authorizationHeader))
10✔
63
                return AuthenticateResult.NoResult();
1✔
64

65
            var result = HmacHeaderParser.TryParse(authorizationHeader, true, out var hmacHeader);
9✔
66

67
            // not an HMAC Authorization header, return no result
68
            if (result == HmacHeaderError.InvalidSchema)
9✔
69
                return AuthenticateResult.NoResult();
1✔
70

71
            // invalid HMAC Authorization header format
72
            if (result != HmacHeaderError.None)
8!
73
            {
NEW
74
                Logger.LogWarning("Invalid Authorization header: {Error}", result);
×
75
                return AuthenticateResult.Fail($"Invalid Authorization header: {result}");
×
76
            }
77

78
            if (!ValidateTimestamp())
8✔
79
            {
80
                Logger.LogWarning("Invalid or expired timestamp");
2✔
81
                return InvalidTimestampHeader;
2✔
82
            }
83

84
            if (!await ValidateContentHash())
6✔
85
            {
86
                Logger.LogWarning("Invalid body content hash");
2✔
87
                return InvalidContentHashHeader;
2✔
88
            }
89

90
            var clientSecret = await _keyProvider
4✔
91
                .GetSecretAsync(hmacHeader.Client, Context.RequestAborted)
4✔
92
                .ConfigureAwait(false);
4✔
93

94
            if (string.IsNullOrEmpty(clientSecret))
4!
95
                return InvalidClientName;
×
96

97
            var headerValues = GetHeaderValues(hmacHeader.SignedHeaders);
4✔
98

99
            var stringToSign = HmacAuthenticationShared.CreateStringToSign(
4✔
100
                method: Request.Method,
4✔
101
                pathAndQuery: Request.Path + Request.QueryString,
4✔
102
                headerValues: headerValues);
4✔
103

104
            var expectedSignature = HmacAuthenticationShared.GenerateSignature(stringToSign, clientSecret);
4✔
105
            if (!HmacAuthenticationShared.FixedTimeEquals(expectedSignature, hmacHeader.Signature))
4✔
106
                return InvalidSignature;
1✔
107

108
            var identity = await _keyProvider
3✔
109
                .GenerateClaimsAsync(hmacHeader.Client, Scheme.Name, Context.RequestAborted)
3✔
110
                .ConfigureAwait(false);
3✔
111

112
            var principal = new ClaimsPrincipal(identity);
3✔
113
            var ticket = new AuthenticationTicket(principal, Scheme.Name);
3✔
114

115
            return AuthenticateResult.Success(ticket);
3✔
116
        }
117
        catch (Exception ex)
×
118
        {
119
            Logger.LogError(ex, "Error during HMAC authentication");
×
120
            return AuthenticationError;
×
121
        }
122
    }
10✔
123

124

125
    private async Task<bool> ValidateContentHash()
126
    {
127
        if (!Request.Headers.TryGetValue(HmacAuthenticationShared.ContentHashHeaderName, out var contentHashHeader))
6✔
128
            return false;
1✔
129

130
        var contentHash = contentHashHeader.ToString();
5✔
131
        if (string.IsNullOrEmpty(contentHash))
5!
132
            return false;
×
133

134
        var computedHash = await GenerateContentHash().ConfigureAwait(false);
5✔
135

136
        return HmacAuthenticationShared.FixedTimeEquals(computedHash, contentHash);
5✔
137
    }
6✔
138

139
    private bool ValidateTimestamp()
140
    {
141
        var timestampHeader = GetHeaderValue(HmacAuthenticationShared.TimeStampHeaderName);
8✔
142
        if (!long.TryParse(timestampHeader, out var timestamp))
8✔
143
            return false;
1✔
144

145
        var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestamp);
7✔
146
        var now = DateTimeOffset.UtcNow;
7✔
147
        var timeDifference = Math.Abs((now - requestTime).TotalMinutes);
7✔
148

149
        // Use configured tolerance from options
150
        return timeDifference <= Options.ToleranceWindow;
7✔
151
    }
152

153

154
    private async Task<string> GenerateContentHash()
155
    {
156
        // Ensure the request body can be read multiple times
157
        Request.EnableBuffering();
5✔
158

159
        // Return empty content hash if there is no body
160
        if (Request.ContentLength == 0 || Request.Body == Stream.Null)
5✔
161
            return HmacAuthenticationShared.EmptyContentHash;
2✔
162

163
        await using var memoryStream = new MemoryStream();
3✔
164
        await Request.BodyReader.CopyToAsync(memoryStream).ConfigureAwait(false);
3✔
165

166
        // Reset position after reading
167
        Request.Body.Position = 0;
3✔
168

169
        // If the body is empty after reading, return empty content hash
170
        if (memoryStream.Length == 0)
3!
171
            return HmacAuthenticationShared.EmptyContentHash;
×
172

173
        var hashBytes = SHA256.HashData(memoryStream.ToArray());
3✔
174

175
        // 32 bytes SHA256 -> 44 chars base64
176
        Span<char> base64 = stackalloc char[44];
3✔
177
        if (Convert.TryToBase64Chars(hashBytes, base64, out int charsWritten))
3!
178
            return new string(base64[..charsWritten]);
3✔
179

180
        // if stackalloc is not large enough (should not happen for SHA256)
181
        return Convert.ToBase64String(hashBytes);
✔
182
    }
5✔
183

184
    private string[] GetHeaderValues(IReadOnlyList<string> signedHeaders)
185
    {
186
        var headerValues = new string[signedHeaders.Count];
4✔
187

188
        for (var i = 0; i < signedHeaders.Count; i++)
32✔
189
            headerValues[i] = GetHeaderValue(signedHeaders[i]) ?? string.Empty;
12!
190

191
        return headerValues;
4✔
192
    }
193

194
    private string? GetHeaderValue(string headerName)
195
    {
196
        if (headerName.Equals(HmacAuthenticationShared.HostHeaderName, StringComparison.InvariantCultureIgnoreCase))
20✔
197
        {
198
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.HostHeaderName, out var hostValue))
4!
199
                return hostValue.ToString();
4✔
200

201
            return Request.Host.Value;
×
202
        }
203

204
        // Handle date headers specifically
205
        if (headerName.Equals(HmacAuthenticationShared.DateHeaderName, StringComparison.InvariantCultureIgnoreCase)
16!
206
            || headerName.Equals(HmacAuthenticationShared.DateOverrideHeaderName, StringComparison.InvariantCultureIgnoreCase))
16✔
207
        {
208
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.DateOverrideHeaderName, out var xDateValue))
×
209
                return xDateValue.ToString();
×
210

211
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.DateHeaderName, out var dateValue))
×
212
                return dateValue.ToString();
×
213

214
            return Request.Headers.Date.ToString();
×
215
        }
216

217
        // Handle content-type and content-length headers specifically
218
        if (headerName.Equals(HmacAuthenticationShared.ContentTypeHeaderName, StringComparison.InvariantCultureIgnoreCase))
16!
219
            return Request.ContentType?.ToString();
×
220

221
        if (headerName.Equals(HmacAuthenticationShared.ContentLengthHeaderName, StringComparison.InvariantCultureIgnoreCase))
16!
222
            return Request.ContentLength?.ToString();
×
223

224
        // For all other headers, try to get the value directly
225
        if (Request.Headers.TryGetValue(headerName, out var value))
16✔
226
            return value.ToString();
15✔
227

228
        return null;
1✔
229
    }
230
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc