Skip to content

ID-279: replace a mismatched Authorization header on server Subscriptions - #50

Merged
karlnaden merged 5 commits into
inferno-framework:mainfrom
alexd-leaporbit:id-279-replace-auth-header
Oct 21, 2026
Merged

karlnaden merged 5 commits into
inferno-framework:mainfrom
alexd-leaporbit:id-279-replace-auth-header

Conversation

@alexd-leaporbit

@alexd-leaporbit alexd-leaporbit commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Previously, when the Subscription a tester gave the server suite already had an Authorization entry in channel.header with a different token than the Notification Access Token input, Inferno added a second Authorization header instead of replacing the first. The server sent notifications with the token Inferno asked for, but the notification verification compared them against the stale header and failed (Expected Bearer <old token>, received Bearer <new token>).

Now the server suite replaces any Authorization entry that doesn't match, comparing the header name case-insensitively, and keeps all other headers. The warning says whether the header was added or replaced. This affects the server suite and the Da Vinci PAS kit, which reuses the same check.

Additionally:

  • A header written as a single string is converted into a list with a warning, and any other non-list header fails the Subscription conformance test. A Subscription with no channel, or a channel that isn't an object, fails with "The Subscription must include a channel element." instead of an error.
  • The event notification verification now checks the notification's headers against the Subscription Inferno submitted, not the copy the server returned. The IG requires every requested header to be sent but doesn't require the server to echo them back. A server that left header out of its response used to make this test error, and one that returned different values had the notification compared against those values.
  • Requested headers are parsed the way HTTP defines them, so X-Custom:value (no space after the colon) matches the header the server sent.
  • CI runners are pinned to ubuntu-24.04 instead of ubuntu-latest, as requested for all in-progress PRs.

Fixes #48

Run the specs with bundle exec rspec spec/subscriptions_test_kit/common spec/subscriptions_test_kit/subscription_r4_server/common.

How it works

lib/subscriptions_test_kit/common/subscription_conformance_verification.rb

  • server_check_channel returns the Subscription unchanged when channel is missing or isn't an object. The conformance test's subscription_verification then fails it with the existing channel message.
  • server_check_channel now calls a new server_check_channel_authorization_header.
  • An entry counts as an Authorization header when it is a string whose name (the text before the first :, trimmed) is authorization in any casing.
  • The header is left unchanged only when there is exactly one such entry and it is exactly Authorization: Bearer <access token>. Otherwise every Authorization entry is removed, other entries are kept in order, and Authorization: Bearer <access token> is appended.
  • The warning now reads "Tester-provided Subscription updated before use: Added …" or "… Replaced the Authorization header entry in the header field with the Bearer token <access token> provided in the "Notification Access Token" input." and no longer shows the old token.
  • A header that is a single string is converted into a one-entry list with the warning "Tester-provided Subscription updated before use: incorrect string value for element header converted into a list containing the provided string.", and the Authorization check then runs on that list. Any other non-list header is left unchanged and recorded as an error, so the test fails. Entries that aren't strings are left alone.
  • The "subscription url was changed" warning is now a <<~ heredoc, so an endpoint containing a newline no longer renders as a code block.

lib/subscriptions_test_kit/common/notification_conformance_verification.rb

  • notification_header_verification takes a new optional requested_subscription: keyword. It defaults to the subscription argument, so existing callers (including other kits) are unaffected. The requested headers come from requested_subscription; the content type is still checked against the payload of subscription.
  • Requested header strings are split on the first :, the value is trimmed, and names are compared case-insensitively. Entries that aren't strings, blank names, a missing or malformed channel or header, and a payload that isn't a string are skipped instead of raising an error.

lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction/subscription_conformance_test.rb

  • The test description now says Inferno replaces any other Authorization entry, that header must be a list of strings (a single string is converted, any other value fails), and that a type other than rest-hook fails the test. It previously said Inferno would correct the type.

