Repository navigation
Add the Shared Signals sample: a transmitter and a receiver - #44
Merged
Merged
Conversation
Two applications rather than one host with both roles, because the question the sample answers - how a receiver knows an event is genuine - has nothing to check when the signature, the key set, the issuer and the audience all belong to the process doing the checking. The pair now runs end to end: revoking a session at the transmitter reaches the receiver as a signed Security Event Token over a real HTTPS push, and the receiver answers 202. Three things the draft was missing before it did: - AllowedReceiverAddresses. A transmitter refuses by default to POST to a host inside its own network, which is what localhost is, so the operator has to name the destination. Without it the sweep refused every delivery. - StreamIssuer. SSF 1.0 Section 4.1.6 wants "iss" checked against the issuer of the stream the event arrived on, and the receiver threw on its first token until it was set. - A key id minted with the key. The draft fixed KeyId to a constant while minting a fresh key per run, so a restarted transmitter signed with a new key under a name the receiver already held: no refetch, and every push answered 400. This is the one failure the sample exists to make visible, so it now demonstrates the working case and the README says what breaking it looks like. References the released 2.4.0 packages rather than the sibling checkout, so the sample builds for a reader who cloned only this repository. Closes #36
… code lacks A review round against the code found the sample's prose asserting things the sample does not do. The corrections, and what each one was hiding: - The rollover exercise omitted the cooldown under the forced refetch (JwksKeyResolutionOptions.RolloverRefetchCooldown, 30 seconds). A push landing inside that window is judged against the stale key set, refused, and acknowledged out of the transmitter's queue rather than retried, so the event is lost. A reader following the old instructions briskly would have seen the opposite of what the section promised. The window is the same order as the delivery sweep, which is what makes it easy to hit. - The trust-boundary exercise named the wrong mechanism. Pointing the receiver at another transmitter address is refused on the issuer, not the signature: IssuerAllowlistStep runs before SignatureStep, and the key resolver would have found the real key set anyway. - DefaultSubjectsMode did nothing here. It is read only when a stream is created through the management API, which this sample does not map, and a configured stream carries its own SubjectsMode, defaulting to All. Removed with its comment. - The comment about a receiver pausing the stream through the API described an API this app deliberately does not map. - SessionStore claimed to keep the session-named and user-named cases apart. It handles only the first; the comment now says so, and the README says what a real receiver does with the second. - The JWK Set does not carry "the modulus and exponent and nothing else": the key type, id, algorithm and use travel too, which is what the next section told the reader to go and read. - Two log quotes were prefixes presented as whole lines, and "Refusing to deliver" is an exception message inside a warning rather than a line of its own. Also, from the same round: configuration is read as required rather than defaulted, so a lost key cannot produce an app running on a hardcoded address; AllowedReceiverAddresses is derived from the declared streams so the permitted address and the delivered-to address cannot be edited apart; the direct Abblix.JWT reference the code actually uses is declared; and the solution file keeps its alphabetical order. The pair was re-run end to end after these changes: empty before the revoke, ["alice-laptop-session"] after, receiver answering 202.
… for copying The rollover exercise asked the reader to wait out RolloverRefetchCooldown, 30 seconds, before revoking. That default is a rate limit, not a propagation delay: the push endpoint accepts a token before any signature is verified, so anyone reaching it can send tokens naming key ids nobody published, and without the floor each one becomes a fetch against the transmitter. The receiver would work as an amplifier aimed at the party it trusts most. A single reader running two local processes generates none of that traffic, and the wait was drowning the one exercise the sample exists for. Set to zero, with the reasoning next to the line and a section in the README, both saying not to copy it and what the floor costs where it is kept: within the window a token signed by the new key is judged against the old key set and refused, and a refused push is acknowledged out of the queue rather than retried, so those events are lost. Verified: restarting the transmitter and revoking immediately now delivers, where the same sequence was refused with 400 before.
…e cooldown Second review round. The first finding is the one worth stating plainly: the previous commit claimed to have removed prose asserting behaviour the code does not have, and introduced one. It said a constant key id leaves "nothing in either log" mentioning keys. The transmitter logs the receiver's refusal with the error code, and for a signature failure that code is invalid_key. The correction keeps what was useful in the point: invalid_key names the symptom, not the cause. It is equally what a corrupt token or a wrong algorithm produces, so it does not lead anyone to a key id reused across a rotation. Two mechanisms were also described loosely, in the same texts that teach being precise about mechanisms: - The amplification the cooldown limits does not work because signatures go unchecked. The issuer allowlist runs first and admits only tokens naming this transmitter, which is a public value; what leaves the endpoint open is that it carries no transport authentication. - Issuer is the identity the transmitter claims, not the address it listens on, and losing it makes the receiver's expected issuers stop matching. The comment said it would run on a hardcoded address, which comes from launchSettings. Configuration keys are now named once as constants, so renaming one cannot leave the exception message describing a key that no longer exists, and the comments explain what each value is for rather than what the previous revision did. Re-run after the change: empty before, ["alice-laptop-session"] after, 202.
…really has Two claims the sample carried, both traced to the same place: the library's class-level remark on PushDeliveryHandler said a repeat is short-circuited, and the sample repeated it. It is not. The handler calls the sink unconditionally and writes the replay entry afterwards, discarding the reservation's answer, so a duplicate reaches the sink every time and idempotency there is the only protection rather than a second line behind the cache. The order is right and the reason is worth keeping, so the comment now states it: an entry written before the sink would stand even when the sink refused, and the transmitter's retry would be answered 202 with nobody having seen the event. Filed against the library separately. The rollover exercise said deliveries fail "from then on" with a constant key id. The cached key set also has an ordinary lifetime, fifteen minutes by default, and when it expires the receiver refetches for its own reasons and picks up the new key under the pinned name. The failure heals itself, which is worse for a teaching exercise than a failure that stays. The README now names the window and says the events refused inside it are already gone. Also from the same round: the DeliveryError comment invited a sink to answer with one when it merely could not act right now, which destroys the event, because everything except access_denied and authentication_failed is final. And two comments asserted what the app does when configuration goes missing while the throw two lines below says otherwise - the counterfactual has its anchor back, without narrating the previous revision. Re-run after the change: empty before, ["alice-laptop-session"] after, 202.
…deliver nothing Три вещи, каждая на своей строке кода. Точка отзыва и точка приёма не защищены ничем, и об этом было сказано везде, кроме тех двух строк, которые копирует читатель. Теперь сказано на них: подпись отвечает на вопрос, кто подписал, а не кто попросил, поэтому в бою вызов стоит за той же проверкой, что и завершение сеанса, а точка приёма несёт учётные данные потока. Объявленный поток без адреса доставки больше не связывается молча. Опечатка в имени ключа делала поток опросным, точка опроса тут не отображена, и передатчик стартовал чисто, ничего не доставляя и ничего не записывая - ровно тот исход, который соседний комментарий обещает предотвратить. Проверено подстановкой опечатки: приложение падает и называет поток. Адрес доставки жил в коде приёмника и в настройках передатчика одновременно. Теперь он читается из настроек обеими сторонами, а расхождение даёт 404, про который сказано в разборе журнала. Заодно: разрешающий список адресов назван тем, чем является - обходом отказа, а не фильтром; профили запуска приведены к виду прочих примеров, поэтому хосты стартуют в Development; в README добавлены два стартовых предупреждения, которые здесь неприменимы, упражнение с проверкой аудитории, оговорка про curl -k и рецепт запуска с перенаправлением ошибок; ссылки ведут на руководство, описывающее этот пример; идентификаторы пакетов записаны так, как опубликованы.
Комментарий у точки приёма предлагал RequireAuthorization как образец для боя. Рецензент применил эту строку и запустил: хост стартует чисто, а на каждой доставке отвечает 500 - нашлись метаданные авторизации и не нашлось обслуживающего их слоя. Это ровно тот исход, от которого предостерегает соседняя правка, поэтому теперь названо и условие: схема аутентификации должна быть зарегистрирована. Комментарий у точки отзыва обещал больше, чем бывает: "каждый получатель, доверяющий издателю, поверит". Получатель с другой ожидаемой аудиторией отказывает - это проверено прогоном и описано в соседнем разделе README, который тем самым опровергал строку выше. Теперь сказано про получателей, которым адресованы потоки. Там же убрана взаимоисключающая пара: точка не может одновременно стоять за проверкой и не существовать. В бою её нет вовсе, а вызов делается внутри процесса, там где решение уже принято и разрешено. Проверка потока без адреса доставки переписана условием вместо цикла, который всегда падает на первом обороте; отказ проверен подстановкой опечатки заново. В README: раздел про устройство получил файл настроек приёмника, куда переехал маршрут доставки; разрешающий список назван исключением и там; объяснение предупреждения 2012 признаёт, что публикуемый документ действительно объявляет NONE, и это ничего не стоит лишь потому, что получатель здесь настроен, а не обнаруживает.
…кумент Комментарий у точки приёма предлагал в качестве лекарства AddAuthentication и AddAuthorization. Рецензент применил ровно это и получил ту же пятисотку, только с другим сообщением: схема по умолчанию не найдена. То есть читателя переводили из одного отказа в другой, и он заключил бы, что виновата библиотека. Проверено на отдельной копии: с зарегистрированным обработчиком схемы доставка без учётных данных получает 401, с ними - 400 от разбора токена, то есть авторизация пройдена. Теперь в комментарии названы обе половинчатые меры с их сообщениями и то, что работает. README повторял ту же рекомендацию во втором месте и вдобавок сохранял формулировку, удалённую из кода как противоречивую: конечная точка не может одновременно стоять за проверкой и не существовать. Обе половины приведены к виду кода. Добавлено то, о чём пример молчал: документ конфигурации объявляет адреса API управления независимо от того, отображён ли он, и каждый из них отвечает 404. Проверено на живом хосте - configuration_endpoint из документа отвечает 404. Здесь это никого не задевает, потому что единственный получатель настроен, а не обнаруживает, но документ - тот самый артефакт пары, который посторонняя сторона разбирает машинно. Отказ потока без адреса доставки больше не печатает пустое имя, когда в настройках потеряно и имя потока.
AddJwtBearer был назван как готовое лекарство, а он не компилируется без своего пакета: этот проект его не подключает. Тот же класс, что и в двух прошлых раундах, только отказ переехал со времени выполнения на время сборки. Теперь пакет назван прямо и сказано, что в проекте его нет. Проверка потока без адреса доставки перехватывала и тот случай, когда в объявлении нет вообще ничего, и сообщала о поле доставки, тогда как библиотека называет пропавшего получателя точнее. Теперь проверка пропускает такой поток к её собственному отказу, а свои случаи опознаёт по позиции: связывание настроек не соблюдает required, поэтому имя может отсутствовать, а позиция есть всегда. Обе ветви прогнаны. Факт про документ конфигурации переехал к самому вызову: копирующий берёт код с комментарием, а не README. Заодно в README добавлено, что документ объявляет ещё и схему авторизации OAuth для управления, которого здесь нет, и снят повтор объяснения, уже данного в разделе про стартовые предупреждения.
…сузить её README называл только зарегистрированную схему, а без AddAuthorization рядом хост стартует чисто и отвечает 500 на каждую доставку - проверено запуском. Формально предложение утверждало необходимое условие и ложным не было, но читатель, применивший ровно названное, получал тот самый отказ, от которого предложение его отговаривает. Проверка потока сравнивала получателя с null, тогда как библиотека считает пустую строку отсутствием и требует обоих имён. Из-за этого поток с пустым получателем и поток без имени получали от примера сообщение про адрес доставки вместо точного библиотечного про пропавший член. Теперь проверка повторяет тест библиотеки. Прогнаны четыре формы объявления: второй поток без адреса даёт отказ с позицией и именем, поток без имени и поток с пустым получателем уходят к библиотеке и получают её сообщение, пустой элемент тоже. Сквозная доставка после правок работает.
…отсутствии Проверка потока без адреса доставки стояла на утверждении, что иначе передатчик стартует чисто и ничего не доставляет. Утверждение неверно для 2.4: ConfigurationStreamStore отказывается стартовать сам и называет обе починки - задать PushEndpointUrl либо дать передатчику адрес опроса. Моя проверка приходила первой и сообщала более узкое. Прогнано без неё: опечатка в имени ключа даёт библиотечный отказ до "Now listening", сквозная доставка на исправных настройках работает. Вместе с проверкой ушли два её следствия: разбор смешанного объявления, где она называла позицию не того потока, и необходимость повторять её тест идентификаторов.
Отказ ConfigurationStreamStore условен: он держится, пока опрос не предложен, а предложенным опрос становится ровно при вызове, отображающем API управления - к которому приглашает соседний комментарий. Читатель, принявший это приглашение, получает от той же опечатки поток, тихо обслуживаемый опросом, и передатчик, который стартует и ничего не доставляет push-приёмнику. Теперь условие названо там же, вместе с тем, что в этой паре доставляет только push: приёмник опроса не умеет, поэтому вторая починка из библиотечного сообщения здесь ничего не даёт.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
SharedSignalsSample: a transmitter that revokes a session and announces it, and a receiver that hears the announcement and closes its own, with a real HTTPS push between them.Why two applications
The question a reader arrives with is how a receiver tells a genuine event from a forged one. The answer is a signature, a published key set, an expected issuer and an expected audience, and in a single process all four are the application checking its own word. The second host is what makes any of them mean something.
What the end-to-end run needed
The draft built and both halves started, but nothing was delivered. Each of these was found by running it, not by reading:
AllowedReceiverAddresses. A receiver names its own delivery endpoint, so a transmitter that POSTs wherever it is told is an SSRF engine with the deployment's network position. Private hosts are refused by default, andlocalhostis exactly that case: the sweep refused every delivery until the operator named the destination. The list is derived from the declared streams, so the permitted address and the delivered-to address cannot be edited apart.StreamIssuer. SSF 1.0 Section 4.1.6 wantsissmatched against the issuer of the stream the event arrived on, not merely a trusted issuer. Unset, the receiver threw on its first token and answered 500.KeyIdfixed at"ssf-sign". A receiver caches keys bykidand refetches only on an unknown one, so a restarted transmitter signed with a new key under a name the receiver already held: no refetch, and every push answered 400.What the review round then corrected
A read of the prose against the code found several claims the sample did not honour. The substantive ones:
RolloverRefetchCooldown, 30 seconds). A push inside that window is refused against the stale key set and acknowledged out of the queue rather than retried, so the event is lost. That window is the same order as the delivery sweep, so a reader moving briskly would have seen the opposite of what the section promised. Both the README and the code comment now say it.IssuerAllowlistStepruns beforeSignatureStep.DefaultSubjectsModedid nothing here, being read only for streams created through the management API this sample does not map. Removed.SessionStoreclaimed to keep the session-named and user-named subjects apart; it handles only the first, and now says so.Configuration is also read as required rather than defaulted, so a lost key cannot yield an app quietly running on a hardcoded address.
Verified by running it
Empty before the revoke,
["alice-laptop-session"]after, receiver answering202. Restarting the transmitter and revoking again after the cooldown delivers the second event, which is the rollover working; revoking inside the cooldown does not, which is the cost the README now documents. Pointing the receiver at another transmitter address, or at an audience the transmitter does not address, gets every delivery refused.Packages rather than the sibling checkout
The draft referenced
..\..\..\Oidc.Server\src\. Those packages shipped as 2.4.0, so the sample references them and builds for a reader who cloned only this repository.Closes Abblix/Docs#36