11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4- using Microsoft . AspNetCore . Builder ;
5- using Microsoft . AspNetCore . Hosting ;
6- using Microsoft . AspNetCore . Http ;
7- using Microsoft . AspNetCore . TestHost ;
8- using Microsoft . Extensions . DependencyInjection ;
9- using Microsoft . Extensions . Hosting ;
10- using Microsoft . Net . Http . Headers ;
114using System ;
5+ using System . Collections . Generic ;
126using System . Linq ;
137using System . Net ;
148using System . Net . Http ;
159using System . Security . Claims ;
1610using System . Text ;
11+ using System . Text . Encodings . Web ;
1712using System . Threading . Tasks ;
13+ using Microsoft . AspNetCore . Builder ;
14+ using Microsoft . AspNetCore . DataProtection ;
15+ using Microsoft . AspNetCore . Hosting ;
16+ using Microsoft . AspNetCore . Http ;
17+ using Microsoft . AspNetCore . TestHost ;
18+ using Microsoft . Extensions . DependencyInjection ;
19+ using Microsoft . Extensions . Hosting ;
20+ using Microsoft . Extensions . Logging . Abstractions ;
21+ using Microsoft . Net . Http . Headers ;
1822using Xunit ;
1923
2024namespace Microsoft . AspNetCore . Authentication . Twitter
@@ -381,6 +385,158 @@ public async Task BadCallbackCallsRemoteAuthFailedWithState()
381385 Assert . Equal ( HttpStatusCode . NotAcceptable , response . StatusCode ) ;
382386 }
383387
388+ [ Fact ]
389+ public async Task CanSignIn ( )
390+ {
391+ var stateFormat = new SecureDataFormat < RequestToken > ( new RequestTokenSerializer ( ) , new EphemeralDataProtectionProvider ( NullLoggerFactory . Instance ) . CreateProtector ( "TwitterTest" ) ) ;
392+ using var host = await CreateHost ( ( options ) =>
393+ {
394+ options . ConsumerKey = "Test App Id" ;
395+ options . ConsumerSecret = "PLACEHOLDER" ;
396+ options . SaveTokens = true ;
397+ options . StateDataFormat = stateFormat ;
398+ options . BackchannelHttpHandler = new TestHttpMessageHandler
399+ {
400+ Sender = req =>
401+ {
402+ if ( req . RequestUri . GetComponents ( UriComponents . SchemeAndServer | UriComponents . Path , UriFormat . UriEscaped ) == "https://api.twitter.com/oauth/access_token" )
403+ {
404+ var res = new HttpResponseMessage ( HttpStatusCode . OK ) ;
405+ var content = new Dictionary < string , string > ( )
406+ {
407+ [ "oauth_token" ] = "Test Access Token" ,
408+ [ "oauth_token_secret" ] = "PLACEHOLDER" ,
409+ [ "user_id" ] = "123456" ,
410+ [ "screen_name" ] = "@dotnet"
411+ } ;
412+ res . Content = new FormUrlEncodedContent ( content ) ;
413+ return res ;
414+ }
415+ return null ;
416+ }
417+ } ;
418+ } ) ;
419+
420+ var token = new RequestToken ( )
421+ {
422+ Token = "TestToken" ,
423+ TokenSecret = "PLACEHOLDER" ,
424+ Properties = new ( )
425+ } ;
426+
427+ var correlationKey = ".xsrf" ;
428+ var correlationValue = "TestCorrelationId" ;
429+ token . Properties . Items . Add ( correlationKey , correlationValue ) ;
430+ token . Properties . RedirectUri = "/me" ;
431+ var state = stateFormat . Protect ( token ) ;
432+ using var server = host . GetTestServer ( ) ;
433+ var transaction = await server . SendAsync (
434+ "https://example.com/signin-twitter?oauth_token=TestToken&oauth_verifier=TestVerifier" ,
435+ $ ".AspNetCore.Correlation.{ correlationValue } =N;__TwitterState={ UrlEncoder . Default . Encode ( state ) } ") ;
436+ Assert . Equal ( HttpStatusCode . Redirect , transaction . Response . StatusCode ) ;
437+ Assert . Equal ( "/me" , transaction . Response . Headers . GetValues ( "Location" ) . First ( ) ) ;
438+
439+ var authCookie = transaction . AuthenticationCookieValue ;
440+ transaction = await server . SendAsync ( "https://example.com/me" , authCookie ) ;
441+ Assert . Equal ( HttpStatusCode . OK , transaction . Response . StatusCode ) ;
442+ var expectedIssuer = TwitterDefaults . AuthenticationScheme ;
443+ Assert . Equal ( "@dotnet" , transaction . FindClaimValue ( ClaimTypes . Name , expectedIssuer ) ) ;
444+ Assert . Equal ( "123456" , transaction . FindClaimValue ( ClaimTypes . NameIdentifier , expectedIssuer ) ) ;
445+ Assert . Equal ( "123456" , transaction . FindClaimValue ( "urn:twitter:userid" , expectedIssuer ) ) ;
446+ Assert . Equal ( "@dotnet" , transaction . FindClaimValue ( "urn:twitter:screenname" , expectedIssuer ) ) ;
447+
448+ transaction = await server . SendAsync ( "https://example.com/tokens" , authCookie ) ;
449+ Assert . Equal ( HttpStatusCode . OK , transaction . Response . StatusCode ) ;
450+ Assert . Equal ( "Test Access Token" , transaction . FindTokenValue ( "access_token" ) ) ;
451+ Assert . Equal ( "PLACEHOLDER" , transaction . FindTokenValue ( "access_token_secret" ) ) ;
452+ }
453+
454+ [ Fact ]
455+ public async Task CanFetchUserDetails ( )
456+ {
457+ var verifyCredentialsEndpoint = "https://api.twitter.com/1.1/account/verify_credentials.json" ;
458+ var finalVerifyCredentialsEndpoint = string . Empty ;
459+ var finalAuthorizationParameter = string . Empty ;
460+ var stateFormat = new SecureDataFormat < RequestToken > ( new RequestTokenSerializer ( ) , new EphemeralDataProtectionProvider ( NullLoggerFactory . Instance ) . CreateProtector ( "TwitterTest" ) ) ;
461+ using var host = await CreateHost ( ( options ) =>
462+ {
463+ options . ConsumerKey = "Test App Id" ;
464+ options . ConsumerSecret = "PLACEHOLDER" ;
465+ options . RetrieveUserDetails = true ;
466+ options . StateDataFormat = stateFormat ;
467+ options . BackchannelHttpHandler = new TestHttpMessageHandler
468+ {
469+ Sender = req =>
470+ {
471+ if ( req . RequestUri . GetComponents ( UriComponents . SchemeAndServer | UriComponents . Path , UriFormat . UriEscaped ) == "https://api.twitter.com/oauth/access_token" )
472+ {
473+ var res = new HttpResponseMessage ( HttpStatusCode . OK ) ;
474+ var content = new Dictionary < string , string > ( )
475+ {
476+ [ "oauth_token" ] = "Test Access Token" ,
477+ [ "oauth_token_secret" ] = "PLACEHOLDER" ,
478+ [ "user_id" ] = "123456" ,
479+ [ "screen_name" ] = "@dotnet"
480+ } ;
481+ res . Content = new FormUrlEncodedContent ( content ) ;
482+ return res ;
483+ }
484+ if ( req . RequestUri . GetComponents ( UriComponents . SchemeAndServer | UriComponents . Path , UriFormat . UriEscaped ) ==
485+ new Uri ( verifyCredentialsEndpoint ) . GetComponents ( UriComponents . SchemeAndServer | UriComponents . Path , UriFormat . UriEscaped ) )
486+ {
487+ finalVerifyCredentialsEndpoint = req . RequestUri . ToString ( ) ;
488+ finalAuthorizationParameter = req . Headers . Authorization . Parameter ;
489+ var res = new HttpResponseMessage ( HttpStatusCode . OK ) ;
490+ var graphResponse = "{ \" email\" : \" Test email\" }" ;
491+ res . Content = new StringContent ( graphResponse , Encoding . UTF8 ) ;
492+ return res ;
493+ }
494+ return null ;
495+ }
496+ } ;
497+ } ) ;
498+
499+ var token = new RequestToken ( )
500+ {
501+ Token = "TestToken" ,
502+ TokenSecret = "PLACEHOLDER" ,
503+ Properties = new ( )
504+ } ;
505+
506+ var correlationKey = ".xsrf" ;
507+ var correlationValue = "TestCorrelationId" ;
508+ token . Properties . Items . Add ( correlationKey , correlationValue ) ;
509+ token . Properties . RedirectUri = "/me" ;
510+ var state = stateFormat . Protect ( token ) ;
511+ using var server = host . GetTestServer ( ) ;
512+ var transaction = await server . SendAsync (
513+ "https://example.com/signin-twitter?oauth_token=TestToken&oauth_verifier=TestVerifier" ,
514+ $ ".AspNetCore.Correlation.{ correlationValue } =N;__TwitterState={ UrlEncoder . Default . Encode ( state ) } ") ;
515+ Assert . Equal ( HttpStatusCode . Redirect , transaction . Response . StatusCode ) ;
516+ Assert . Equal ( "/me" , transaction . Response . Headers . GetValues ( "Location" ) . First ( ) ) ;
517+
518+ Assert . Equal ( 1 , finalVerifyCredentialsEndpoint . Count ( c => c == '?' ) ) ;
519+ Assert . Contains ( "include_email=true" , finalVerifyCredentialsEndpoint ) ;
520+
521+ Assert . Contains ( "oauth_consumer_key=" , finalAuthorizationParameter ) ;
522+ Assert . Contains ( "oauth_nonce=" , finalAuthorizationParameter ) ;
523+ Assert . Contains ( "oauth_signature=" , finalAuthorizationParameter ) ;
524+ Assert . Contains ( "oauth_signature_method=" , finalAuthorizationParameter ) ;
525+ Assert . Contains ( "oauth_timestamp=" , finalAuthorizationParameter ) ;
526+ Assert . Contains ( "oauth_token=" , finalAuthorizationParameter ) ;
527+ Assert . Contains ( "oauth_version=" , finalAuthorizationParameter ) ;
528+
529+ var authCookie = transaction . AuthenticationCookieValue ;
530+ transaction = await server . SendAsync ( "https://example.com/me" , authCookie ) ;
531+ Assert . Equal ( HttpStatusCode . OK , transaction . Response . StatusCode ) ;
532+ var expectedIssuer = TwitterDefaults . AuthenticationScheme ;
533+ Assert . Equal ( "@dotnet" , transaction . FindClaimValue ( ClaimTypes . Name , expectedIssuer ) ) ;
534+ Assert . Equal ( "123456" , transaction . FindClaimValue ( ClaimTypes . NameIdentifier , expectedIssuer ) ) ;
535+ Assert . Equal ( "123456" , transaction . FindClaimValue ( "urn:twitter:userid" , expectedIssuer ) ) ;
536+ Assert . Equal ( "@dotnet" , transaction . FindClaimValue ( "urn:twitter:screenname" , expectedIssuer ) ) ;
537+ Assert . Equal ( "Test email" , transaction . FindClaimValue ( ClaimTypes . Email , expectedIssuer ) ) ;
538+ }
539+
384540 private static async Task < IHost > CreateHost ( Action < TwitterOptions > options , Func < HttpContext , Task < bool > > handler = null )
385541 {
386542 var host = new HostBuilder ( )
@@ -405,6 +561,16 @@ private static async Task<IHost> CreateHost(Action<TwitterOptions> options, Func
405561 {
406562 await Assert . ThrowsAsync < InvalidOperationException > ( ( ) => context . ForbidAsync ( "Twitter" ) ) ;
407563 }
564+ else if ( req . Path == new PathString ( "/me" ) )
565+ {
566+ await res . DescribeAsync ( context . User ) ;
567+ }
568+ else if ( req . Path == new PathString ( "/tokens" ) )
569+ {
570+ var result = await context . AuthenticateAsync ( TestExtensions . CookieAuthenticationScheme ) ;
571+ var tokens = result . Properties . GetTokens ( ) ;
572+ await res . DescribeAsync ( tokens ) ;
573+ }
408574 else if ( handler == null || ! await handler ( context ) )
409575 {
410576 await next ( context ) ;
@@ -418,8 +584,8 @@ private static async Task<IHost> CreateHost(Action<TwitterOptions> options, Func
418584 o . SignInScheme = "External" ;
419585 options ( o ) ;
420586 } ;
421- services . AddAuthentication ( )
422- . AddCookie ( "External" , _ => { } )
587+ services . AddAuthentication ( TestExtensions . CookieAuthenticationScheme )
588+ . AddCookie ( TestExtensions . CookieAuthenticationScheme , o => o . ForwardChallenge = TwitterDefaults . AuthenticationScheme )
423589 . AddTwitter ( wrapOptions ) ;
424590 } ) )
425591 . Build ( ) ;
0 commit comments