// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. using System; using System.Linq; using System.Net; using System.Net.Http; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.Tests; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.Authentication.Twitter { public class TwitterTests { private void ConfigureDefaults(TwitterOptions o) { o.ConsumerKey = "whatever"; o.ConsumerSecret = "whatever"; o.SignInScheme = "auth1"; } [Fact] public async Task CanForwardDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; }); var forwardDefault = new TestHandler(); services.AddSingleton(forwardDefault); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; Assert.Equal(0, forwardDefault.AuthenticateCount); Assert.Equal(0, forwardDefault.ForbidCount); Assert.Equal(0, forwardDefault.ChallengeCount); Assert.Equal(0, forwardDefault.SignInCount); Assert.Equal(0, forwardDefault.SignOutCount); await context.AuthenticateAsync(); Assert.Equal(1, forwardDefault.AuthenticateCount); await context.ForbidAsync(); Assert.Equal(1, forwardDefault.ForbidCount); await context.ChallengeAsync(); Assert.Equal(1, forwardDefault.ChallengeCount); await Assert.ThrowsAsync(() => context.SignOutAsync()); await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); } [Fact] public async Task ForwardSignInThrows() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardSignOut = "specific"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); } [Fact] public async Task ForwardSignOutThrows() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardSignOut = "specific"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await Assert.ThrowsAsync(() => context.SignOutAsync()); } [Fact] public async Task ForwardForbidWinsOverDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardForbid = "specific"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await context.ForbidAsync(); Assert.Equal(0, specific.SignOutCount); Assert.Equal(0, specific.AuthenticateCount); Assert.Equal(1, specific.ForbidCount); Assert.Equal(0, specific.ChallengeCount); Assert.Equal(0, specific.SignInCount); Assert.Equal(0, forwardDefault.AuthenticateCount); Assert.Equal(0, forwardDefault.ForbidCount); Assert.Equal(0, forwardDefault.ChallengeCount); Assert.Equal(0, forwardDefault.SignInCount); Assert.Equal(0, forwardDefault.SignOutCount); } [Fact] public async Task ForwardAuthenticateWinsOverDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardAuthenticate = "specific"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await context.AuthenticateAsync(); Assert.Equal(0, specific.SignOutCount); Assert.Equal(1, specific.AuthenticateCount); Assert.Equal(0, specific.ForbidCount); Assert.Equal(0, specific.ChallengeCount); Assert.Equal(0, specific.SignInCount); Assert.Equal(0, forwardDefault.AuthenticateCount); Assert.Equal(0, forwardDefault.ForbidCount); Assert.Equal(0, forwardDefault.ChallengeCount); Assert.Equal(0, forwardDefault.SignInCount); Assert.Equal(0, forwardDefault.SignOutCount); } [Fact] public async Task ForwardChallengeWinsOverDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("specific", "specific"); o.AddScheme("auth1", "auth1"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardChallenge = "specific"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await context.ChallengeAsync(); Assert.Equal(0, specific.SignOutCount); Assert.Equal(0, specific.AuthenticateCount); Assert.Equal(0, specific.ForbidCount); Assert.Equal(1, specific.ChallengeCount); Assert.Equal(0, specific.SignInCount); Assert.Equal(0, forwardDefault.AuthenticateCount); Assert.Equal(0, forwardDefault.ForbidCount); Assert.Equal(0, forwardDefault.ChallengeCount); Assert.Equal(0, forwardDefault.SignInCount); Assert.Equal(0, forwardDefault.SignOutCount); } [Fact] public async Task ForwardSelectorWinsOverDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("selector", "selector"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardDefaultSelector = _ => "selector"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var selector = new TestHandler3(); services.AddSingleton(selector); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await context.AuthenticateAsync(); Assert.Equal(1, selector.AuthenticateCount); await context.ForbidAsync(); Assert.Equal(1, selector.ForbidCount); await context.ChallengeAsync(); Assert.Equal(1, selector.ChallengeCount); await Assert.ThrowsAsync(() => context.SignOutAsync()); await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); Assert.Equal(0, forwardDefault.AuthenticateCount); Assert.Equal(0, forwardDefault.ForbidCount); Assert.Equal(0, forwardDefault.ChallengeCount); Assert.Equal(0, forwardDefault.SignInCount); Assert.Equal(0, forwardDefault.SignOutCount); Assert.Equal(0, specific.AuthenticateCount); Assert.Equal(0, specific.ForbidCount); Assert.Equal(0, specific.ChallengeCount); Assert.Equal(0, specific.SignInCount); Assert.Equal(0, specific.SignOutCount); } [Fact] public async Task NullForwardSelectorUsesDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("selector", "selector"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardDefaultSelector = _ => null; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var selector = new TestHandler3(); services.AddSingleton(selector); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await context.AuthenticateAsync(); Assert.Equal(1, forwardDefault.AuthenticateCount); await context.ForbidAsync(); Assert.Equal(1, forwardDefault.ForbidCount); await context.ChallengeAsync(); Assert.Equal(1, forwardDefault.ChallengeCount); await Assert.ThrowsAsync(() => context.SignOutAsync()); await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); Assert.Equal(0, selector.AuthenticateCount); Assert.Equal(0, selector.ForbidCount); Assert.Equal(0, selector.ChallengeCount); Assert.Equal(0, selector.SignInCount); Assert.Equal(0, selector.SignOutCount); Assert.Equal(0, specific.AuthenticateCount); Assert.Equal(0, specific.ForbidCount); Assert.Equal(0, specific.ChallengeCount); Assert.Equal(0, specific.SignInCount); Assert.Equal(0, specific.SignOutCount); } [Fact] public async Task SpecificForwardWinsOverSelectorAndDefault() { var services = new ServiceCollection().AddLogging(); services.AddAuthentication(o => { o.DefaultScheme = TwitterDefaults.AuthenticationScheme; o.AddScheme("auth1", "auth1"); o.AddScheme("selector", "selector"); o.AddScheme("specific", "specific"); }) .AddTwitter(o => { ConfigureDefaults(o); o.ForwardDefault = "auth1"; o.ForwardDefaultSelector = _ => "selector"; o.ForwardAuthenticate = "specific"; o.ForwardChallenge = "specific"; o.ForwardSignIn = "specific"; o.ForwardSignOut = "specific"; o.ForwardForbid = "specific"; }); var specific = new TestHandler(); services.AddSingleton(specific); var forwardDefault = new TestHandler2(); services.AddSingleton(forwardDefault); var selector = new TestHandler3(); services.AddSingleton(selector); var sp = services.BuildServiceProvider(); var context = new DefaultHttpContext(); context.RequestServices = sp; await context.AuthenticateAsync(); Assert.Equal(1, specific.AuthenticateCount); await context.ForbidAsync(); Assert.Equal(1, specific.ForbidCount); await context.ChallengeAsync(); Assert.Equal(1, specific.ChallengeCount); await Assert.ThrowsAsync(() => context.SignOutAsync()); await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); Assert.Equal(0, forwardDefault.AuthenticateCount); Assert.Equal(0, forwardDefault.ForbidCount); Assert.Equal(0, forwardDefault.ChallengeCount); Assert.Equal(0, forwardDefault.SignInCount); Assert.Equal(0, forwardDefault.SignOutCount); Assert.Equal(0, selector.AuthenticateCount); Assert.Equal(0, selector.ForbidCount); Assert.Equal(0, selector.ChallengeCount); Assert.Equal(0, selector.SignInCount); Assert.Equal(0, selector.SignOutCount); } [Fact] public async Task VerifySignInSchemeCannotBeSetToSelf() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; o.SignInScheme = TwitterDefaults.AuthenticationScheme; }); var error = await Assert.ThrowsAsync(() => server.SendAsync("https://example.com/challenge")); Assert.Contains("cannot be set to itself", error.Message); } [Fact] public async Task VerifySchemeDefaults() { var services = new ServiceCollection(); services.AddAuthentication().AddTwitter(); var sp = services.BuildServiceProvider(); var schemeProvider = sp.GetRequiredService(); var scheme = await schemeProvider.GetSchemeAsync(TwitterDefaults.AuthenticationScheme); Assert.NotNull(scheme); Assert.Equal("TwitterHandler", scheme.HandlerType.Name); Assert.Equal(TwitterDefaults.AuthenticationScheme, scheme.DisplayName); } [Fact] public async Task ChallengeWillTriggerApplyRedirectEvent() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; o.Events = new TwitterEvents { OnRedirectToAuthorizationEndpoint = context => { context.Response.Redirect(context.RedirectUri + "&custom=test"); return Task.FromResult(0); } }; o.BackchannelHttpHandler = new TestHttpMessageHandler { Sender = BackchannelRequestToken }; }, async context => { await context.ChallengeAsync("Twitter"); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var query = transaction.Response.Headers.Location.Query; Assert.Contains("custom=test", query); } /// /// Validates the Twitter Options to check if the Consumer Key is missing in the TwitterOptions and if so throws the ArgumentException /// /// [Fact] public async Task ThrowsIfClientIdMissing() { var server = CreateServer(o => { o.ConsumerSecret = "Test Consumer Secret"; }); await Assert.ThrowsAsync("ConsumerKey", async () => await server.SendAsync("http://example.com/challenge")); } /// /// Validates the Twitter Options to check if the Consumer Secret is missing in the TwitterOptions and if so throws the ArgumentException /// /// [Fact] public async Task ThrowsIfClientSecretMissing() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; }); await Assert.ThrowsAsync("ConsumerSecret", async () => await server.SendAsync("http://example.com/challenge")); } [Fact] public async Task BadSignInWillThrow() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; }); // Send a bogus sign in var error = await Assert.ThrowsAnyAsync(() => server.SendAsync("https://example.com/signin-twitter")); Assert.Equal("Invalid state cookie.", error.GetBaseException().Message); } [Fact] public async Task SignInThrows() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; }); var transaction = await server.SendAsync("https://example.com/signIn"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task SignOutThrows() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; }); var transaction = await server.SendAsync("https://example.com/signOut"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task ForbidThrows() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; }); var transaction = await server.SendAsync("https://example.com/signOut"); Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); } [Fact] public async Task ChallengeWillTriggerRedirection() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; o.BackchannelHttpHandler = new TestHttpMessageHandler { Sender = BackchannelRequestToken }; }, async context => { await context.ChallengeAsync("Twitter"); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location.AbsoluteUri; Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); } [Fact] public async Task BadCallbackCallsRemoteAuthFailedWithState() { var server = CreateServer(o => { o.ConsumerKey = "Test Consumer Key"; o.ConsumerSecret = "Test Consumer Secret"; o.BackchannelHttpHandler = new TestHttpMessageHandler { Sender = BackchannelRequestToken }; o.Events = new TwitterEvents() { OnRemoteFailure = context => { Assert.NotNull(context.Failure); Assert.Equal("The user denied permissions.", context.Failure.Message); Assert.NotNull(context.Properties); Assert.Equal("testvalue", context.Properties.Items["testkey"]); context.Response.StatusCode = StatusCodes.Status406NotAcceptable; context.HandleResponse(); return Task.CompletedTask; } }; }, async context => { var properties = new AuthenticationProperties(); properties.Items["testkey"] = "testvalue"; await context.ChallengeAsync("Twitter", properties); return true; }); var transaction = await server.SendAsync("http://example.com/challenge"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); var location = transaction.Response.Headers.Location.AbsoluteUri; Assert.Contains("https://api.twitter.com/oauth/authenticate?oauth_token=", location); Assert.True(transaction.Response.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookie)); Assert.True(SetCookieHeaderValue.TryParseList(setCookie.ToList(), out var setCookieValues)); Assert.Single(setCookieValues); var setCookieValue = setCookieValues.Single(); var cookie = new CookieHeaderValue(setCookieValue.Name, setCookieValue.Value); var request = new HttpRequestMessage(HttpMethod.Get, "/signin-twitter?denied=ABCDEFG"); request.Headers.Add(HeaderNames.Cookie, cookie.ToString()); var client = server.CreateClient(); var response = await client.SendAsync(request); Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode); } private static TestServer CreateServer(Action options, Func> handler = null) { var builder = new WebHostBuilder() .Configure(app => { app.UseAuthentication(); app.Use(async (context, next) => { var req = context.Request; var res = context.Response; if (req.Path == new PathString("/signIn")) { await Assert.ThrowsAsync(() => context.SignInAsync("Twitter", new ClaimsPrincipal())); } else if (req.Path == new PathString("/signOut")) { await Assert.ThrowsAsync(() => context.SignOutAsync("Twitter")); } else if (req.Path == new PathString("/forbid")) { await Assert.ThrowsAsync(() => context.ForbidAsync("Twitter")); } else if (handler == null || ! await handler(context)) { await next(); } }); }) .ConfigureServices(services => { Action wrapOptions = o => { o.SignInScheme = "External"; options(o); }; services.AddAuthentication() .AddCookie("External", _ => { }) .AddTwitter(wrapOptions); }); return new TestServer(builder); } private HttpResponseMessage BackchannelRequestToken(HttpRequestMessage req) { if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") { return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", Encoding.UTF8, "application/x-www-form-urlencoded") }; } throw new NotImplementedException(req.RequestUri.AbsoluteUri); } } }