Repository navigation
Make authentication behave the same in every client - #80
Merged
Merged
Conversation
Swift and Kotlin carried the connection token in the transport URL as access_token, where proxy logs and error reports keep it. Both now send it as an Authorization header on the WebSocket upgrade, the SSE stream and posts and every Long Polling request, fetched again on every connect. A new transportCredential option (.query / QUERY) keeps the URL for servers that read it only there. UseSignalARRRAccessTokenValidation now only fills in a missing Authorization header instead of overwriting one the client sent. The middleware is documented, with a table of where each client sends the connection token. The integration test hub reports where the transport request carried the token; Kotlin and Swift test both modes over all three transports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clients used five names for two credentials and two different defaults: .NET and TypeScript kept connection and message credential apart, Swift and Kotlin quietly used one for both. Every client now has connectionCredential, messageCredential and credential for both (WithConnectionCredential, WithMessageCredential, WithCredential in .NET), with nothing coupled implicitly. The former options keep working and their behaviour: WithAuthorization, authorization, messageAccessTokenProvider and Swift's accessTokenFactory are deprecated. A credential set in two places is an error: at create in .NET, TypeScript and Kotlin, from start() in Swift, whose create does not throw. In .NET and TypeScript the connection credential becomes SignalR's AccessTokenProvider/accessTokenFactory, so it needs the overload where SignalARRR builds the connection; TypeScript gains create(url, options, configure?). Every client guide and the authorization guide show the same table. Two doc errors are fixed: file transfers and challenge answers use the message credential, not the connection's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Error codes in TypeScript and Swift: invoke() in TypeScript rejects with code and normalizedCode, Swift's HARRRError carries code, version and normalizedCode, and both export HARRRErrorCodes. Both accept an envelope by the same test as .NET and Kotlin, so a code-only envelope is no longer dropped. The HTTP status of a rejected negotiate: statusCode on Kotlin's NegotiationFailedException, Swift's SignalRError and the error TypeScript's start() rejects with (SignalR's JS client only mentions it in the message). .NET already had HttpRequestException.StatusCode; a test pins it down. A headers option in the Swift client, applied to negotiate and every transport request, as in Kotlin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The .NET Framework client took one bare accessTokenProvider parameter which, despite SignalR's meaning of that name, was the message credential. It now has the .NET client's options: Create(builder, options => options .WithCredential/WithConnectionCredential/WithMessageCredential), with the same errors for a credential set in two places. The former parameter keeps working and is marked obsolete. A test on net48 confirms that SignalR's .NET Framework client sends the connection token as a header on WebSocket, SSE and Long Polling; the docs' transport table and the client comparison say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tials # Conflicts: # CHANGELOG.md # website/changelog.md
ASP.NET Core answers the first poll after negotiate at once and without data; it completes the connection on the server. The .NET, TypeScript and Kotlin clients send it when they open the transport. The Swift client did not, so the handshake read got the empty answer and failed with "Incomplete handshake response" - Long Polling never connected. The new transport tests were the first to exercise it end to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Why
An app with .NET, TypeScript, iOS and Android clients needs authentication to behave the same everywhere. It didn't. Swift and Kotlin put the connection token into the transport URL, where proxy logs and error reports keep it. The clients used five names for two credentials and had two different defaults. TypeScript and Swift could not tell
unauthorizedfrom other errors without matching the message. Background: the amZettel feature request "Token beim Transport als Header statt in der URL".What
Token as a header on the transport (
a9e3f5a)Authorizationheader on the WebSocket upgrade, on SSE and on Long Polling. It is fetched again on every connect.transportCredential = .query/QUERYkeeps the URL for servers that read the token only there.UseSignalARRRAccessTokenValidationnow only fills in a missing header. It no longer overwrites a header the client sent. The middleware is now documented.The same credential options in every client (
03b79ea,e64352a)credential,connectionCredential,messageCredential. In .NET and .NET Framework they areWithCredential,WithConnectionCredentialandWithMessageCredential.Create/createin .NET, .NET Framework, TypeScript and Kotlin;start()in Swift, whosecreatedoes not throw.WithAuthorization,authorization,messageAccessTokenProvider, Swift'saccessTokenFactory:, and the .NET FrameworkaccessTokenProviderparameter.create(url, options, configure?), so SignalARRR can hand the connection credential to SignalR.Remaining gaps closed (
b9c763f)code,normalizedCode,HARRRErrorCodes) in TypeScript and Swift, which .NET and Kotlin already had.statusCodeon a rejected negotiate:NegotiationFailedException.SignalRError.start()rejects with. SignalR's JS client mentions the status only in its message.HttpRequestException.StatusCode; a test now pins it down.headersoption in Swift.Verification
TransportCredential()that reports where the transport request carried the token.WithCredentialis tested end to end against a hub with[Authorize].🤖 Generated with Claude Code