lib/subscriptions_test_kit/suites/subscriptions_r5_backport_r4_server/common/interaction_verification/notification_conformance_test.rb

  • Passes the Subscription Inferno submitted (the creation request's body) as requested_subscription:.
  • Fails with "The Subscription the server returned is not a JSON object." when the creation response is valid JSON but not an object, instead of erroring.

.github/workflows/ruby.yml, .github/workflows/run_inferno_execution_scripts.yml

  • Every job's runs-on is ubuntu-24.04 instead of ubuntu-latest.

Specs

  • subscription_conformance_verification_spec.rb: the harness outputs the checked Subscription, so the specs assert on the resulting channel.header as well as the warning. Cases: correct header kept (also with a lowercase name, and with a token containing a colon); header added when missing; a different token replaced without echoing it; an uppercase AUTHORIZATION : name replaced; a mixed list where every Authorization entry is replaced and X-Custom-Header is kept; four malformed variants of the correct header replaced; duplicate correct entries collapsed to one; a lowercase bearer scheme replaced; non-string entries ignored; names that only contain "Authorization" (X-Authorization, Authorization-Info) kept; a string header converted (correct token kept, different token replaced, other header kept); an object or number header failing unchanged; a missing, string or array channel returned unchanged; an endpoint containing a newline rendering as prose.
  • subscription_conformance_test_spec.rb: the conformance test fails, rather than erroring, for a missing, string or array channel and for an object header.
  • notification_conformance_verification_spec.rb: a missing, string, object or non-string-entry header, a channel that isn't an object, a payload that isn't a string, blank header names, a header written without a space after the colon, and whitespace around the value.
  • notification_conformance_test_spec.rb: the server's response has no header, adds a header, or has a different Authorization token than the one Inferno submitted (the notification sending the submitted token passes; sending the server's token fails); a response that isn't a JSON object fails.

Impact on existing results

  • The warning text changed from "Added the Authorization header field with a Bearer token set to X to the header field …" to "Tester-provided Subscription updated before use: Added/Replaced the Authorization header entry in the header field with the Bearer token X …". This kit's execution scripts don't depend on it: all three still match their expected results unchanged.
  • When the header is replaced, the new Authorization entry moves to the end of the list.
  • The event notification verification can now fail where it passed before only when the notification matched headers the server echoed back but not the ones Inferno requested; it now passes when it sent the requested ones. Cases that used to raise an error now pass or fail cleanly.
  • Da Vinci PAS reuses server_check_channel (v2.2.1 test 1.02 and subscriptions_r4_server_subscription_conformance in v2.0.1), and its pended-scenario notification test 2.3.2.02 failed on the stale header. It needs no code change, only bundle update subscriptions_test_kit after a release (its gemspec allows ~> 0.13, >= 0.13.0). Callers of notification_header_verification that pass two arguments behave as before, apart from the more lenient header parsing.

Testing Guidance

Run the client and server suites against each other in the same Inferno, changing the server's Notification Access Token so it no longer matches the Authorization: Bearer SAMPLE_TOKEN header in the preset's Subscription.

1. Start the client session

Steps

  1. Create a session for the Subscriptions R4 Client suite and select the Inferno Subscription R4 Client Empty Preset preset.
  2. Select group 1 Demonstrate the Rest-Hook Subscription Workflow, click Run Tests, and submit.

Expected Result

  • A "User Action Required" dialog shows that Inferno is waiting for a Subscription creation request (test 1.1.01).

2. Run the server workflow with a different Notification Access Token

Steps

  1. In another tab, create a session for the Subscriptions R4 Server suite and select the Empty Notifications Against The Subscriptions Client Suite preset.
  2. Select group 1 Demonstrate the Subscription Workflow and click Run Tests.
  3. In the input dialog, change Notification Access Token from SAMPLE_TOKEN to ID279_TOKEN. Leave the Subscription unchanged and submit.
  4. When the "User Action Required" dialog appears (test 1.1.02), wait about 20 seconds for the client suite to send its notifications, then click the link to continue.

