In the previous chapter, we learned about the OneIdAuthenticationHandler, the main "security guard" managing the OneID login flow. We saw that one of its crucial tasks is to securely exchange the temporary code (received from OneID via the user's browser) for actual user tokens (ID token, access token) by talking directly to OneID's TokenEndpoint. This direct server-to-server communication happens over a secure "backchannel".
But how does your application prove its own identity during this sensitive backchannel exchange? How does OneID know that the request to swap the code for tokens is really coming from your registered application and not an imposter? This is where the OneIdAuthenticationBackChannelHandler comes in.
Imagine our security guard (OneIdAuthenticationHandler) needs to send a top-secret message (the authorization code) back to headquarters (OneID's Token Endpoint) to get the official entry badges (tokens). They can't just send it via regular mail; it needs to be highly secure. They need a special courier who knows how to:
- Prepare a Secure Pouch: Create a tamper-proof package for the message.
- Include Official Credentials: Put the building's official seal (your application's private certificate) on the pouch.
- Write a Signed Letter of Introduction: Craft a special, digitally signed letter (a JWT called a "client assertion") confirming the message is legitimately from your application.
The OneIdAuthenticationBackChannelHandler is this specialized courier and document preparer. It's a low-level component focused only on making the outgoing backchannel requests (specifically the token request) secure and verifiable. Its main jobs are:
- Finding the Certificate: Locating your application's private certificate based on the settings you provided in OneIdAuthenticationOptions (like
CertificateThumbprintorCertificateFilename). It uses a helper calledOneIdCertificateUtilityfor this. - Creating the Client Assertion: Generating a unique, short-lived, digitally signed JSON Web Token (JWT). This JWT contains claims identifying your application (
ClientId) and is signed using your private certificate. This proves to OneID that the request originated from the legitimate owner of the certificate associated with yourClientId. - Attaching Credentials: Ensuring both the client assertion JWT and the certificate itself are properly attached to the outgoing HTTP request being sent to OneID's Token Endpoint.
Essentially, it handles the specific security requirements OneID imposes for the critical step of exchanging the code for tokens, proving your application's identity beyond just the ClientId.
Important Note: You generally don't interact directly with OneIdAuthenticationBackChannelHandler. It works behind the scenes. The main OneIdAuthenticationHandler uses it automatically when making the backchannel call (the ExchangeCodeAsync step we saw previously). You configure the inputs it needs (certificate details) in OneIdAuthenticationOptions.
-
Certificate Lookup:
- Analogy: The courier needs to find the official company seal before preparing the package.
- How it works: It reads the
CertificateThumbprint(and store details) orCertificateFilename/Passwordfrom the OneIdAuthenticationOptions. It then usesOneIdCertificateUtilityto fetch the actualX509Certificate2object from the specified location (Windows Certificate Store or a file).
-
Client Assertion Creation:
- Analogy: The courier writes a formal letter stating, "This message is from Application XYZ, sent at this specific time," and signs it with an unforgeable signature using the company seal.
- How it works: It creates a JWT with specific claims:
iss(Issuer): Your application'sClientId.sub(Subject): Your application'sClientId.aud(Audience): The intended recipient, usually OneID's Token Endpoint URL (derived from OneIdAuthenticationOptions).iat(Issued At): The current time.exp(Expiration Time): A short time in the future (e.g., 20 minutes).jti(JWT ID): A unique random identifier for this specific token.
- It then digitally signs this JWT using the private key of the certificate it just looked up.
-
Attaching to Request:
- Analogy: The courier puts the signed letter (
client_assertion) inside the secure pouch (HttpRequestMessage) and makes sure the official seal (the certificate itself, used for the HTTPS connection) is visible. - How it works:
- It modifies the outgoing
HttpRequestMessagethat the OneIdAuthenticationHandler prepared for the token endpoint. - It adds the generated JWT as a parameter named
client_assertion. - It adds another parameter
client_assertion_typewith the valueurn:ietf:params:oauth:client-assertion-type:jwt-bearer. - As an
HttpClientHandler, it also automatically attaches the client certificate to the underlying HTTPS connection itself, allowing server-level validation.
- It modifies the outgoing
- Analogy: The courier puts the signed letter (
You don't call this handler directly. When you configure the OneID middleware using .AddOneId() (as seen in Chapter 3), the options you provide, particularly the certificate details in OneIdAuthenticationOptions, are used to create an instance of this OneIdAuthenticationBackChannelHandler. This instance is then automatically assigned to the BackchannelHttpHandler property of the main OneIdAuthenticationHandler.
// Simplified view from AddOneId() setup:
services.AddAuthentication()
.AddOneId(options => {
options.ClientId = "...";
// *** You configure the certificate here ***
options.CertificateThumbprint = "YOUR_CERT_THUMBPRINT";
options.CertificateStoreLocation = StoreLocation.LocalMachine;
options.CertificateStoreName = StoreName.My;
// ... other options ...
// *** Middleware automatically does this internally: ***
// options.BackchannelHttpHandler =
// new OneIdAuthenticationBackChannelHandler(options);
});When the OneIdAuthenticationHandler's ExchangeCodeAsync method makes the HTTP call to the token endpoint, it uses the configured BackchannelHttpHandler. This means our OneIdAuthenticationBackChannelHandler gets activated automatically to perform its security tasks just before the request goes out.
Let's trace the steps specifically involving the backchannel handler during the token exchange:
Simplified Flow:
- The main OneIdAuthenticationHandler decides it needs to exchange the
codefor tokens. - It prepares a standard
HttpRequestMessagecontaining thecode,client_id,grant_type, etc. - It uses its configured
Backchannel(which uses theOneIdAuthenticationBackChannelHandler) to send this request. OneIdAuthenticationBackChannelHandlerintercepts the outgoing request.- It reads certificate details from its configured
OneIdAuthenticationOptions. - It calls
OneIdCertificateUtility.FindCertificateByThumbprint(or reads from file) to get theX509Certificate2. - It creates the JWT client assertion, signing it with the certificate's private key.
- It modifies the
HttpRequestMessageto includeclient_assertionandclient_assertion_typeparameters. - It attaches the certificate to the HTTPS connection handler.
- It passes the modified request to the base
HttpClientHandlerto actually send it over the network. - OneID receives the request, validates the HTTPS certificate, validates the
client_assertionJWT signature and claims. - If valid, OneID processes the token exchange and sends back the tokens.
- The response travels back through the handlers to the main
OneIdAuthenticationHandler.
Simplified Diagram:
sequenceDiagram
participant MainHandler as YourApp (Main Handler)
participant BackChannelHandler as YourApp (BackChannel Handler)
participant BaseHttp as Base HttpClient
participant OneIDTokenEp as OneID Token Endpoint
MainHandler->>BackChannelHandler: Send Token Request (with code, client_id)
BackChannelHandler->>BackChannelHandler: Find Certificate (using Options)
BackChannelHandler->>BackChannelHandler: Create JWT Client Assertion
BackChannelHandler->>BackChannelHandler: Sign JWT with Certificate Key
BackChannelHandler->>BackChannelHandler: Add Assertion & Cert to Request
BackChannelHandler->>BaseHttp: Send Modified Request
BaseHttp->>+OneIDTokenEp: HTTPS Request + Cert + Assertion
OneIDTokenEp->>OneIDTokenEp: Validate Cert & Assertion
OneIDTokenEp-->>-BaseHttp: Tokens (ID, Access, Refresh)
BaseHttp-->>BackChannelHandler: Receive Token Response
BackChannelHandler-->>MainHandler: Forward Token Response
Code Insight:
Let's look at simplified snippets from OneIdAuthenticationBackChannelHandler.cs.
-
Finding the Certificate:
// From: src/AspNet.Security.OAuth.OneID/OneIdAuthenticationBackChannelHandler.cs // Inside the SendAsync method override: protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { X509Certificate2? cert = null; try { // Check if Thumbprint is configured in options if (!string.IsNullOrEmpty(_options.CertificateThumbprint)) { // Use utility to find the cert in the specified store cert = OneIdCertificateUtility.FindCertificateByThumbprint( _options.CertificateStoreName, _options.CertificateStoreLocation, _options.CertificateThumbprint, false); // 'false' means don't require validation here } // Else if Filename is configured... (simplified) else if (!string.IsNullOrEmpty(_options.CertificateFilename)) { // Read file bytes, create certificate object var certBytes = File.ReadAllBytes(_options.CertificateFilename); cert = new X509Certificate2(certBytes, _options.CertificatePassword, ...); } if (cert == null) { throw new InvalidOperationException("Certificate not found!"); } // ... rest of the logic using 'cert' ...
Explanation: This code checks the
_options(which are your OneIdAuthenticationOptions) to see how the certificate is specified (thumbprint or file) and then uses the helperOneIdCertificateUtilityor file reading to load the actual certificate object. -
Creating the Client Assertion JWT:
// From: src/AspNet.Security.OAuth.OneID/OneIdAuthenticationBackChannelHandler.cs // Still inside SendAsync, after getting the 'cert': // Wrap the cert in a security key object X509SecurityKey key = new(cert); // Specify the signing algorithm SigningCredentials credentials = new(key, SecurityAlgorithms.RsaSha256); // Set issuance and expiration times (e.g., expires in 20 mins) var now = DateTimeOffset.Now.ToUnixTimeSeconds(); var expire = DateTimeOffset.Now.AddMinutes(20).ToUnixTimeSeconds(); // Define the claims for the JWT var permClaims = new List<Claim> { new("iss", _options.ClientId), // Issuer = Your App ID new("sub", _options.ClientId), // Subject = Your App ID new("aud", _options.Audience), // Audience = OneID Token Endpoint new("iat", now.ToString(...), ClaimValueTypes.Integer64), // Issued At new("exp", expire.ToString(...), ClaimValueTypes.Integer64), // Expiration new("jti", Guid.NewGuid().ToString("N")) // Unique JWT ID }; // Create the JWT object with claims and signing credentials var token = new JwtSecurityToken(claims: permClaims, signingCredentials: credentials); // Convert the token object into its string representation var jwtToken = new JwtSecurityTokenHandler().WriteToken(token); // 'jwtToken' now holds the signed client assertion string. // ... code to add jwtToken to the request follows ...
Explanation: This part constructs the JWT. It defines the necessary claims (who issued it, who it's for, when it expires) using details from your options and the current time. It then uses the certificate's key (
credentials) to sign this information, producing the finaljwtTokenstring. -
Adding Assertion to the Request:
// From: src/AspNet.Security.OAuth.OneID/OneIdAuthenticationBackChannelHandler.cs // Still inside SendAsync, after creating 'jwtToken': // Read the original request parameters var oldContent = await request.Content.ReadAsStringAsync(cancellationToken); var data = oldContent.Split('&').ToDictionary(x => x.Split('=')[0], x => Uri.UnescapeDataString(x.Split('=')[1])); // Add/replace the client assertion parameters data["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; data["client_assertion"] = jwtToken; // Add the signed JWT string // Add the audience parameter again (as required by OneID) data["aud"] = ClaimNames.ApiAudience; // Put the modified parameters back into the request content request.Content = new FormUrlEncodedContent(data); // Pass the modified request to the base handler to send return await base.SendAsync(request, cancellationToken);
Explanation: This code takes the existing parameters being sent to the token endpoint (like
code,client_id,grant_type), adds theclient_assertion(containing the JWT we just created) andclient_assertion_type, and updates theaudparameter. It then repackages these parameters into therequest.Contentbefore finally callingbase.SendAsyncto send the request on its way. The certificate attachment for the HTTPS connection happens automatically because this class inherits fromHttpClientHandlerand the certificate is loaded.
The OneIdAuthenticationBackChannelHandler is a vital but hidden component responsible for the specialized security required during the backchannel token exchange. It acts like a secure courier, automatically finding your application's certificate (based on your OneIdAuthenticationOptions), creating a digitally signed "letter of introduction" (the client assertion JWT), and attaching these credentials to the outgoing request to OneID's Token Endpoint. By handling this complex security procedure, it allows the main OneIdAuthenticationHandler to securely prove your application's identity when trading the authorization code for user tokens. You don't configure it directly, but knowing it's there helps understand the robust security mechanisms protecting the OneID flow.
In the next chapter, we'll look at OneIdHelper, a utility class offering helpful functions you can use directly in your application code, particularly for managing tokens after login.
Next: Chapter 6: OneIdHelper
Generated by AI Codebase Knowledge Builder