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

loresoft / HashGate / 16778951366

06 Aug 2025 01:53PM UTC coverage: 82.946% (-0.09%) from 83.037%
16778951366

push

github

pwelter34
bug fix in HmacAuthenticationHandler

109 of 162 branches covered (67.28%)

Branch coverage included in aggregate %.

5 of 6 new or added lines in 1 file covered. (83.33%)

319 of 354 relevant lines covered (90.11%)

12.48 hits per line

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

77.55
/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 InvalidAuthorizationHeader = AuthenticateResult.Fail("Invalid Authorization header");
1✔
25
    private static readonly AuthenticateResult InvalidTimestampHeader = AuthenticateResult.Fail("Invalid timestamp header");
1✔
26
    private static readonly AuthenticateResult InvalidContentHashHeader = AuthenticateResult.Fail("Invalid content hash header");
1✔
27
    private static readonly AuthenticateResult InvalidClientName = AuthenticateResult.Fail("Invalid client name");
1✔
28
    private static readonly AuthenticateResult InvalidSignature = AuthenticateResult.Fail("Invalid signature");
1✔
29
    private static readonly AuthenticateResult AuthenticationError = AuthenticateResult.Fail("Authentication error");
1✔
30

31
    private readonly IHmacKeyProvider _keyProvider;
32

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

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

64
            var result = HmacHeaderParser.TryParse(authorizationHeader, true, out var hmacHeader);
9✔
65
            if (result != HmacHeaderError.None)
9✔
66
                return AuthenticateResult.Fail($"Invalid Authorization header: {result}");
1✔
67

68
            if (!ValidateTimestamp())
8✔
69
                return InvalidTimestampHeader;
2✔
70

71
            if (!await ValidateContentHash())
6✔
72
                return InvalidContentHashHeader;
2✔
73

74
            var clientSecret = await _keyProvider.GetSecretAsync(hmacHeader.Client);
4✔
75
            if (string.IsNullOrEmpty(clientSecret))
4!
76
                return InvalidClientName;
×
77

78
            var headerValues = GetHeaderValues(hmacHeader.SignedHeaders);
4✔
79

80
            var stringToSign = HmacAuthenticationShared.CreateStringToSign(
4✔
81
                method: Request.Method,
4✔
82
                pathAndQuery: Request.Path + Request.QueryString,
4✔
83
                headerValues: headerValues);
4✔
84

85
            var expectedSignature = HmacAuthenticationShared.GenerateSignature(stringToSign, clientSecret);
4✔
86
            if (!HmacAuthenticationShared.FixedTimeEquals(expectedSignature, hmacHeader.Signature))
4✔
87
                return InvalidSignature;
1✔
88

89
            var claims = new[]
3✔
90
            {
3✔
91
                new Claim(ClaimTypes.Name, hmacHeader.Client),
3✔
92
            };
3✔
93

94
            var identity = new ClaimsIdentity(claims, Scheme.Name);
3✔
95
            var principal = new ClaimsPrincipal(identity);
3✔
96
            var ticket = new AuthenticationTicket(principal, Scheme.Name);
3✔
97

98
            return AuthenticateResult.Success(ticket);
3✔
99
        }
100
        catch (Exception ex)
×
101
        {
102
            Logger.LogError(ex, "Error during HMAC authentication");
×
103
            return AuthenticationError;
×
104
        }
105
    }
10✔
106

107

108
    private async Task<bool> ValidateContentHash()
109
    {
110
        if (!Request.Headers.TryGetValue(HmacAuthenticationShared.ContentHashHeaderName, out var contentHashHeader))
6✔
111
            return false;
1✔
112

113
        var contentHash = contentHashHeader.ToString();
5✔
114
        if (string.IsNullOrEmpty(contentHash))
5!
115
            return false;
×
116

117
        var computedHash = await GenerateContentHash().ConfigureAwait(false);
5✔
118

119
        return HmacAuthenticationShared.FixedTimeEquals(computedHash, contentHash);
5✔
120
    }