Expected Result

  • 1.1.01 Verify Subscription to Send to Server passes with the warning "Tester-provided Subscription updated before use: Replaced the Authorization header entry in the header field with the Bearer token ID279_TOKEN provided in the "Notification Access Token" input."
  • The Subscription sent to the client contains a single Authorization: Bearer ID279_TOKEN header.
  • Before this change, 1.1.01 warned "Added the Authorization header field with a Bearer token set to ID279_TOKEN to the header field …", and the Subscription was sent with both Authorization: Bearer SAMPLE_TOKEN and Authorization: Bearer ID279_TOKEN.
28-after-1 1 01-replaced-warning-reworded 1-before-1 1 01-added-warning-two-headers

3. Check the notification verification

Steps

  1. Once the server run finishes, open group 1.2 Subscription Workflow Interaction Verification.

Expected Result

  • 1.2.02 Subscription Event Notification Verification passes, and group 1 passes.
6-after-1 2 02-row 4-after-1 2 02-passes
  • Before this change, 1.2.02 failed with "Requested value for header authorization does not match the request sent. Expected Bearer SAMPLE_TOKEN, received Bearer ID279_TOKEN", and group 1 failed.
5-before-1 2 02-row 2-before-1 2 02-fails-stale-authorization-header

4. Finish the client session

Steps

  1. Return to the client session and click the link in the "User Action Required" dialog to confirm the Subscription interactions are done.
  2. Complete the attestation that follows.

Expected Result

  • The client run completes, so no session is left waiting for requests.

5. A header written as a single string

Steps

  1. Repeat steps 1–4 with new client and server sessions, but in step 2 leave Notification Access Token as SAMPLE_TOKEN and, in the Workflow Subscription Resource input, change "header": ["Authorization: Bearer SAMPLE_TOKEN"] to "header": "Authorization: Bearer SAMPLE_TOKEN".

Expected Result

  • 1.1.01 Verify Subscription to Send to Server passes with the warning "Tester-provided Subscription updated before use: incorrect string value for element header converted into a list containing the provided string." and no "Added" or "Replaced" warning.
29-after-string-header-1 1 01-converted-warning-reworded
  • 1.2.02 Subscription Event Notification Verification passes, and group 1 passes.
19-after-string-header-1 2 02-passes-row 20-after-string-header-group1-passes
  • In the previous version of this PR, 1.1.01 discarded the string and warned "Added the Authorization header field with a Bearer token set to SAMPLE_TOKEN …".
18-before-string-header-1 1 01-added-warning

6. A malformed header or a Subscription without a channel

These runs only need the server suite. Group 1 can't be run test by test, so run the whole group. 1.1.02 fails ("expected 201, but received 500") and 1.2.02 is skipped because no client session is waiting for the Subscription.

Steps

  1. In a new Subscriptions R4 Server session with the Empty Notifications Against The Subscriptions Client Suite preset, select group 1 Demonstrate the Subscription Workflow and click Run Tests.
  2. In the Workflow Subscription Resource input, change "header": ["Authorization: Bearer SAMPLE_TOKEN"] to "header": {"Authorization": "Bearer SAMPLE_TOKEN"} and submit.
  3. Run group 1 again, this time deleting the whole "channel": { … } element from the Workflow Subscription Resource input.

Expected Result

  • With the object header, 1.1.01 fails with "Resource does not conform to the profile: …backport-subscription". Its messages include the error "The header element on the tester-provided Subscription must be a list of strings." and the validator's "The property header must be a JSON Array, not an Object".
30-after-object-header-1 1 01-fails-reworded
  • In the previous version of this PR, 1.1.01 passed: it discarded the object and warned "Added the Authorization header field …".
22-before-object-header-1 1 01-added-warning
  • Without a channel, 1.1.01 fails with "The Subscription must include a channel element."
23-after-no-channel-1 1 01-fails
  • Before this change, 1.1.01 ended in an error: "Error: undefined method `[]' for nil".
