diff --git a/src/MusicStore/Mocks/Common/CustomStateDataFormat.cs b/src/MusicStore/Mocks/Common/CustomStateDataFormat.cs new file mode 100644 index 0000000000..b91c141c7f --- /dev/null +++ b/src/MusicStore/Mocks/Common/CustomStateDataFormat.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security; +using Newtonsoft.Json; + +namespace MusicStore.Mocks.Common +{ + public class CustomStateDataFormat : ISecureDataFormat + { + private static string lastSavedAuthenticationProperties; + + public string Protect(AuthenticationProperties data) + { + lastSavedAuthenticationProperties = Serialize(data); + return "ValidStateData"; + } + + public AuthenticationProperties Unprotect(string state) + { + return state == "ValidStateData" ? DeSerialize(lastSavedAuthenticationProperties) : null; + } + + private string Serialize(AuthenticationProperties data) + { + return JsonConvert.SerializeObject(data, Formatting.Indented); + } + + private AuthenticationProperties DeSerialize(string state) + { + return JsonConvert.DeserializeObject(state); + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Common/Extensions.cs b/src/MusicStore/Mocks/Common/Extensions.cs new file mode 100644 index 0000000000..9e2c89aecc --- /dev/null +++ b/src/MusicStore/Mocks/Common/Extensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace System.Net +{ + public static class Extensions + { + /// + /// https://github.com/aspnet/HttpAbstractions/issues/121 - Helpers implemented here until that. + /// + /// + public static Dictionary ParseQueryString(this Uri uri) + { + var queryParameters = Uri.UnescapeDataString(uri.Query.TrimStart('?')).Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + var queryItemCollection = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (queryParameters != null && queryParameters.Length > 0) + { + foreach (var queryParameter in queryParameters) + { + var queryParameterParts = queryParameter.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + var value = queryParameterParts.Length == 1 ? string.Empty : queryParameterParts[1]; + queryItemCollection.Add(queryParameterParts[0], value); + } + } + + return queryItemCollection; + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Common/Helpers.cs b/src/MusicStore/Mocks/Common/Helpers.cs new file mode 100644 index 0000000000..73e900e379 --- /dev/null +++ b/src/MusicStore/Mocks/Common/Helpers.cs @@ -0,0 +1,15 @@ +using System; + +namespace MusicStore.Mocks.Common +{ + internal class Helpers + { + internal static void ThrowIfConditionFailed(Func condition, string errorMessage) + { + if (!condition()) + { + throw new Exception(errorMessage); + } + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Facebook/FacebookMockBackChannelHttpHandler.cs b/src/MusicStore/Mocks/Facebook/FacebookMockBackChannelHttpHandler.cs new file mode 100644 index 0000000000..57c7b2821c --- /dev/null +++ b/src/MusicStore/Mocks/Facebook/FacebookMockBackChannelHttpHandler.cs @@ -0,0 +1,48 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Net; +using MusicStore.Mocks.Common; + +namespace MusicStore.Mocks.Facebook +{ + /// + /// Summary description for FacebookMockBackChannelHttpHandler + /// + public class FacebookMockBackChannelHttpHandler : WebRequestHandler + { + protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage(); + var queryParameters = request.RequestUri.ParseQueryString(); + + if (request.RequestUri.AbsoluteUri.StartsWith("https://graph.facebook.com/oauth/access_token")) + { + if (queryParameters["grant_type"] == "authorization_code") + { + if (queryParameters["code"] == "ValidCode") + { + Helpers.ThrowIfConditionFailed(() => queryParameters["redirect_uri"].EndsWith("signin-facebook"), "Redirect URI is not ending with /signin-facebook"); + Helpers.ThrowIfConditionFailed(() => queryParameters["client_id"] == "[AppId]", "Invalid client Id received"); + Helpers.ThrowIfConditionFailed(() => queryParameters["client_secret"] == "[AppSecret]", "Invalid client secret received"); + response.Content = new StringContent("access_token=ValidAccessToken&expires=100"); + } + } + } + else if (request.RequestUri.AbsoluteUri.StartsWith("https://graph.facebook.com/me")) + { + Helpers.ThrowIfConditionFailed(() => queryParameters["appsecret_proof"] != null, "appsecret_proof is null"); + if (queryParameters["access_token"] == "ValidAccessToken") + { + response.Content = new StringContent("{\"id\":\"Id\",\"name\":\"AspnetvnextTest AspnetvnextTest\",\"first_name\":\"AspnetvnextTest\",\"last_name\":\"AspnetvnextTest\",\"link\":\"https:\\/\\/www.facebook.com\\/myLink\",\"username\":\"AspnetvnextTest.AspnetvnextTest.7\",\"gender\":\"male\",\"email\":\"AspnetvnextTest\\u0040gmail.com\",\"timezone\":-7,\"locale\":\"en_US\",\"verified\":true,\"updated_time\":\"2013-08-06T20:38:48+0000\",\"CertValidatorInvoked\":\"ValidAccessToken\"}"); + } + else + { + response.Content = new StringContent("{\"error\":{\"message\":\"Invalid OAuth access token.\",\"type\":\"OAuthException\",\"code\":190}}"); + } + } + + return await Task.FromResult(response); + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Facebook/FacebookNotifications.cs b/src/MusicStore/Mocks/Facebook/FacebookNotifications.cs new file mode 100644 index 0000000000..5f716287fc --- /dev/null +++ b/src/MusicStore/Mocks/Facebook/FacebookNotifications.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNet.Security.Facebook; +using Microsoft.AspNet.Security.OAuth; +using MusicStore.Mocks.Common; +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace MusicStore.Mocks.Facebook +{ + /// + /// Summary description for FacebookNotifications + /// + internal class FacebookNotifications + { + internal static async Task OnAuthenticated(FacebookAuthenticatedContext context) + { + if (context.Identity != null) + { + Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", ""); + Helpers.ThrowIfConditionFailed(() => context.Email == "AspnetvnextTest@gmail.com", ""); + Helpers.ThrowIfConditionFailed(() => context.Id == "Id", ""); + Helpers.ThrowIfConditionFailed(() => context.Link == "https://www.facebook.com/myLink", ""); + Helpers.ThrowIfConditionFailed(() => context.Name == "AspnetvnextTest AspnetvnextTest", ""); + Helpers.ThrowIfConditionFailed(() => context.UserName == "AspnetvnextTest.AspnetvnextTest.7", ""); + Helpers.ThrowIfConditionFailed(() => context.User.SelectToken("id").ToString() == context.Id, ""); + Helpers.ThrowIfConditionFailed(() => context.ExpiresIn.Value == TimeSpan.FromSeconds(100), ""); + Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", ""); + context.Identity.AddClaim(new Claim("ManageStore", "false")); + } + + await Task.FromResult(0); + } + + internal static async Task OnReturnEndpoint(OAuthReturnEndpointContext context) + { + if (context.Identity != null && context.SignInAsAuthenticationType == "External") + { + //This way we will know all notifications were fired. + var manageStoreClaim = context.Identity.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault(); + if (manageStoreClaim != null) + { + context.Identity.RemoveClaim(manageStoreClaim); + context.Identity.AddClaim(new Claim("ManageStore", "true")); + } + } + + await Task.FromResult(0); + } + + internal static void OnApplyRedirect(OAuthApplyRedirectContext context) + { + context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom"); + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Google/GoogleMockBackChannelHttpHandler.cs b/src/MusicStore/Mocks/Google/GoogleMockBackChannelHttpHandler.cs new file mode 100644 index 0000000000..a14c3d1c40 --- /dev/null +++ b/src/MusicStore/Mocks/Google/GoogleMockBackChannelHttpHandler.cs @@ -0,0 +1,48 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text; +using Microsoft.AspNet.WebUtilities; + +namespace MusicStore.Mocks.Google +{ + /// + /// Summary description for GoogleMockBackChannelHttpHandler + /// + public class GoogleMockBackChannelHttpHandler : WebRequestHandler + { + protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage(); + + if (request.RequestUri.AbsoluteUri.StartsWith("https://accounts.google.com/o/oauth2/token")) + { + var formData = FormHelpers.ParseForm(await request.Content.ReadAsStringAsync()); + if (formData["grant_type"] == "authorization_code") + { + if (formData["code"] == "ValidCode") + { + if (formData["redirect_uri"] != null && formData["redirect_uri"].EndsWith("signin-google") && + formData["client_id"] == "[ClientId]" && formData["client_secret"] == "[ClientSecret]") + { + response.Content = new StringContent("{\"access_token\":\"ValidAccessToken\",\"refresh_token\":\"ValidRefreshToken\",\"token_type\":\"Bearer\",\"expires_in\":\"1200\",\"id_token\":\"Token\"}", Encoding.UTF8, "application/json"); + } + } + } + } + else if (request.RequestUri.AbsoluteUri.StartsWith("https://www.googleapis.com/plus/v1/people/me")) + { + if (request.Headers.Authorization.Parameter == "ValidAccessToken") + { + response.Content = new StringContent("{ \"kind\": \"plus#person\",\n \"etag\": \"\\\"YFr-hUROXQN7IOa3dUHg9dQ8eq0/2hY18HdHEP8NLykSTVEiAhkKsBE\\\"\",\n \"gender\": \"male\",\n \"emails\": [\n {\n \"value\": \"AspnetvnextTest@gmail.com\",\n \"type\": \"account\"\n }\n ],\n \"objectType\": \"person\",\n \"id\": \"106790274378320830963\",\n \"displayName\": \"AspnetvnextTest AspnetvnextTest\",\n \"name\": {\n \"familyName\": \"AspnetvnextTest\",\n \"givenName\": \"FirstName\"\n },\n \"url\": \"https://plus.google.com/106790274378320830963\",\n \"image\": {\n \"url\": \"https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg?sz=50\"\n },\n \"isPlusUser\": true,\n \"language\": \"en\",\n \"circledByCount\": 0,\n \"verified\": false\n}\n", Encoding.UTF8, "application/json"); + } + else + { + response.Content = new StringContent("{\"error\":{\"message\":\"Invalid OAuth access token.\",\"type\":\"OAuthException\",\"code\":190}}"); + } + } + + return response; + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Google/GoogleNotifications.cs b/src/MusicStore/Mocks/Google/GoogleNotifications.cs new file mode 100644 index 0000000000..e7bb60cfa5 --- /dev/null +++ b/src/MusicStore/Mocks/Google/GoogleNotifications.cs @@ -0,0 +1,55 @@ +using Microsoft.AspNet.Security.Google; +using Microsoft.AspNet.Security.OAuth; +using MusicStore.Mocks.Common; +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace MusicStore.Mocks.Google +{ + /// + /// Summary description for GoogleNotifications + /// + internal class GoogleNotifications + { + internal static async Task OnAuthenticated(GoogleAuthenticatedContext context) + { + if (context.Identity != null) + { + Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", "Access token is not valid"); + Helpers.ThrowIfConditionFailed(() => context.RefreshToken == "ValidRefreshToken", "Refresh token is not valid"); + Helpers.ThrowIfConditionFailed(() => context.Email == "AspnetvnextTest@gmail.com", "Email is not valid"); + Helpers.ThrowIfConditionFailed(() => context.Id == "106790274378320830963", "Id is not valid"); + Helpers.ThrowIfConditionFailed(() => context.FamilyName == "AspnetvnextTest", "FamilyName is not valid"); + Helpers.ThrowIfConditionFailed(() => context.Name == "AspnetvnextTest AspnetvnextTest", "Name is not valid"); + Helpers.ThrowIfConditionFailed(() => context.ExpiresIn.Value == TimeSpan.FromSeconds(1200), "ExpiresIn is not valid"); + Helpers.ThrowIfConditionFailed(() => context.User != null, "User object is not valid"); + context.Identity.AddClaim(new Claim("ManageStore", "false")); + } + + await Task.FromResult(0); + } + + internal static async Task OnReturnEndpoint(OAuthReturnEndpointContext context) + { + if (context.Identity != null && context.SignInAsAuthenticationType == "External") + { + //This way we will know all notifications were fired. + var manageStoreClaim = context.Identity.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault(); + if (manageStoreClaim != null) + { + context.Identity.RemoveClaim(manageStoreClaim); + context.Identity.AddClaim(new Claim("ManageStore", "true")); + } + } + + await Task.FromResult(0); + } + + internal static void OnApplyRedirect(OAuthApplyRedirectContext context) + { + context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom"); + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Readme.md b/src/MusicStore/Mocks/Readme.md new file mode 100644 index 0000000000..53632c201e --- /dev/null +++ b/src/MusicStore/Mocks/Readme.md @@ -0,0 +1 @@ +Only for testing purposes. Not needed for production deployments. \ No newline at end of file diff --git a/src/MusicStore/Mocks/StartupSocialTesting.cs b/src/MusicStore/Mocks/StartupSocialTesting.cs new file mode 100644 index 0000000000..dc285b5299 --- /dev/null +++ b/src/MusicStore/Mocks/StartupSocialTesting.cs @@ -0,0 +1,192 @@ +using System; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.Data.Entity; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using MusicStore.Models; +using Microsoft.AspNet.Security.Facebook; +using Microsoft.AspNet.Security.Google; +using Microsoft.AspNet.Security.Twitter; +using Microsoft.AspNet.Security.MicrosoftAccount; +using Microsoft.AspNet.Security; +using Microsoft.AspNet.MemoryCache; +using MusicStore.Mocks.Common; +using MusicStore.Mocks.Facebook; +using MusicStore.Mocks.Twitter; +using MusicStore.Mocks.Google; + +namespace MusicStore +{ + public class StartupSocialTesting + { + public void Configure(IBuilder app) + { + Console.WriteLine("Social Testing mode..."); + //Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1' is found in both the registered sources, + //then the later source will win. By this way a Local config can be overridden by a different setting while deployed remotely. + var configuration = new Configuration(); + configuration.AddJsonFile("LocalConfig.json"); + configuration.AddEnvironmentVariables(); //All environment variables in the process's context flow in as configuration values. + + //Error page middleware displays a nice formatted HTML page for any unhandled exceptions in the request pipeline. + //Note: ErrorPageOptions.ShowAll to be used only at development time. Not recommended for production. + app.UseErrorPage(ErrorPageOptions.ShowAll); + + app.SetDefaultSignInAsAuthenticationType("External"); + + app.UseServices(services => + { + //If this type is present - we're on mono + var runningOnMono = Type.GetType("Mono.Runtime") != null; + + // Add EF services to the services container + if (runningOnMono) + { + services.AddEntityFramework() + .AddInMemoryStore(); + } + else + { + services.AddEntityFramework() + .AddSqlServer(); + } + + services.AddScoped(); + + // Configure DbContext + services.SetupOptions(options => + { + options.DefaultAdminUserName = configuration.Get("DefaultAdminUsername"); + options.DefaultAdminPassword = configuration.Get("DefaultAdminPassword"); + if (runningOnMono) + { + options.UseInMemoryStore(); + } + else + { + options.UseSqlServer(configuration.Get("Data:DefaultConnection:ConnectionString")); + } + }); + + // Add Identity services to the services container + services.AddIdentitySqlServer() + .AddAuthentication(); + + // Add MVC services to the services container + services.AddMvc(); + + //Add all SignalR related services to IoC. + services.AddSignalR(); + + //Add InMemoryCache + //Currently not able to AddSingleTon + services.AddInstance(new MemoryCache()); + }); + + //Configure SignalR + app.UseSignalR(); + + // Add static files to the request pipeline + app.UseStaticFiles(); + + // Add cookie-based authentication to the request pipeline + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = "External", + AuthenticationMode = AuthenticationMode.Passive, + ExpireTimeSpan = TimeSpan.FromMinutes(5) + }); + + // Add cookie-based authentication to the request pipeline + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = ClaimsIdentityOptions.DefaultAuthenticationType, + LoginPath = new PathString("/Account/Login") + }); + + app.UseTwoFactorSignInCookies(); + + var facebookOptions = new FacebookAuthenticationOptions() + { + AppId = "[AppId]", + AppSecret = "[AppSecret]", + Notifications = new FacebookAuthenticationNotifications() + { + OnAuthenticated = FacebookNotifications.OnAuthenticated, + OnReturnEndpoint = FacebookNotifications.OnReturnEndpoint, + OnApplyRedirect = FacebookNotifications.OnApplyRedirect + }, + BackchannelHttpHandler = new FacebookMockBackChannelHttpHandler(), + StateDataFormat = new CustomStateDataFormat() + }; + + facebookOptions.Scope.Add("email"); + facebookOptions.Scope.Add("read_friendlists"); + facebookOptions.Scope.Add("user_checkins"); + + app.UseFacebookAuthentication(facebookOptions); + + app.UseGoogleAuthentication(new GoogleAuthenticationOptions() + { + ClientId = "[ClientId]", + ClientSecret = "[ClientSecret]", + AccessType = "offline", + Notifications = new GoogleAuthenticationNotifications() + { + OnAuthenticated = GoogleNotifications.OnAuthenticated, + OnReturnEndpoint = GoogleNotifications.OnReturnEndpoint, + OnApplyRedirect = GoogleNotifications.OnApplyRedirect + }, + StateDataFormat = new CustomStateDataFormat(), + BackchannelHttpHandler = new GoogleMockBackChannelHttpHandler() + }); + + app.UseTwitterAuthentication(new TwitterAuthenticationOptions() + { + ConsumerKey = "[ConsumerKey]", + ConsumerSecret = "[ConsumerSecret]", + Notifications = new TwitterAuthenticationNotifications() + { + OnAuthenticated = TwitterNotifications.OnAuthenticated, + OnReturnEndpoint = TwitterNotifications.OnReturnEndpoint, + OnApplyRedirect = TwitterNotifications.OnApplyRedirect + }, + StateDataFormat = new CustomTwitterStateDataFormat(), + BackchannelHttpHandler = new TwitterMockBackChannelHttpHandler() + }); + + app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions() + { + Caption = "MicrosoftAccount - Requires project changes", + ClientId = "[ClientId]", + ClientSecret = "[ClientSecret]", + }); + + // Add MVC to the request pipeline + app.UseMvc(routes => + { + routes.MapRoute( + name: "areaRoute", + template: "{area:exists}/{controller}/{action}", + defaults: new { action = "Index" }); + + routes.MapRoute( + name: "default", + template: "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }); + + routes.MapRoute( + name: "api", + template: "{controller}/{id?}"); + }); + + //Populates the MusicStore sample data + SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait(); + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Twitter/CustomTwitterStateDataFormat.cs b/src/MusicStore/Mocks/Twitter/CustomTwitterStateDataFormat.cs new file mode 100644 index 0000000000..58fe11967c --- /dev/null +++ b/src/MusicStore/Mocks/Twitter/CustomTwitterStateDataFormat.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNet.Security; +using Microsoft.AspNet.Security.Twitter.Messages; +using Newtonsoft.Json; + +namespace MusicStore.Mocks.Twitter +{ + /// + /// Summary description for CustomTwitterStateDataFormat + /// + public class CustomTwitterStateDataFormat : ISecureDataFormat + { + private static string lastSavedRequestToken; + + public string Protect(RequestToken data) + { + data.Token = "valid_oauth_token"; + lastSavedRequestToken = Serialize(data); + return "valid_oauth_token"; + } + + public RequestToken Unprotect(string state) + { + return state == "valid_oauth_token" ? DeSerialize(lastSavedRequestToken) : null; + } + + private string Serialize(RequestToken data) + { + return JsonConvert.SerializeObject(data, Formatting.Indented); + } + + private RequestToken DeSerialize(string state) + { + return JsonConvert.DeserializeObject(state); + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Twitter/TwitterMockBackChannelHttpHandler.cs b/src/MusicStore/Mocks/Twitter/TwitterMockBackChannelHttpHandler.cs new file mode 100644 index 0000000000..f2fccb1c34 --- /dev/null +++ b/src/MusicStore/Mocks/Twitter/TwitterMockBackChannelHttpHandler.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.WebUtilities; +using System.Collections.Generic; +using System.Net; + +namespace MusicStore.Mocks.Twitter +{ + /// + /// Summary description for TwitterMockBackChannelHttpHandler + /// + public class TwitterMockBackChannelHttpHandler : WebRequestHandler + { + private static bool RequestTokenEndpointInvoked = false; + + protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var response = new HttpResponseMessage(); + Console.WriteLine(request.RequestUri.AbsoluteUri); + + if (request.RequestUri.AbsoluteUri.StartsWith("https://api.twitter.com/oauth/access_token")) + { + var formData = FormHelpers.ParseForm(await request.Content.ReadAsStringAsync()); + if (formData["oauth_verifier"] == "valid_oauth_verifier") + { + if (RequestTokenEndpointInvoked) + { + var response_Form_data = new List>() + { + new KeyValuePair("oauth_token", "valid_oauth_token"), + new KeyValuePair("oauth_token_secret", "valid_oauth_token_secret"), + new KeyValuePair("user_id", "valid_user_id"), + new KeyValuePair("screen_name", "valid_screen_name"), + }; + + response.Content = new FormUrlEncodedContent(response_Form_data); + } + else + { + response.StatusCode = HttpStatusCode.InternalServerError; + response.Content = new StringContent("RequestTokenEndpoint is not invoked"); + } + } + } + else if (request.RequestUri.AbsoluteUri.StartsWith("https://api.twitter.com/oauth/request_token")) + { + var response_Form_data = new List>() + { + new KeyValuePair("oauth_callback_confirmed", "true"), + new KeyValuePair("oauth_token", "valid_oauth_token"), + new KeyValuePair("oauth_token_secret", "valid_oauth_token_secret") + }; + + RequestTokenEndpointInvoked = true; + response.Content = new FormUrlEncodedContent(response_Form_data); + } + + return response; + } + } +} \ No newline at end of file diff --git a/src/MusicStore/Mocks/Twitter/TwitterNotifications.cs b/src/MusicStore/Mocks/Twitter/TwitterNotifications.cs new file mode 100644 index 0000000000..00180a46d9 --- /dev/null +++ b/src/MusicStore/Mocks/Twitter/TwitterNotifications.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNet.Security.Twitter; +using MusicStore.Mocks.Common; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace MusicStore.Mocks.Twitter +{ + /// + /// Summary description for TwitterNotifications + /// + internal class TwitterNotifications + { + internal static async Task OnAuthenticated(TwitterAuthenticatedContext context) + { + if (context.Identity != null) + { + Helpers.ThrowIfConditionFailed(() => context.UserId == "valid_user_id", "UserId is not valid"); + Helpers.ThrowIfConditionFailed(() => context.ScreenName == "valid_screen_name", "ScreenName is not valid"); + Helpers.ThrowIfConditionFailed(() => context.AccessToken == "valid_oauth_token", "AccessToken is not valid"); + Helpers.ThrowIfConditionFailed(() => context.AccessTokenSecret == "valid_oauth_token_secret", "AccessTokenSecret is not valid"); + context.Identity.AddClaim(new Claim("ManageStore", "false")); + } + + await Task.FromResult(0); + } + + internal static async Task OnReturnEndpoint(TwitterReturnEndpointContext context) + { + if (context.Identity != null && context.SignInAsAuthenticationType == "External") + { + //This way we will know all notifications were fired. + var manageStoreClaim = context.Identity.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault(); + if (manageStoreClaim != null) + { + context.Identity.RemoveClaim(manageStoreClaim); + context.Identity.AddClaim(new Claim("ManageStore", "true")); + } + } + + await Task.FromResult(0); + } + + internal static void OnApplyRedirect(TwitterApplyRedirectContext context) + { + context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom"); + } + } +} \ No newline at end of file diff --git a/test/E2ETests/Extensions.cs b/test/E2ETests/Extensions.cs index 0f901fe922..b864f677a6 100644 --- a/test/E2ETests/Extensions.cs +++ b/test/E2ETests/Extensions.cs @@ -1,4 +1,4 @@ -using System.Net; +using System.Collections.Generic; namespace System.Net { @@ -16,5 +16,27 @@ namespace System.Net return null; } + + /// + /// https://github.com/aspnet/HttpAbstractions/issues/121 - Helpers implemented here until that. + /// + /// + public static Dictionary ParseQueryString(this Uri uri) + { + var queryParameters = Uri.UnescapeDataString(uri.Query.TrimStart('?')).Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + var queryItemCollection = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (queryParameters != null && queryParameters.Length > 0) + { + foreach (var queryParameter in queryParameters) + { + var queryParameterParts = queryParameter.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries); + var value = queryParameterParts.Length == 1 ? string.Empty : queryParameterParts[1]; + queryItemCollection.Add(queryParameterParts[0], value); + } + } + + return queryItemCollection; + } } } diff --git a/test/E2ETests/FacebookLoginScenarios.cs b/test/E2ETests/FacebookLoginScenarios.cs new file mode 100644 index 0000000000..4ba0bcba0d --- /dev/null +++ b/test/E2ETests/FacebookLoginScenarios.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net; +using Xunit; + +namespace E2ETests +{ + public partial class SmokeTests + { + private void LoginWithFacebook() + { + httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = false }; + httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(ApplicationBaseUrl) }; + + var response = httpClient.GetAsync("Account/Login").Result; + ThrowIfResponseStatusNotOk(response); + var responseContent = response.Content.ReadAsStringAsync().Result; + Console.WriteLine("Signing in with Facebook account"); + var formParameters = new List> + { + new KeyValuePair("provider", "Facebook"), + new KeyValuePair("returnUrl", "/"), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/ExternalLogin")), + }; + + var content = new FormUrlEncodedContent(formParameters.ToArray()); + response = httpClient.PostAsync("Account/ExternalLogin", content).Result; + Assert.Equal("https://www.facebook.com/dialog/oauth", response.Headers.Location.AbsoluteUri.Replace(response.Headers.Location.Query, string.Empty)); + var queryItems = response.Headers.Location.ParseQueryString(); + Assert.Equal("code", queryItems["response_type"]); + Assert.Equal("[AppId]", queryItems["client_id"]); + Assert.Equal(ApplicationBaseUrl + "signin-facebook", queryItems["redirect_uri"]); + Assert.Equal("email,read_friendlists,user_checkins", queryItems["scope"]); + Assert.Equal("ValidStateData", queryItems["state"]); + Assert.Equal("custom", queryItems["custom_redirect_uri"]); + //Check for the correlation cookie + Assert.NotNull(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Correlation.Facebook")); + + //This is just to generate a correlation cookie. Previous step would generate this cookie, but we have reset the handler now. + httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true }; + httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(ApplicationBaseUrl) }; + + response = httpClient.GetAsync("Account/Login").Result; + responseContent = response.Content.ReadAsStringAsync().Result; + formParameters = new List> + { + new KeyValuePair("provider", "Facebook"), + new KeyValuePair("returnUrl", "/"), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/ExternalLogin")), + }; + + content = new FormUrlEncodedContent(formParameters.ToArray()); + response = httpClient.PostAsync("Account/ExternalLogin", content).Result; + + //Post a message to the Facebook middleware + response = httpClient.GetAsync("signin-facebook?code=ValidCode&state=ValidStateData").Result; + //This should land us in ExternalLoginCallBack - this action is not implemented yet. We need to wait to complete automation. + + //Correlation cookie not getting cleared after successful signin? + //Assert.Null(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Correlation.Facebook")); + } + } +} \ No newline at end of file diff --git a/test/E2ETests/GoogleLoginScenarios.cs b/test/E2ETests/GoogleLoginScenarios.cs new file mode 100644 index 0000000000..2bc8a1ef02 --- /dev/null +++ b/test/E2ETests/GoogleLoginScenarios.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net; +using Xunit; + +namespace E2ETests +{ + public partial class SmokeTests + { + private void LoginWithGoogle() + { + httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = false }; + httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(ApplicationBaseUrl) }; + + var response = httpClient.GetAsync("Account/Login").Result; + ThrowIfResponseStatusNotOk(response); + var responseContent = response.Content.ReadAsStringAsync().Result; + Console.WriteLine("Signing in with Google account"); + var formParameters = new List> + { + new KeyValuePair("provider", "Google"), + new KeyValuePair("returnUrl", "/"), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/ExternalLogin")), + }; + + var content = new FormUrlEncodedContent(formParameters.ToArray()); + response = httpClient.PostAsync("Account/ExternalLogin", content).Result; + Assert.Equal("https://accounts.google.com/o/oauth2/auth", response.Headers.Location.AbsoluteUri.Replace(response.Headers.Location.Query, string.Empty)); + var queryItems = response.Headers.Location.ParseQueryString(); + Assert.Equal("code", queryItems["response_type"]); + Assert.Equal("offline", queryItems["access_type"]); + Assert.Equal("[ClientId]", queryItems["client_id"]); + Assert.Equal(ApplicationBaseUrl + "signin-google", queryItems["redirect_uri"]); + Assert.Equal("openid profile email", queryItems["scope"]); + Assert.Equal("ValidStateData", queryItems["state"]); + Assert.Equal("custom", queryItems["custom_redirect_uri"]); + //Check for the correlation cookie + Assert.NotNull(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Correlation.Google")); + + //This is just to generate a correlation cookie. Previous step would generate this cookie, but we have reset the handler now. + httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true }; + httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(ApplicationBaseUrl) }; + + response = httpClient.GetAsync("Account/Login").Result; + responseContent = response.Content.ReadAsStringAsync().Result; + formParameters = new List> + { + new KeyValuePair("provider", "Google"), + new KeyValuePair("returnUrl", "/"), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/ExternalLogin")), + }; + + content = new FormUrlEncodedContent(formParameters.ToArray()); + response = httpClient.PostAsync("Account/ExternalLogin", content).Result; + + //Post a message to the Google middleware + response = httpClient.GetAsync("signin-google?code=ValidCode&state=ValidStateData").Result; + //This should land us in ExternalLoginCallBack - this action is not implemented yet. We need to wait to complete automation. + + //Correlation cookie not getting cleared after successful signin? + //Assert.Null(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl)).GetCookieWithName(".AspNet.Correlation.Google")); + } + } +} \ No newline at end of file diff --git a/test/E2ETests/TwitterLoginScenarios.cs b/test/E2ETests/TwitterLoginScenarios.cs new file mode 100644 index 0000000000..2490d75142 --- /dev/null +++ b/test/E2ETests/TwitterLoginScenarios.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net; +using Xunit; + +namespace E2ETests +{ + /// + /// Summary description for TwitterLoginScenarios + /// + public partial class SmokeTests + { + private void LoginWithTwitter() + { + httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = false }; + httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(ApplicationBaseUrl) }; + + var response = httpClient.GetAsync("Account/Login").Result; + ThrowIfResponseStatusNotOk(response); + var responseContent = response.Content.ReadAsStringAsync().Result; + Console.WriteLine("Signing in with Twitter account"); + var formParameters = new List> + { + new KeyValuePair("provider", "Twitter"), + new KeyValuePair("returnUrl", "/"), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/ExternalLogin")), + }; + + var content = new FormUrlEncodedContent(formParameters.ToArray()); + response = httpClient.PostAsync("Account/ExternalLogin", content).Result; + Assert.Equal("https://twitter.com/oauth/authenticate", response.Headers.Location.AbsoluteUri.Replace(response.Headers.Location.Query, string.Empty)); + var queryItems = response.Headers.Location.ParseQueryString(); + Assert.Equal("custom", queryItems["custom_redirect_uri"]); + Assert.Equal("valid_oauth_token", queryItems["oauth_token"]); + //Check for the correlation cookie + Assert.NotNull(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl))["__TwitterState"]); + + //This is just to generate a correlation cookie. Previous step would generate this cookie, but we have reset the handler now. + httpClientHandler = new HttpClientHandler() { AllowAutoRedirect = true }; + httpClient = new HttpClient(httpClientHandler) { BaseAddress = new Uri(ApplicationBaseUrl) }; + + response = httpClient.GetAsync("Account/Login").Result; + responseContent = response.Content.ReadAsStringAsync().Result; + formParameters = new List> + { + new KeyValuePair("provider", "Twitter"), + new KeyValuePair("returnUrl", "/"), + new KeyValuePair("__RequestVerificationToken", HtmlDOMHelper.RetrieveAntiForgeryToken(responseContent, "/Account/ExternalLogin")), + }; + + content = new FormUrlEncodedContent(formParameters.ToArray()); + response = httpClient.PostAsync("Account/ExternalLogin", content).Result; + + //Post a message to the Facebook middleware + response = httpClient.GetAsync("signin-twitter?oauth_token=valid_oauth_token&oauth_verifier=valid_oauth_verifier").Result; + //This should land us in ExternalLoginCallBack - this action is not implemented yet. We need to wait to complete automation. + + //Correlation cookie not getting cleared after successful signin? + //Assert.Null(httpClientHandler.CookieContainer.GetCookies(new Uri(ApplicationBaseUrl))["__TwitterState"]); + } + } +} \ No newline at end of file