Bugfix: 1385: Authentication fails if both date and x-ms-date headers are present - #2316
Bugfix: 1385: Authentication fails if both date and x-ms-date headers are present#2316BillBooks wants to merge 8 commits into
Conversation
specified For queues and blobs, when forming the shared key token, the Date header should be treated as the empty string when the x-ms-date header is present. For tables, the value of the x-ms-date header shoudl be used as the value of the Date header if teh x-ms-date header is present. This behavior is documented at https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
Wei Wei (blueww)
left a comment
There was a problem hiding this comment.
The change generally looks good to me.
It's better to also add test cases for blob/Queue servicce.
|
I'll look at adding some tests |
There was a problem hiding this comment.
🟡 Changes recommended
The current implementation uses value-truthiness instead of header-presence checks for x-ms-date, which can still diverge from the documented signing rules, and it also lacks a regression test covering requests that include both headers.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates Azurite’s Shared Key / Shared Key Lite signature generation to follow Azure Storage’s documented rules when both Date and x-ms-date headers are present, which previously could cause authentication failures.
Changes:
- Table: prefer
x-ms-dateas the effectiveDatevalue when constructing strings-to-sign. - Blob/Queue: force the
Datecomponent in the string-to-sign to be empty whenx-ms-dateis present. - Documentation: add a changelog entry for the fix.
File summaries
| File | Description |
|---|---|
| tests/table/utils/table.entity.tests.utils.for.rest.ts | Updates test signing helper to prefer x-ms-date for table SharedKeyLite strings-to-sign. |
| src/table/authentication/TableSharedKeyLiteAuthenticator.ts | Adjusts table SharedKeyLite string-to-sign to prefer x-ms-date over Date. |
| src/table/authentication/TableSharedKeyAuthenticator.ts | Adjusts table SharedKey string-to-sign to prefer x-ms-date over Date (including secondary path). |
| src/queue/authentication/QueueSharedKeyAuthenticator.ts | Blanks out Date in strings-to-sign when x-ms-date is present for queue auth. |
| src/blob/authentication/BlobSharedKeyAuthenticator.ts | Blanks out Date in strings-to-sign when x-ms-date is present for blob auth (including secondary path). |
| ChangeLog.md | Notes the shared key signature generation fix for x-ms-date. |
Review details
Suppressed comments (5)
src/blob/authentication/BlobSharedKeyAuthenticator.ts:151
- Same issue as above: Date should be signed as an empty string whenever x-ms-date is present, but this truthiness check can fall back to Date when x-ms-date is an empty string. Prefer checking header presence via req.getHeader(x-ms-date) !== undefined.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
src/queue/authentication/QueueSharedKeyAuthenticator.ts:347
- For queue SharedKeyLite, Date must be an empty string when x-ms-date is present. This truthiness check can incorrectly fall back to Date if x-ms-date is an empty string. Use a presence check (req.getHeader(x-ms-date) !== undefined).
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/table/authentication/TableSharedKeyAuthenticator.ts:115
- Same issue in the secondary string-to-sign path: if x-ms-date is present, its value must be used as the Date component (even if empty), but || can fall back to Date. Use an explicit presence check on x-ms-date.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:112
- Same issue in the secondary Table SharedKeyLite string-to-sign: || can fall back to Date when x-ms-date is present but empty. Use an explicit presence check on x-ms-date to match the service rules.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/blob/authentication/BlobSharedKeyAuthenticator.ts:85
- This change fixes signature generation for requests that include both Date and x-ms-date, but there are existing auth tests that only cover the default SDK header set. Add a regression test that sends a request with both headers set (Blob/Queue/and Table) and verifies authentication succeeds, to prevent future regressions.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
- Files reviewed: 6/6 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| this.getHeaderValueToSign(req, HeaderConstants.CONTENT_MD5), | ||
| this.getHeaderValueToSign(req, HeaderConstants.CONTENT_TYPE), | ||
| this.getHeaderValueToSign(req, HeaderConstants.DATE), | ||
| this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE), |
| this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) || | ||
| this.getHeaderValueToSign(req, HeaderConstants.DATE) |
| this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) || | ||
| this.getHeaderValueToSign(req, HeaderConstants.DATE) |
| getHeaderValueToSign(HeaderConstants.X_MS_DATE, headers) || | ||
| getHeaderValueToSign(HeaderConstants.DATE, headers) |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Several updated call sites use truthiness/getHeaderValueToSign() to detect x-ms-date presence, but that helper collapses “missing” and “present-but-empty” to "", so the documented “header present” rule is not implemented reliably/consistently.
Review details
Suppressed comments (8)
Previously missed (2) — in code that hasn't changed since the last review.
src/table/authentication/TableSharedKeyAuthenticator.ts:115
- Same issue as the primary string-to-sign:
||makes the x-ms-date preference depend on value truthiness rather than header presence. Use a presence check so Date is not used when x-ms-date is present (even if empty).
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:112 - Secondary string-to-sign has the same header-presence vs truthiness issue:
||falls back to Date when x-ms-date is present but empty. Use a presence check to follow the documented behavior.
src/table/authentication/TableSharedKeyAuthenticator.ts:57
- Using
||here only prefers x-ms-date when it has a truthy value; if the x-ms-date header is present but empty, this will incorrectly fall back to Date. The Shared Key docs describe behavior based on header presence, so this should key off header existence instead of truthiness.
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_MD5),
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_TYPE),
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:58
- Using
||here only prefers x-ms-date when it has a truthy value; if the x-ms-date header is present but empty, this will incorrectly fall back to Date. The Shared Key docs describe behavior based on header presence, so this should key off header existence instead of truthiness.
[
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
].join("\n") +
src/queue/authentication/QueueSharedKeyAuthenticator.ts:332
getHeaderValueToSign()returns "" when a header is missing or present-but-empty, so this conditional can't reliably implement the "if x-ms-date header is present" rule. Usereq.getHeader(HeaderConstants.X_MS_DATE) !== undefinedto check presence instead.
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_MD5),
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_TYPE),
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
this.getHeaderValueToSign(req, HeaderConstants.IF_MODIFIED_SINCE),
src/queue/authentication/QueueSharedKeyAuthenticator.ts:348
- Same presence-detection issue as the SharedKey path:
getHeaderValueToSign()collapses missing and empty headers to "", so the ternary should checkreq.getHeader(...) !== undefinedinstead.
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_MD5),
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_TYPE),
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE)
].join("\n") +
src/blob/authentication/BlobSharedKeyAuthenticator.ts:152
- In the secondary-account string-to-sign, this ternary uses
getHeaderValueToSign(X_MS_DATE)which cannot distinguish missing vs present-but-empty headers (it returns "" for both). For consistency with the primary path (which usesreq.getHeader(...) !== undefined) and to follow the documented "header present" rule, switch this to a presence check.
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_MD5),
this.getHeaderValueToSign(req, HeaderConstants.CONTENT_TYPE),
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
this.getHeaderValueToSign(req, HeaderConstants.IF_MODIFIED_SINCE),
tests/table/utils/table.entity.tests.utils.for.rest.ts:57
- This uses
||, which prefers x-ms-date only when the value is truthy; if the x-ms-date header key is present but empty, it will incorrectly fall back to Date. Mirror the production behavior by checking header presence (e.g.,hasOwnProperty) instead of value truthiness.
[
getHeaderValueToSign(HeaderConstants.X_MS_DATE, headers) ||
getHeaderValueToSign(HeaderConstants.DATE, headers)
].join("\n") +
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Header-presence checks are still value/truthiness-based in several updated call sites (can mis-handle empty-but-present x-ms-date) and the new behavior lacks direct regression tests for “both Date and x-ms-date present”.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (6)
tests/table/utils/table.entity.tests.utils.for.rest.ts:56
||uses truthiness, so an explicitly present but emptyx-ms-dateheader will be treated as absent and the Date value will be signed instead. For Table SharedKeyLite the spec is presence-based: whenx-ms-dateis present, use it as the Date value even if empty.
getHeaderValueToSign(HeaderConstants.X_MS_DATE, headers) ||
getHeaderValueToSign(HeaderConstants.DATE, headers)
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:57
- Using
||here is value-based; ifx-ms-dateis present but empty, the code will fall back toDate, which contradicts the presence-based rule described in the PR. Prefer an explicit presence check on the header and then choose which value to sign.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/table/authentication/TableSharedKeyAuthenticator.ts:57
- Using
||makes the selection value-based; an empty-but-presentx-ms-datewill incorrectly fall back toDate. The shared-key docs referenced in the PR describe presence-based behavior, so prefer checking header presence explicitly.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/queue/authentication/QueueSharedKeyAuthenticator.ts:331
getHeaderValueToSign()returns "" for both a missing header and a present-but-empty header, so this truthiness check can fail to blank out the Date line whenx-ms-dateis present. Usereq.getHeader(...) !== undefinedto detect presence.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
src/queue/authentication/QueueSharedKeyAuthenticator.ts:347
- Same presence check issue as the SharedKey path: the truthiness check on
getHeaderValueToSign(X_MS_DATE)cannot distinguish missing vs present-but-empty. Usereq.getHeader(...) !== undefinedso any providedx-ms-dateforces an empty Date element.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/blob/authentication/BlobSharedKeyAuthenticator.ts:154
- This presence check uses
getHeaderValueToSign(X_MS_DATE)(truthiness), but that helper returns "" for both missing and empty values. For consistency with the primary path (and to match the presence-based rule), usereq.getHeader(X_MS_DATE) !== undefinedhere too.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
| this.getHeaderValueToSign(req, HeaderConstants.CONTENT_MD5), | ||
| this.getHeaderValueToSign(req, HeaderConstants.CONTENT_TYPE), | ||
| this.getHeaderValueToSign(req, HeaderConstants.DATE), | ||
| req.getHeader(HeaderConstants.X_MS_DATE) !== undefined ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE), |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Several updated string-to-sign paths still use ||/truthy checks that can fall back to Date despite x-ms-date being present, and there’s no explicit regression test for the “both headers present” scenario.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (7)
Previously missed (4) — in code that hasn't changed since the last review.
src/blob/authentication/BlobSharedKeyAuthenticator.ts:154
- The primary string-to-sign uses
req.getHeader(... ) !== undefined, but the secondary string-to-sign usesgetHeaderValueToSign(...) ? ..., which can treat an emptyx-ms-datevalue as "not present" and reintroduceDateinto the signature. Make the secondary path use the same explicit header-presence check as the primary path for consistent SharedKey behavior.
src/queue/authentication/QueueSharedKeyAuthenticator.ts:331 - This conditional uses
getHeaderValueToSign(...)as the presence check, but that helper returns "" for any falsey value. Ifx-ms-dateis present but empty, the code will incorrectly includeDatein the signature instead of treating it as an empty string (per SharedKey rules whenx-ms-dateis present). Prefer checking header presence viareq.getHeader(HeaderConstants.X_MS_DATE) !== undefined.
This issue also appears on line 347 of the same file.
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:57
- As with SharedKey, using
||here means an empty/falseyx-ms-datevalue will fall back toDateeven though the header is present. Switch to an explicit header-presence check sox-ms-datealways wins when supplied.
This issue also appears on line 111 of the same file.
tests/table/utils/table.entity.tests.utils.for.rest.ts:56
- Using
||to pick betweenx-ms-dateandDatefalls back toDatewhenx-ms-dateis present but empty/falsey, which still contradicts the documented rule of preferringx-ms-datewhenever the header is present. Use an explicit presence check (likeheaders[HeaderConstants.X_MS_DATE] !== undefined) so the signing logic matches the Table service behavior.
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:112
- Same issue as the primary path:
||can fall back toDatewhenx-ms-dateis present but empty/falsey. Use a presence check so the secondary string-to-sign follows the documented precedence rules consistently.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/queue/authentication/QueueSharedKeyAuthenticator.ts:347
- Same issue as above:
getHeaderValueToSign(...)is not a reliable presence check. Usereq.getHeader(HeaderConstants.X_MS_DATE) !== undefinedso theDatecomponent is always empty whenx-ms-dateis present.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/blob/authentication/BlobSharedKeyAuthenticator.ts:86
- This changes SharedKey signing behavior across Blob/Queue/Table, but there are no regression tests that explicitly cover requests containing both
Dateandx-ms-date(the typical failure mode for this bug). Adding a targeted test that sends both headers and verifies the request is authorized would help prevent future regressions.
req.getHeader(HeaderConstants.X_MS_DATE) !== undefined ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Several updated code paths still use value-based (|| / truthy) checks for x-ms-date rather than header-presence checks, which can diverge from the documented behavior and lacks targeted regression coverage.
Review details
Suppressed comments (8)
Previously missed (5) — in code that hasn't changed since the last review.
src/blob/authentication/BlobSharedKeyAuthenticator.ts:154
- This x-ms-date check is value-based (
getHeaderValueToSign(...) ?) rather than presence-based. If x-ms-date is present but an empty string, Date would incorrectly be signed as non-empty, which contradicts the shared key rules when x-ms-date is present.
src/queue/authentication/QueueSharedKeyAuthenticator.ts:331 - The shared key rules require treating the Date field as empty whenever the x-ms-date header is present. Using
getHeaderValueToSign(req, X_MS_DATE) ?is value-based and will not trigger if x-ms-date is present-but-empty (getHeaderValueToSign returns ""). Check header presence (req.getHeader(...) !== undefined) instead.
This issue also appears on line 347 of the same file.
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:58
- Table SharedKeyLite stringToSign should use the x-ms-date header whenever it is present (even if its value is an empty string). Using
||with getHeaderValueToSign() falls back to Date when x-ms-date is present-but-empty because getHeaderValueToSign() returns "" for falsy values; use an explicit presence check instead.
This issue also appears on line 109 of the same file.
tests/table/utils/table.entity.tests.utils.for.rest.ts:57
- The REST test signing helper uses
||to choose between x-ms-date and Date, which is value-based and can diverge from the intended behavior when x-ms-date is present but empty. To mirror the service behavior and spec, choose based on header presence instead of truthiness.
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:108 - The comment contains a typo/garbled fragment ("stringToSignconst stringToSign: string ="), which makes the intent hard to read.
src/table/authentication/TableSharedKeyLiteAuthenticator.ts:114
- Secondary-account SharedKeyLite stringToSign has the same issue:
||selects Date when x-ms-date is present but empty. The spec is based on header presence, so checkreq.getHeader(...) !== undefinedand then sign with that header's value (which may legitimately be empty).
const stringToSign_secondary: string =
[
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ||
this.getHeaderValueToSign(req, HeaderConstants.DATE)
].join("\n") +
"\n" +
src/queue/authentication/QueueSharedKeyAuthenticator.ts:347
- Same presence-vs-value issue as above for SharedKeyLite header signing: the Date line should be empty if x-ms-date header exists, regardless of whether the x-ms-date value is an empty string.
this.getHeaderValueToSign(req, HeaderConstants.X_MS_DATE) ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE)
src/blob/authentication/BlobSharedKeyAuthenticator.ts:86
- There are existing auth tests for Blob/Queue, but I couldn't find any test that exercises the specific regression case described in the PR (both Date and x-ms-date headers present). Adding a regression test that injects a Date header alongside the SDK-generated x-ms-date (e.g., via a custom pipeline policy) would help prevent this from reappearing across Blob and Queue; Table REST tests could add a similar case.
req.getHeader(HeaderConstants.X_MS_DATE) !== undefined ? "" : this.getHeaderValueToSign(req, HeaderConstants.DATE),
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Lite
…Blob/Queue/Table regression tests
There was a problem hiding this comment.
🟡 Changes recommended
There are small but concrete issues to fix before merge (changelog formatting typo and an obvious comment paste/formatting error in the modified auth code block).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
| @@ -108,8 +109,9 @@ export default class TableSharedKeyLiteAuthenticator implements IAuthenticator { | |||
| // JS/.net Track2 SDK will generate stringToSign from IP style URI with "-secondary" in authenticationPath, so will also compare signature with this kind stringToSignconst stringToSign: string = | |||
There was a problem hiding this comment.
🟡 Changes recommended
The newly added raw-HTTP Blob/Queue regression tests should clean up the created container/queue to keep test state isolated and avoid leaving resources behind.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 2
- Review effort level: Lite
| assert.strictEqual( | ||
| statusCode, | ||
| 201, | ||
| `Expected container create to succeed (201) with both Date and x-ms-date headers, got ${statusCode}` | ||
| ); | ||
| }); |
| assert.strictEqual( | ||
| statusCode, | ||
| 201, | ||
| `Expected queue create to succeed (201) with both Date and x-ms-date headers, got ${statusCode}` | ||
| ); | ||
| }); |
… blob/queue regression tests
There was a problem hiding this comment.
🟢 Approval recommended
The signing changes match documented Azure Storage rules and are covered by new regression tests for Blob, Queue, and Table.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Lite
For queues and blobs, when forming the shared key token, the Date header should be treated as the empty string when the x-ms-date header is present.
For tables, the value of the x-ms-date header should be used as the value of the Date header if the x-ms-date header is present.
This behavior is documented at
https://learn.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key