24-before-no-channel-1 1 01-error

…ions

The server suite's Subscription check only looked for an exact
"Authorization: Bearer <token>" entry in channel.header. When the
tester's Subscription already carried an Authorization header with a
different token, Inferno appended a second Authorization header instead
of replacing the first. Notification verification then compared the
notification against the stale header and failed, even though the
server sent the token Inferno asked for.

server_check_channel now finds Authorization entries by header name,
case-insensitively, and leaves the header untouched only when there is
exactly one entry and it matches "Bearer <access token>" in the same
form notification verification parses. Otherwise it removes every
Authorization entry, keeps all other headers, and appends the correct
one, with a warning that says whether the header was added or
replaced. The warning no longer repeats the old token. A header field
that is not a list is replaced with a list instead of raising an
error, and entries that are not strings are left alone.

The Subscription conformance test description now says Inferno
replaces any other Authorization entry.

Fixes inferno-framework#48

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@karlnaden karlnaden left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two minor issues to fix

end

def server_check_channel_authorization_header(subscription_channel, access_token)
headers = subscription_channel['header'].is_a?(Array) ? subscription_channel['header'] : []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the tester incorrectly provided a string instead of a list of strings for the header, this will cover that over and throw out that header. Previously it would result in a purple error. Let's do something smarter. We could either fix it up, but add a warning that we did so, or have an explicit failure due to a malformed Subscription. I think its probably right to fix it up, but not completely confident in that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with fixing it up (0b85f92). A string header is now converted into a one-entry list with a warning ("The header field on the Subscription resource must be a list of strings. Inferno converted the single string value into a list containing that value."), and the normal Authorization check then runs on that list: a correct token is kept, a different one is replaced, and any other header is kept with ours added. Any other non-list value (object, number, boolean) is left as is and recorded as an error, so the Subscription conformance test fails rather than erroring or quietly discarding it. Specs cover each of these shapes, including a run of the conformance test itself, and Testing Guidance steps 5 and 6 show the string and object cases in the UI.