6✔
121

122
    private bool ValidateTimestamp()
123
    {
124
        var timestampHeader = GetHeaderValue(HmacAuthenticationShared.TimeStampHeaderName);
8✔
125
        if (!long.TryParse(timestampHeader, out var timestamp))
8✔
126
            return false;
1✔
127

128
        var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestamp);
7✔
129
        var now = DateTimeOffset.UtcNow;
7✔
130
        var timeDifference = Math.Abs((now - requestTime).TotalMinutes);
7✔
131

132
        // Use configured tolerance from options
133
        return timeDifference <= Options.ToleranceWindow;
7✔
134
    }
135

136

137
    private async Task<string> GenerateContentHash()
138
    {
139
        // Ensure the request body can be read multiple times
140
        Request.EnableBuffering();
5✔
141

142
        // Return empty content hash if there is no body
143
        if (Request.ContentLength == 0 || Request.Body == Stream.Null)
5✔
144
            return HmacAuthenticationShared.EmptyContentHash;
2✔
145

146
        await using var memoryStream = new MemoryStream();
3✔
147
        await Request.BodyReader.CopyToAsync(memoryStream).ConfigureAwait(false);
3✔
148

149
        // Reset position after reading
150
        Request.Body.Position = 0;
3✔
151

152
        // If the body is empty after reading, return empty content hash
153
        if (memoryStream.Length == 0)
3!
NEW
154
            return HmacAuthenticationShared.EmptyContentHash;
×
155

156
        var hashBytes = SHA256.HashData(memoryStream.ToArray());
3✔
157

158
        // 32 bytes SHA256 -> 44 chars base64
159
        Span<char> base64 = stackalloc char[44];
3✔
160
        if (Convert.TryToBase64Chars(hashBytes, base64, out int charsWritten))
3!
161
            return new string(base64[..charsWritten]);
3✔
162

163
        // if stackalloc is not large enough (should not happen for SHA256)
164
        return Convert.ToBase64String(hashBytes);
✔
165
    }
5✔
166

167
    private string[] GetHeaderValues(IReadOnlyList<string> signedHeaders)
168
    {
169
        var headerValues = new string[signedHeaders.Count];
4✔
170

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

174
        return headerValues;
4✔
175
    }
176

177
    private string? GetHeaderValue(string headerName)
178
    {
179
        if (headerName.Equals(HmacAuthenticationShared.HostHeaderName, StringComparison.InvariantCultureIgnoreCase))
20✔
180
        {
181
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.HostHeaderName, out var hostValue))
4!
182
                return hostValue.ToString();
4✔
183

184
            return Request.Host.Value;
×
185
        }
186

187
        // Handle date headers specifically
188
        if (headerName.Equals(HmacAuthenticationShared.DateHeaderName, StringComparison.InvariantCultureIgnoreCase)
16!
189
            || headerName.Equals(HmacAuthenticationShared.DateOverrideHeaderName, StringComparison.InvariantCultureIgnoreCase))
16✔
190
        {
191
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.DateOverrideHeaderName, out var xDateValue))
×
192
                return xDateValue.ToString();
×
193

194
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.DateHeaderName, out var dateValue))
×
195
                return dateValue.ToString();
×
196

197
            return Request.Headers.Date.ToString();
×
198
        }
199

200
        // Handle content-type and content-length headers specifically
201
        if (headerName.Equals(HmacAuthenticationShared.ContentTypeHeaderName, StringComparison.InvariantCultureIgnoreCase))
16!
202
            return Request.ContentType?.ToString();
×
203

204
        if (headerName.Equals(HmacAuthenticationShared.ContentLengthHeaderName, StringComparison.InvariantCultureIgnoreCase))
16!
205
            return Request.ContentLength?.ToString();
×
206

207
        // For all other headers, try to get the value directly
208
        if (Request.Headers.TryGetValue(headerName, out var value))
16✔
209
            return value.ToString();
15✔
210

211
        return null;
1✔
212
    }
213
}
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