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

loresoft / HashGate / 16897665204

12 Aug 2025 02:53AM UTC coverage: 81.559% (-0.3%) from 81.836%
16897665204

push

github

pwelter34
fix tests

110 of 166 branches covered (66.27%)

Branch coverage included in aggregate %.

319 of 360 relevant lines covered (88.61%)

12.3 hits per line

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

76.67
/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
                return AuthenticateResult.Fail($"Invalid Authorization header: {result}");
×
74

75
            if (!ValidateTimestamp())
8✔
76
                return InvalidTimestampHeader;
2✔
77

78
            if (!await ValidateContentHash())
6✔
79
                return InvalidContentHashHeader;
2✔
80

81
            var clientSecret = await _keyProvider
4✔
82
                .GetSecretAsync(hmacHeader.Client, Context.RequestAborted)
4✔
83
                .ConfigureAwait(false);
4✔
84

85
            if (string.IsNullOrEmpty(clientSecret))
4!
86
                return InvalidClientName;
×
87

88
            var headerValues = GetHeaderValues(hmacHeader.SignedHeaders);
4✔
89

90
            var stringToSign = HmacAuthenticationShared.CreateStringToSign(
4✔
91
                method: Request.Method,
4✔
92
                pathAndQuery: Request.Path + Request.QueryString,
4✔
93
                headerValues: headerValues);
4✔
94

95
            var expectedSignature = HmacAuthenticationShared.GenerateSignature(stringToSign, clientSecret);
4✔
96
            if (!HmacAuthenticationShared.FixedTimeEquals(expectedSignature, hmacHeader.Signature))
4✔
97
                return InvalidSignature;
1✔
98

99
            var identity = await _keyProvider
3✔
100
                .GenerateClaimsAsync(hmacHeader.Client, Scheme.Name, Context.RequestAborted)
3✔
101
                .ConfigureAwait(false);
3✔
102

103
            var principal = new ClaimsPrincipal(identity);
3✔
104
            var ticket = new AuthenticationTicket(principal, Scheme.Name);
3✔
105

106
            return AuthenticateResult.Success(ticket);
3✔
107
        }
108
        catch (Exception ex)
×
109
        {
110
            Logger.LogError(ex, "Error during HMAC authentication");
×
111
            return AuthenticationError;
×
112
        }
113
    }
10✔
114

115

116
    private async Task<bool> ValidateContentHash()
117
    {
118
        if (!Request.Headers.TryGetValue(HmacAuthenticationShared.ContentHashHeaderName, out var contentHashHeader))
6✔
119
            return false;
1✔
120

121
        var contentHash = contentHashHeader.ToString();
5✔
122
        if (string.IsNullOrEmpty(contentHash))
5!
123
            return false;
×
124

125
        var computedHash = await GenerateContentHash().ConfigureAwait(false);
5✔
126

127
        return HmacAuthenticationShared.FixedTimeEquals(computedHash, contentHash);
5✔
128
    }
6✔
129

130
    private bool ValidateTimestamp()
131
    {
132
        var timestampHeader = GetHeaderValue(HmacAuthenticationShared.TimeStampHeaderName);
8✔
133
        if (!long.TryParse(timestampHeader, out var timestamp))
8✔
134
            return false;
1✔
135

136
        var requestTime = DateTimeOffset.FromUnixTimeSeconds(timestamp);
7✔
137
        var now = DateTimeOffset.UtcNow;
7✔
138
        var timeDifference = Math.Abs((now - requestTime).TotalMinutes);
7✔
139

140
        // Use configured tolerance from options
141
        return timeDifference <= Options.ToleranceWindow;
7✔
142
    }
143

144

145
    private async Task<string> GenerateContentHash()
146
    {
147
        // Ensure the request body can be read multiple times
148
        Request.EnableBuffering();
5✔
149

150
        // Return empty content hash if there is no body
151
        if (Request.ContentLength == 0 || Request.Body == Stream.Null)
5✔
152
            return HmacAuthenticationShared.EmptyContentHash;
2✔
153

154
        await using var memoryStream = new MemoryStream();
3✔
155
        await Request.BodyReader.CopyToAsync(memoryStream).ConfigureAwait(false);
3✔
156

157
        // Reset position after reading
158
        Request.Body.Position = 0;
3✔
159

160
        // If the body is empty after reading, return empty content hash
161
        if (memoryStream.Length == 0)
3!
162
            return HmacAuthenticationShared.EmptyContentHash;
×
163

164
        var hashBytes = SHA256.HashData(memoryStream.ToArray());
3✔
165

166
        // 32 bytes SHA256 -> 44 chars base64
167
        Span<char> base64 = stackalloc char[44];
3✔
168
        if (Convert.TryToBase64Chars(hashBytes, base64, out int charsWritten))
3!
169
            return new string(base64[..charsWritten]);
3✔
170

171
        // if stackalloc is not large enough (should not happen for SHA256)
172
        return Convert.ToBase64String(hashBytes);
✔
173
    }
5✔
174

175
    private string[] GetHeaderValues(IReadOnlyList<string> signedHeaders)
176
    {
177
        var headerValues = new string[signedHeaders.Count];
4✔
178

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

182
        return headerValues;
4✔
183
    }
184

185
    private string? GetHeaderValue(string headerName)
186
    {
187
        if (headerName.Equals(HmacAuthenticationShared.HostHeaderName, StringComparison.InvariantCultureIgnoreCase))
20✔
188
        {
189
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.HostHeaderName, out var hostValue))
4!
190
                return hostValue.ToString();
4✔
191

192
            return Request.Host.Value;
×
193
        }
194

195
        // Handle date headers specifically
196
        if (headerName.Equals(HmacAuthenticationShared.DateHeaderName, StringComparison.InvariantCultureIgnoreCase)
16!
197
            || headerName.Equals(HmacAuthenticationShared.DateOverrideHeaderName, StringComparison.InvariantCultureIgnoreCase))
16✔
198
        {
199
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.DateOverrideHeaderName, out var xDateValue))
×
200
                return xDateValue.ToString();
×
201

202
            if (Request.Headers.TryGetValue(HmacAuthenticationShared.DateHeaderName, out var dateValue))
×
203
                return dateValue.ToString();
×
204

205
            return Request.Headers.Date.ToString();
×
206
        }
207

208
        // Handle content-type and content-length headers specifically
209
        if (headerName.Equals(HmacAuthenticationShared.ContentTypeHeaderName, StringComparison.InvariantCultureIgnoreCase))
16!
210
            return Request.ContentType?.ToString();
×
211

212
        if (headerName.Equals(HmacAuthenticationShared.ContentLengthHeaderName, StringComparison.InvariantCultureIgnoreCase))
16!
213
            return Request.ContentLength?.ToString();
×
214

215
        // For all other headers, try to get the value directly
216
        if (Request.Headers.TryGetValue(headerName, out var value))
16✔
217
            return value.ToString();
15✔
218

219
        return null;
1✔
220
    }
221
}
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