Comment on lines 74 to 75
subscription_channel = subscription['channel']
unless channel_field_matches?(subscription_channel, 'endpoint', subscription_channel_url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-existing problem if the channel is missing or malformed. makes sense to fix, I think. Add specs as well.

Suggested change
subscription_channel = subscription['channel']
return subscription unless subscription_channel.is_a?(Hash)
unless channel_field_matches?(subscription_channel, 'endpoint', subscription_channel_url)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied the suggestion (0b85f92). A Subscription with no channel, or a channel that isn't an object (string or array), is now returned unchanged, and subscription_verification fails it with the existing "The Subscription must include a channel element." message instead of a purple error. Added specs for each case at both levels: server_check_channel and the Subscription conformance test. Testing Guidance step 6 shows the missing-channel case in the UI.

alexd-leaporbit and others added 3 commits October 20, 2026 13:35
The CI and execution-script workflows ran on ubuntu-latest, which
moves to a new Ubuntu release whenever GitHub updates the label and
was causing CI problems. Every job in ruby.yml and
run_inferno_execution_scripts.yml now runs on ubuntu-24.04, which
the maintainer asked all in-progress PRs to use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
server_check_channel treated a channel.header that was not a list as
empty, so a tester who wrote the header as a single string lost it
without being told. A header string is now converted into a list with
a warning saying Inferno did so, and the usual Authorization check
then runs on that list. Any other non-list value is left unchanged and
recorded as an error, so the Subscription conformance test fails
instead of quietly replacing what the tester provided.

server_check_channel also read channel.endpoint without checking that
channel exists, so a Subscription with no channel, or with a channel
that is not an object, raised an error. It now returns such a
Subscription unchanged, and subscription_verification fails it with
the existing "must include a channel element" message.

The warning about a changed channel endpoint is now a squiggly
heredoc, so an endpoint value containing a newline no longer turns
the message into a code block. The Subscription conformance test
description now says that header must be a list of strings, that a
single string is converted and any other value fails, and that a type
other than rest-hook fails the test rather than being corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The event notification conformance test took the requested headers
from the Subscription the server returned when it was created. The IG
requires each channel.header value of the Subscription to be sent on
notifications, but does not require a server to echo those headers
back. A server that left header out of its response made the test
raise an error. A server that returned different header values had
the notification compared against those values instead of the ones
Inferno asked for, including the Authorization header.

notification_header_verification takes a new optional
requested_subscription keyword, which defaults to the subscription
argument so existing callers are unaffected. The notification
conformance test passes the Subscription Inferno submitted (the
creation request body), so the requested headers always come from
what Inferno asked for, while the content type is still checked
against the payload the server returned. The test now fails with a
clear message when the returned Subscription is not a JSON object.

Requested header strings are parsed the way HTTP defines header
fields: the name ends at the first colon, whitespace around the value
is ignored, and names are compared case-insensitively. Previously a
header written without a space after the colon, such as
"X-Custom:value", could never match the header the server sent. A
missing or malformed channel or header, entries that are not strings,
blank header names, and a payload that is not a string are skipped
instead of raising an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@karlnaden karlnaden left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small wording tweaks - will probably need to adjust line length after applying the suggestions

action = authorization_headers.empty? ? 'Added' : 'Replaced'
add_message('warning', %(
#{action} the Authorization header field with a Bearer token set to `#{access_token}` in the `header` field on
the Subscription resource in order to connect successfully with the Inferno subscription channel.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that testers will necessarily understand why this change is needed to
"connect successfully". Suggested change focuses on what happened and points them to specific input for more details.

Suggested change
the Subscription resource in order to connect successfully with the Inferno subscription channel.
Tester-provided Subscription updated before use: #{action} the Authorization header entry in the `header` field with the Bearer token `#{access_token}` provided in the "Notification Access Token" input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrapped it onto two lines to stay under the 120-character limit, and the UI shows it as one sentence. Your suggestion only covered line 115, so I also replaced line 114 by hand. The specs now match the new wording and check that the message still names the token. Testing Guidance step 3 has a new screenshot.

Comment on lines +135 to +136
The `header` field on the Subscription resource must be a list of strings, so Inferno could not add the
Authorization header field with a Bearer token set to `#{access_token}`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `header` field on the Subscription resource must be a list of strings, so Inferno could not add the
Authorization header field with a Bearer token set to `#{access_token}`.
The `header` element on the tester-provided Subscription must be a list of strings.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the token gone from this message, access_token was no longer used in server_channel_header_list, and RuboCop flagged it (Lint/UnusedMethodArgument). I removed the argument from the method and from its one caller in server_check_channel_authorization_header. Testing Guidance step 6 has a new screenshot.

Comment on lines +129 to +130
The `header` field on the Subscription resource must be a list of strings. Inferno converted the single
string value into a list containing that value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The `header` field on the Subscription resource must be a list of strings. Inferno converted the single
string value into a list containing that value.
Tester-provided Subscription updated before use: incorrect string value for element `header` converted into a list containing the provided string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrapped it onto two lines for the line-length limit. The specs match the new wording, and Testing Guidance step 5 has a new screenshot.

Reword the three messages server_check_channel adds when it changes or
rejects the tester's channel.header, as the reviewer suggested. The
Authorization warning now starts with "Tester-provided Subscription
updated before use:" and says the Bearer token comes from the
"Notification Access Token" input. It no longer says the change is
needed to connect to the Inferno subscription channel. The warning for
a single-string header and the error for any other non-list header use
the same wording.

The error for a non-list header no longer names the access token, so
server_channel_header_list had an unused access_token argument. That
argument is removed from the method and from its one caller. The
module specs now match on the new wording, and each one also checks
that the Authorization warning still names the token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@karlnaden
karlnaden merged commit 0c71363 into inferno-framework:main Oct 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server_check_channel appends a second Authorization header instead of replacing a different one

2 participants