1- using System . Security . Claims ;
1+ using System . Runtime . InteropServices ;
2+ using System . Security . Claims ;
3+ using System . Security . Cryptography ;
4+ using System . Security . Cryptography . X509Certificates ;
25using Microsoft . Extensions . Hosting ;
36using OpenIddict . Abstractions ;
47using OpenIddict . Client ;
@@ -148,6 +151,19 @@ await _service.AuthenticateInteractivelyAsync(new()
148151 var type = await GetSelectedGrantTypeAsync ( registration , configuration , stoppingToken ) ;
149152 if ( type is GrantTypes . DeviceCode )
150153 {
154+ // Note: the OpenIddict server stack supports mTLS-based token binding for public clients:
155+ // while these clients cannot authenticate using a TLS client certificate, the certificate
156+ // can be used to bind the refresh (and access) tokens returned by the authorization server
157+ // to the client application, which prevents such tokens from being used without providing a
158+ // proof-of-possession matching the TLS client certificate used when the token was acquired.
159+ //
160+ // While this sample deliberately doesn't store the generated certificate in a persistent
161+ // location, the certificate used for token binding should typically be stored in the user
162+ // certificate store to be reloaded across application restarts in a real-world application.
163+ var certificate = configuration . TlsClientCertificateBoundAccessTokens is true
164+ ? GenerateEphemeralTlsClientCertificate ( )
165+ : null ;
166+
151167 // Ask OpenIddict to send a device authorization request and write
152168 // the complete verification endpoint URI to the console output.
153169 var result = await _service . ChallengeUsingDeviceAsync ( new ( )
@@ -181,7 +197,8 @@ [yellow]Please visit [link]{result.VerificationUri}[/] and enter
181197 DeviceCode = result . DeviceCode ,
182198 Interval = result . Interval ,
183199 ProviderName = provider ,
184- Timeout = result . ExpiresIn < TimeSpan . FromMinutes ( 5 ) ? result . ExpiresIn : TimeSpan . FromMinutes ( 5 )
200+ Timeout = result . ExpiresIn < TimeSpan . FromMinutes ( 5 ) ? result . ExpiresIn : TimeSpan . FromMinutes ( 5 ) ,
201+ TokenBindingCertificate = certificate
185202 } ) ;
186203
187204 AnsiConsole . MarkupLine ( "[green]Device authentication successful:[/]" ) ;
@@ -223,7 +240,8 @@ await _service.RevokeTokenAsync(new()
223240 {
224241 CancellationToken = stoppingToken ,
225242 ProviderName = provider ,
226- RefreshToken = response . RefreshToken
243+ RefreshToken = response . RefreshToken ,
244+ TokenBindingCertificate = certificate
227245 } ) ) . Principal ) ) ;
228246 }
229247 }
@@ -232,6 +250,10 @@ await _service.RevokeTokenAsync(new()
232250 {
233251 var ( username , password ) = ( await GetUsernameAsync ( stoppingToken ) , await GetPasswordAsync ( stoppingToken ) ) ;
234252
253+ var certificate = configuration . TlsClientCertificateBoundAccessTokens is true
254+ ? GenerateEphemeralTlsClientCertificate ( )
255+ : null ;
256+
235257 AnsiConsole . MarkupLine ( "[cyan]Sending the token request.[/]" ) ;
236258
237259 // Ask OpenIddict to authenticate the user using the resource owner password credentials grant.
@@ -241,7 +263,8 @@ await _service.RevokeTokenAsync(new()
241263 ProviderName = provider ,
242264 Username = username ,
243265 Password = password ,
244- Scopes = [ Scopes . OfflineAccess ]
266+ Scopes = [ Scopes . OfflineAccess ] ,
267+ TokenBindingCertificate = certificate
245268 } ) ;
246269
247270 AnsiConsole . MarkupLine ( "[green]Resource owner password credentials authentication successful:[/]" ) ;
@@ -283,7 +306,8 @@ await _service.RevokeTokenAsync(new()
283306 {
284307 CancellationToken = stoppingToken ,
285308 ProviderName = provider ,
286- RefreshToken = response . RefreshToken
309+ RefreshToken = response . RefreshToken ,
310+ TokenBindingCertificate = certificate
287311 } ) ) . Principal ) ) ;
288312 }
289313 }
@@ -309,6 +333,10 @@ await GetRequestedTokenTypeAsync(stoppingToken),
309333 await GetSubjectTokenAsync ( stoppingToken ) ,
310334 await GetActorTokenAsync ( stoppingToken ) ) ;
311335
336+ var certificate = configuration . TlsClientCertificateBoundAccessTokens is true
337+ ? GenerateEphemeralTlsClientCertificate ( )
338+ : null ;
339+
312340 AnsiConsole . MarkupLine ( "[cyan]Sending the token request.[/]" ) ;
313341
314342 // Ask OpenIddict to send the specified subject token (and actor token, if available).
@@ -320,7 +348,8 @@ await GetSubjectTokenAsync(stoppingToken),
320348 ProviderName = provider ,
321349 RequestedTokenType = identifier ,
322350 SubjectToken = subject . Token ,
323- SubjectTokenType = subject . TokenType
351+ SubjectTokenType = subject . TokenType ,
352+ TokenBindingCertificate = certificate
324353 } ) ;
325354
326355 AnsiConsole . MarkupLine ( "[green]Token exchange authentication successful:[/]" ) ;
@@ -368,7 +397,8 @@ await RefreshTokenAsync(stoppingToken))
368397 {
369398 CancellationToken = stoppingToken ,
370399 ProviderName = provider ,
371- RefreshToken = response . IssuedToken
400+ RefreshToken = response . IssuedToken ,
401+ TokenBindingCertificate = certificate
372402 } ) ) . Principal ) ) ;
373403 }
374404
@@ -381,7 +411,8 @@ await RefreshTokenAsync(stoppingToken))
381411 {
382412 CancellationToken = stoppingToken ,
383413 ProviderName = provider ,
384- RefreshToken = response . RefreshToken
414+ RefreshToken = response . RefreshToken ,
415+ TokenBindingCertificate = certificate
385416 } ) ) . Principal ) ) ;
386417 }
387418 }
@@ -800,5 +831,44 @@ static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(
800831
801832 return Task . Run ( Prompt , cancellationToken ) . WaitAsync ( cancellationToken ) ;
802833 }
834+
835+ #if SUPPORTS_CERTIFICATE_GENERATION
836+ static X509Certificate2 GenerateEphemeralTlsClientCertificate ( )
837+ {
838+ using var algorithm = RSA . Create ( keySizeInBits : 4096 ) ;
839+
840+ var subject = new X500DistinguishedName ( "CN=Self-signed certificate" ) ;
841+ var request = new CertificateRequest ( subject , algorithm , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
842+ request . CertificateExtensions . Add ( new X509KeyUsageExtension ( X509KeyUsageFlags . DigitalSignature , critical : true ) ) ;
843+ request . CertificateExtensions . Add ( new X509EnhancedKeyUsageExtension ( [ new Oid ( "1.3.6.1.5.5.7.3.2" ) ] , critical : true ) ) ;
844+
845+ var certificate = request . CreateSelfSigned ( DateTimeOffset . UtcNow , DateTimeOffset . UtcNow . AddYears ( 2 ) ) ;
846+
847+ // On Windows, a certificate loaded from PEM-encoded material is ephemeral and
848+ // cannot be directly used with TLS, as Schannel cannot access it in this case.
849+ //
850+ // To work this limitation, the certificate is exported and re-imported from a
851+ // PFX blob to ensure the private key is persisted in a way that Schannel can use.
852+ //
853+ // In a real world application, the certificate wouldn't be embedded in the source code
854+ // and would be installed in the certificate store, making this workaround unnecessary.
855+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
856+ {
857+ #if SUPPORTS_CERTIFICATE_LOADER
858+ certificate = X509CertificateLoader . LoadPkcs12 (
859+ data : certificate . Export ( X509ContentType . Pfx , string . Empty ) ,
860+ password : string . Empty ,
861+ keyStorageFlags : X509KeyStorageFlags . DefaultKeySet ) ;
862+ #else
863+ certificate = new X509Certificate2 (
864+ rawData : certificate . Export ( X509ContentType . Pfx , string . Empty ) ,
865+ password : string . Empty ,
866+ keyStorageFlags : X509KeyStorageFlags . DefaultKeySet ) ;
867+ #endif
868+ }
869+
870+ return certificate ;
871+ }
872+ #endif
803873 }
804874}
0 commit comments