From 919fa0c195d3990953bc70b04db9300a11bc4c1c Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 7 Aug 2014 17:19:59 -0700 Subject: [PATCH] Port Facebook middleware from Katana. --- Security.sln | 27 +- samples/SocialSample/Project.json | 33 ++ samples/SocialSample/SocialSample.kproj | 32 ++ samples/SocialSample/Startup.cs | 39 +++ .../CookieAuthenticationExtensions.cs | 5 - .../project.json | 2 +- .../FacebookAuthenticationDefaults.cs | 10 + .../FacebookAuthenticationExtensions.cs | 44 +++ .../FacebookAuthenticationHandler.cs | 293 ++++++++++++++++++ .../FacebookAuthenticationMiddleware.cs | 97 ++++++ .../FacebookAuthenticationOptions.cs | 112 +++++++ .../Microsoft.AspNet.Security.Facebook.kproj | 20 ++ .../NotNullAttribute.cs | 12 + .../FacebookApplyRedirectContext.cs | 43 +++ .../FacebookAuthenticatedContext.cs | 98 ++++++ .../FacebookAuthenticationNotifications.cs | 69 +++++ .../FacebookReturnEndpointContext.cs | 26 ++ .../IFacebookAuthenticationNotifications.cs | 33 ++ .../Project.json | 44 +++ .../Resources.Designer.cs | 81 +++++ .../Resources.resx | 126 ++++++++ ...nsions.cs => BuilderSecurityExtensions.cs} | 14 +- src/Microsoft.AspNet.Security/Constants.cs | 2 +- .../DataHandler/PropertiesDataFormat.cs | 1 - .../Facebook/FacebookMiddlewareTests.cs | 124 ++++++++ .../project.json | 3 + 26 files changed, 1373 insertions(+), 17 deletions(-) create mode 100644 samples/SocialSample/Project.json create mode 100644 samples/SocialSample/SocialSample.kproj create mode 100644 samples/SocialSample/Startup.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Microsoft.AspNet.Security.Facebook.kproj create mode 100644 src/Microsoft.AspNet.Security.Facebook/NotNullAttribute.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Project.json create mode 100644 src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs create mode 100644 src/Microsoft.AspNet.Security.Facebook/Resources.resx rename src/Microsoft.AspNet.Security/{AppBuilderSecurityExtensions.cs => BuilderSecurityExtensions.cs} (89%) create mode 100644 test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs diff --git a/Security.sln b/Security.sln index 268ecf4a6c..0d249fe568 100644 --- a/Security.sln +++ b/Security.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.21916.0 +VisualStudioVersion = 14.0.22013.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}" EndProject @@ -22,6 +22,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.Facebook", "src\Microsoft.AspNet.Security.Facebook\Microsoft.AspNet.Security.Facebook.kproj", "{3984651C-FD44-4394-8793-3D14EE348C04}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SocialSample", "samples\SocialSample\SocialSample.kproj", "{8C73D216-332D-41D8-BFD0-45BC4BC36552}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -73,6 +76,26 @@ Global {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|Mixed Platforms.Build.0 = Release|Any CPU {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B}.Release|x86.ActiveCfg = Release|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Debug|x86.ActiveCfg = Debug|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Release|Any CPU.Build.0 = Release|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3984651C-FD44-4394-8793-3D14EE348C04}.Release|x86.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Debug|x86.ActiveCfg = Debug|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Any CPU.Build.0 = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {8C73D216-332D-41D8-BFD0-45BC4BC36552}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -82,5 +105,7 @@ Global {15F1211B-B695-4A1C-B730-1AC58FC91090} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {558C2C2A-AED8-49DE-BB60-D5F8AE06C714} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} {8DA26CD1-1302-4CFD-9270-9FA1B7C6138B} = {7BF11F3A-60B6-4796-B504-579C67FFBA34} + {3984651C-FD44-4394-8793-3D14EE348C04} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {8C73D216-332D-41D8-BFD0-45BC4BC36552} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} EndGlobalSection EndGlobal diff --git a/samples/SocialSample/Project.json b/samples/SocialSample/Project.json new file mode 100644 index 0000000000..f5434cb1c4 --- /dev/null +++ b/samples/SocialSample/Project.json @@ -0,0 +1,33 @@ +{ + "dependencies": { + "Microsoft.AspNet.Hosting": "1.0.0-*", + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Security": "1.0.0-*", + "Microsoft.AspNet.Security.Cookies": "1.0.0-*", + "Microsoft.AspNet.Security.Facebook": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.Framework.DependencyInjection": "1.0.0-*" + }, + "commands": { "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:12345" }, + "frameworks": { + "net45": { + }, + "k10": { + "dependencies": { + "System.Collections": "4.0.10.0", + "System.Console": "4.0.0.0", + "System.Diagnostics.Debug": "4.0.10.0", + "System.Diagnostics.Tools": "4.0.0.0", + "System.Globalization": "4.0.10.0", + "System.IO": "4.0.0.0", + "System.Linq": "4.0.0.0", + "System.Resources.ResourceManager": "4.0.0.0", + "System.Runtime": "4.0.20.0", + "System.Runtime.Extensions": "4.0.10.0", + "System.Runtime.InteropServices": "4.0.20.0", + "System.Threading.Tasks": "4.0.10.0" + } + } + } +} diff --git a/samples/SocialSample/SocialSample.kproj b/samples/SocialSample/SocialSample.kproj new file mode 100644 index 0000000000..c4ab469bfd --- /dev/null +++ b/samples/SocialSample/SocialSample.kproj @@ -0,0 +1,32 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 8c73d216-332d-41d8-bfd0-45bc4bc36552 + Library + + + ConsoleDebugger + + + WebDebugger + + + + + + + 2.0 + + + + + + + + + \ No newline at end of file diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs new file mode 100644 index 0000000000..aec06926ea --- /dev/null +++ b/samples/SocialSample/Startup.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.Security.Facebook; + +namespace CookieSample +{ + public class Startup + { + public void Configure(IBuilder app) + { + app.UseErrorPage(); + + app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType); + + app.UseCookieAuthentication(new CookieAuthenticationOptions() + { + }); + + app.UseFacebookAuthentication(new FacebookAuthenticationOptions() + { + AppId = "569522623154478", + AppSecret = "a124463c4719c94b4228d9a240e5dc1a", + }); + + app.Run(async context => + { + if (!context.User.Identity.IsAuthenticated) + { + context.Response.Challenge("Facebook"); + return; + } + + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync("Hello " + context.User.Identity.Name); + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs index ad683ac21f..eb4f64903a 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationExtensions.cs @@ -1,12 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.AspNet.Http; using Microsoft.AspNet.Security.Cookies; -using Microsoft.AspNet.Security.DataProtection; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Builder { diff --git a/src/Microsoft.AspNet.Security.Cookies/project.json b/src/Microsoft.AspNet.Security.Cookies/project.json index 59c8b666fb..0336d73e9f 100644 --- a/src/Microsoft.AspNet.Security.Cookies/project.json +++ b/src/Microsoft.AspNet.Security.Cookies/project.json @@ -6,7 +6,7 @@ "Microsoft.AspNet.HttpFeature": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.RequestContainer": "1.0.0-*", - "Microsoft.AspNet.Security": "", + "Microsoft.AspNet.Security": "1.0.0-*", "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Microsoft.Framework.Logging": "1.0.0-*", diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs new file mode 100644 index 0000000000..a9d35151c8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationDefaults.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Security.Facebook +{ + public static class FacebookAuthenticationDefaults + { + public const string AuthenticationType = "Facebook"; + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs new file mode 100644 index 0000000000..cdaade1205 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Security.Facebook; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class FacebookAuthenticationExtensions + { + /// + /// Authenticate users using Facebook + /// + /// The passed to the configure method + /// The appId assigned by Facebook + /// The appSecret assigned by Facebook + /// The updated + public static IBuilder UseFacebookAuthentication([NotNull] this IBuilder app, [NotNull] string appId, [NotNull] string appSecret) + { + return app.UseFacebookAuthentication(new FacebookAuthenticationOptions() + { + AppId = appId, + AppSecret = appSecret, + }); + } + + /// + /// Authenticate users using Facebook + /// + /// The passed to the configure method + /// Middleware configuration options + /// The updated + public static IBuilder UseFacebookAuthentication([NotNull] this IBuilder app, [NotNull] FacebookAuthenticationOptions options) + { + if (string.IsNullOrEmpty(options.SignInAsAuthenticationType)) + { + options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + } + return app.UseMiddleware(options); + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs new file mode 100644 index 0000000000..4dde8b75dc --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationHandler.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Logging; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Security.Facebook +{ + internal class FacebookAuthenticationHandler : AuthenticationHandler + { + private const string XmlSchemaString = "http://www.w3.org/2001/XMLSchema#string"; + private const string TokenEndpoint = "https://graph.facebook.com/oauth/access_token"; + private const string GraphApiEndpoint = "https://graph.facebook.com/me"; + private const string AuthorizationEndpoint = "https://www.facebook.com/dialog/oauth"; + + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + public FacebookAuthenticationHandler(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + protected override AuthenticationTicket AuthenticateCore() + { + return AuthenticateCoreAsync().Result; + } + + protected override async Task AuthenticateCoreAsync() + { + AuthenticationProperties properties = null; + + try + { + string code = null; + string state = null; + + IReadableStringCollection query = Request.Query; + + IList values = query.GetValues("error"); + if (values != null && values.Count >= 1) + { + _logger.WriteVerbose("Remote server returned an error: " + Request.QueryString); + } + + values = query.GetValues("code"); + if (values != null && values.Count == 1) + { + code = values[0]; + } + values = query.GetValues("state"); + if (values != null && values.Count == 1) + { + state = values[0]; + } + + properties = Options.StateDataFormat.Unprotect(state); + if (properties == null) + { + return null; + } + + // OAuth2 10.12 CSRF + if (!ValidateCorrelationId(properties, _logger)) + { + return new AuthenticationTicket(null, properties); + } + + if (code == null) + { + // Null if the remote server returns an error. + return new AuthenticationTicket(null, properties); + } + + string requestPrefix = Request.Scheme + "://" + Request.Host; + string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; + + string tokenRequest = "grant_type=authorization_code" + + "&code=" + Uri.EscapeDataString(code) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&client_id=" + Uri.EscapeDataString(Options.AppId) + + "&client_secret=" + Uri.EscapeDataString(Options.AppSecret); + + var tokenResponse = await _httpClient.GetAsync(TokenEndpoint + "?" + tokenRequest, Context.RequestAborted); + tokenResponse.EnsureSuccessStatusCode(); + string text = await tokenResponse.Content.ReadAsStringAsync(); + IFormCollection form = FormHelpers.ParseForm(text); + + string accessToken = form["access_token"]; + string expires = form["expires"]; + string graphAddress = GraphApiEndpoint + "?access_token=" + Uri.EscapeDataString(accessToken); + if (Options.SendAppSecretProof) + { + graphAddress += "&appsecret_proof=" + GenerateAppSecretProof(accessToken); + } + + var graphResponse = await _httpClient.GetAsync(graphAddress, Context.RequestAborted); + graphResponse.EnsureSuccessStatusCode(); + text = await graphResponse.Content.ReadAsStringAsync(); + JObject user = JObject.Parse(text); + + var context = new FacebookAuthenticatedContext(Context, user, accessToken, expires); + context.Identity = new ClaimsIdentity( + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + if (!string.IsNullOrEmpty(context.Id)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.UserName)) + { + context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.UserName, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Email)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, XmlSchemaString, Options.AuthenticationType)); + } + if (!string.IsNullOrEmpty(context.Name)) + { + context.Identity.AddClaim(new Claim("urn:facebook:name", context.Name, XmlSchemaString, Options.AuthenticationType)); + + // Many Facebook accounts do not set the UserName field. Fall back to the Name field instead. + if (string.IsNullOrEmpty(context.UserName)) + { + context.Identity.AddClaim(new Claim(ClaimsIdentity.DefaultNameClaimType, context.Name, XmlSchemaString, Options.AuthenticationType)); + } + } + if (!string.IsNullOrEmpty(context.Link)) + { + context.Identity.AddClaim(new Claim("urn:facebook:link", context.Link, XmlSchemaString, Options.AuthenticationType)); + } + context.Properties = properties; + + await Options.Notifications.Authenticated(context); + + return new AuthenticationTicket(context.Identity, context.Properties); + } + catch (Exception ex) + { + _logger.WriteError("Authentication failed", ex); + return new AuthenticationTicket(null, properties); + } + } + + protected override void ApplyResponseChallenge() + { + if (Response.StatusCode != 401) + { + return; + } + + // Active middleware should redirect on 401 even if there wasn't an explicit challenge. + if (ChallengeContext == null && Options.AuthenticationMode == AuthenticationMode.Passive) + { + return; + } + + string baseUri = + Request.Scheme + + "://" + + Request.Host + + Request.PathBase; + + string currentUri = + baseUri + + Request.Path + + Request.QueryString; + + string redirectUri = + baseUri + + Options.CallbackPath; + + AuthenticationProperties properties; + if (ChallengeContext == null) + { + properties = new AuthenticationProperties(); + } + else + { + properties = new AuthenticationProperties(ChallengeContext.Properties); + } + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = currentUri; + } + + // OAuth2 10.12 CSRF + GenerateCorrelationId(properties); + + // comma separated + string scope = string.Join(",", Options.Scope); + + string state = Options.StateDataFormat.Protect(properties); + + string authorizationEndpoint = + AuthorizationEndpoint + + "?response_type=code" + + "&client_id=" + Uri.EscapeDataString(Options.AppId) + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&scope=" + Uri.EscapeDataString(scope) + + "&state=" + Uri.EscapeDataString(state); + + var redirectContext = new FacebookApplyRedirectContext(Context, Options, properties, authorizationEndpoint); + Options.Notifications.ApplyRedirect(redirectContext); + } + + protected override void ApplyResponseGrant() + { + // N/A + } + + public override async Task InvokeAsync() + { + return await InvokeReplyPathAsync(); + } + + private async Task InvokeReplyPathAsync() + { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + // TODO: error responses + + AuthenticationTicket ticket = await AuthenticateAsync(); + if (ticket == null) + { + _logger.WriteWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new FacebookReturnEndpointContext(Context, ticket); + context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType; + context.RedirectUri = ticket.Properties.RedirectUri; + + await Options.Notifications.ReturnEndpoint(context); + + if (context.SignInAsAuthenticationType != null && + context.Identity != null) + { + ClaimsIdentity grantIdentity = context.Identity; + if (!string.Equals(grantIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) + { + grantIdentity = new ClaimsIdentity(grantIdentity.Claims, context.SignInAsAuthenticationType, grantIdentity.NameClaimType, grantIdentity.RoleClaimType); + } + Context.Response.SignIn(context.Properties, grantIdentity); + } + + if (!context.IsRequestCompleted && context.RedirectUri != null) + { + string redirectUri = context.RedirectUri; + if (context.Identity == null) + { + // add a redirect hint that sign-in failed in some way + redirectUri = QueryHelpers.AddQueryString(redirectUri, "error", "access_denied"); + } + Response.Redirect(redirectUri); + context.RequestCompleted(); + } + + return context.IsRequestCompleted; + } + return false; + } + + private string GenerateAppSecretProof(string accessToken) + { + using (HMACSHA256 algorithm = new HMACSHA256(Encoding.ASCII.GetBytes(Options.AppSecret))) + { + byte[] hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(accessToken)); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) + { + builder.Append(hash[i].ToString("x2", CultureInfo.InvariantCulture)); + } + return builder.ToString(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs new file mode 100644 index 0000000000..895c7b08d8 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Security.DataHandler; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// ASP.NET middleware for authenticating users using Facebook + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware is not disposable.")] + public class FacebookAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + /// + /// Initializes a + /// + /// The next middleware in the application pipeline to invoke + /// Configuration options for the middleware + public FacebookAuthenticationMiddleware( + RequestDelegate next, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + IServiceProvider services, + FacebookAuthenticationOptions options) + : base(next, options) + { + if (string.IsNullOrWhiteSpace(Options.AppId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AppId")); + } + if (string.IsNullOrWhiteSpace(Options.AppSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "AppSecret")); + } + + _logger = loggerFactory.Create(typeof(FacebookAuthenticationMiddleware).FullName); + + if (Options.Notifications == null) + { + Options.Notifications = new FacebookAuthenticationNotifications(); + } + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider, + typeof(FacebookAuthenticationMiddleware).FullName, options.AuthenticationType, "v1"); + Options.StateDataFormat = new PropertiesDataFormat(dataProtector); + } + + _httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); + _httpClient.Timeout = Options.BackchannelTimeout; + _httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new FacebookAuthenticationHandler(_httpClient, _logger); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(FacebookAuthenticationOptions options) + { + HttpMessageHandler handler = options.BackchannelHttpHandler ?? +#if NET45 + new WebRequestHandler(); + // If they provided a validator, apply it or fail. + if (options.BackchannelCertificateValidator != null) + { + // Set the cert validate callback + var webRequestHandler = handler as WebRequestHandler; + if (webRequestHandler == null) + { + throw new InvalidOperationException(Resources.Exception_ValidatorHandlerMismatch); + } + webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; + } +#else + new WinHttpHandler(); +#endif + return handler; + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs new file mode 100644 index 0000000000..9832734460 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// Configuration options for + /// + public class FacebookAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new + /// + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", + MessageId = "Microsoft.AspNet.Security.Facebook.FacebookAuthenticationOptions.set_Caption(System.String)", Justification = "Not localizable.")] + public FacebookAuthenticationOptions() + : base(FacebookAuthenticationDefaults.AuthenticationType) + { + Caption = FacebookAuthenticationDefaults.AuthenticationType; + CallbackPath = new PathString("/signin-facebook"); + AuthenticationMode = AuthenticationMode.Passive; + Scope = new List(); + BackchannelTimeout = TimeSpan.FromSeconds(60); + SendAppSecretProof = true; + } + + /// + /// Gets or sets the Facebook-assigned appId + /// + public string AppId { get; set; } + + /// + /// Gets or sets the Facebook-assigned app secret + /// + public string AppSecret { get; set; } +#if NET45 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to Facebook. + /// + /// + /// The pinned certificate validator. + /// + /// If this property is null then the default certificate checks are performed, + /// validating the subject name and if the signing chain is a trusted party. + public ICertificateValidator BackchannelCertificateValidator { get; set; } +#endif + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Facebook. + /// + /// + /// The back channel timeout in milliseconds. + /// + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// The HttpMessageHandler used to communicate with Facebook. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// The request path within the application's base path where the user-agent will be returned. + /// The middleware will process this request when it arrives. + /// Default value is "/signin-facebook". + /// + public PathString CallbackPath { get; set; } + + /// + /// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user . + /// + public string SignInAsAuthenticationType { get; set; } + + /// + /// Gets or sets the used to handle authentication events. + /// + public IFacebookAuthenticationNotifications Notifications { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// Gets or sets if the appsecret_proof should be generated and sent with Facebook API calls. + /// This is enabled by default. + /// + public bool SendAppSecretProof { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Microsoft.AspNet.Security.Facebook.kproj b/src/Microsoft.AspNet.Security.Facebook/Microsoft.AspNet.Security.Facebook.kproj new file mode 100644 index 0000000000..c576730aa7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Microsoft.AspNet.Security.Facebook.kproj @@ -0,0 +1,20 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 3984651c-fd44-4394-8793-3d14ee348c04 + Library + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Facebook/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.Facebook/NotNullAttribute.cs new file mode 100644 index 0000000000..6a4d82b169 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/NotNullAttribute.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Security.Facebook +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs new file mode 100644 index 0000000000..020e2d32ee --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Notifications; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// Context passed when a Challenge causes a redirect to authorize endpoint in the Facebook middleware + /// + public class FacebookApplyRedirectContext : BaseContext + { + /// + /// Creates a new context object. + /// + /// The http request context + /// The Facebook middleware options + /// The authenticaiton properties of the challenge + /// The initial redirect URI + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#", + Justification = "Represents header value")] + public FacebookApplyRedirectContext(HttpContext context, FacebookAuthenticationOptions options, + AuthenticationProperties properties, string redirectUri) + : base(context, options) + { + RedirectUri = redirectUri; + Properties = properties; + } + + /// + /// Gets the URI used for the redirect operation. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Represents header value")] + public string RedirectUri { get; private set; } + + /// + /// Gets the authentication properties of the challenge + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs new file mode 100644 index 0000000000..c173dfde89 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using System.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Notifications; +using Newtonsoft.Json.Linq; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// Contains information about the login session as well as the user . + /// + public class FacebookAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The http environment + /// The JSON-serialized user + /// Facebook Access token + /// Seconds until expiration + public FacebookAuthenticatedContext(HttpContext context, JObject user, string accessToken, string expires) + : base(context) + { + User = user; + AccessToken = accessToken; + + int expiresValue; + if (Int32.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue)) + { + ExpiresIn = TimeSpan.FromSeconds(expiresValue); + } + + Id = TryGetValue(user, "id"); + Name = TryGetValue(user, "name"); + Link = TryGetValue(user, "link"); + UserName = TryGetValue(user, "username"); + Email = TryGetValue(user, "email"); + } + + /// + /// Gets the JSON-serialized user + /// + public JObject User { get; private set; } + + /// + /// Gets the Facebook access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the Facebook access token expiration time + /// + public TimeSpan? ExpiresIn { get; set; } + + /// + /// Gets the Facebook user ID + /// + public string Id { get; private set; } + + /// + /// Gets the user's name + /// + public string Name { get; private set; } + + public string Link { get; private set; } + + /// + /// Gets the Facebook username + /// + public string UserName { get; private set; } + + /// + /// Gets the Facebook email + /// + public string Email { get; private set; } + + /// + /// Gets the representing the user + /// + public ClaimsIdentity Identity { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties + /// + public AuthenticationProperties Properties { get; set; } + + private static string TryGetValue(JObject user, string propertyName) + { + JToken value; + return user.TryGetValue(propertyName, out value) ? value.ToString() : null; + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs new file mode 100644 index 0000000000..22dbcfcc02 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// Default implementation. + /// + public class FacebookAuthenticationNotifications : IFacebookAuthenticationNotifications + { + /// + /// Initializes a + /// + public FacebookAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(null); + OnReturnEndpoint = context => Task.FromResult(null); + OnApplyRedirect = context => + context.Response.Redirect(context.RedirectUri); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. + /// + public Action OnApplyRedirect { get; set; } + + /// + /// Invoked whenever Facebook succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(FacebookAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + public virtual Task ReturnEndpoint(FacebookReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware + /// + /// Contains redirect URI and of the challenge + public virtual void ApplyRedirect(FacebookApplyRedirectContext context) + { + OnApplyRedirect(context); + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs new file mode 100644 index 0000000000..e32cad4ee2 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Notifications; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// Provides context information to middleware providers. + /// + public class FacebookReturnEndpointContext : ReturnEndpointContext + { + /// + /// Creates a new context object. + /// + /// The http environment + /// The authentication ticket + public FacebookReturnEndpointContext( + HttpContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs new file mode 100644 index 0000000000..530ea65057 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Security.Facebook +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// + public interface IFacebookAuthenticationNotifications + { + /// + /// Invoked whenever Facebook succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(FacebookAuthenticatedContext context); + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + Task ReturnEndpoint(FacebookReturnEndpointContext context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware + /// + /// Contains redirect URI and of the challenge + void ApplyRedirect(FacebookApplyRedirectContext context); + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Project.json b/src/Microsoft.AspNet.Security.Facebook/Project.json new file mode 100644 index 0000000000..b70ab4b6a4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Project.json @@ -0,0 +1,44 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.RequestContainer": "1.0.0-*", + "Microsoft.AspNet.Security": "1.0.0-*", + "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", + "Microsoft.AspNet.WebUtilities": "1.0.0-*", + "Microsoft.Framework.Logging": "1.0.0-*", + "Newtonsoft.Json": "5.0.8", + "System.Net.Http": "4.0.0.0" + }, + "frameworks": { + "net45": { + "dependencies": { + "System.Net.Http.WebRequest": "" + } + }, + "k10": { + "dependencies": { + "System.Collections": "4.0.10.0", + "System.ComponentModel": "4.0.0.0", + "System.Console": "4.0.0.0", + "System.Diagnostics.Debug": "4.0.10.0", + "System.Diagnostics.Tools": "4.0.0.0", + "System.Globalization": "4.0.10.0", + "System.IO": "4.0.0.0", + "System.IO.Compression": "4.0.0.0", + "System.Linq": "4.0.0.0", + "System.Net.Http.WinHttpHandler": "4.0.0.0", + "System.Reflection": "4.0.10.0", + "System.Resources.ResourceManager": "4.0.0.0", + "System.Runtime": "4.0.20.0", + "System.Runtime.Extensions": "4.0.10.0", + "System.Runtime.InteropServices": "4.0.20.0", + "System.Security.Claims": "1.0.0-*", + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0", + "System.Security.Principal": "4.0.0.0", + "System.Threading": "4.0.0.0", + "System.Threading.Tasks": "4.0.10.0" + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs b/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs new file mode 100644 index 0000000000..dca281f828 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.32559 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Security.Facebook { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Security.Facebook.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided { + get { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.Facebook/Resources.resx b/src/Microsoft.AspNet.Security.Facebook/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Microsoft.AspNet.Security.Facebook/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/AppBuilderSecurityExtensions.cs b/src/Microsoft.AspNet.Security/BuilderSecurityExtensions.cs similarity index 89% rename from src/Microsoft.AspNet.Security/AppBuilderSecurityExtensions.cs rename to src/Microsoft.AspNet.Security/BuilderSecurityExtensions.cs index ca9c394275..6043a0966c 100644 --- a/src/Microsoft.AspNet.Security/AppBuilderSecurityExtensions.cs +++ b/src/Microsoft.AspNet.Security/BuilderSecurityExtensions.cs @@ -1,16 +1,15 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -/* TODO: using System; -using Owin; +using Microsoft.AspNet.Security; -namespace Microsoft.AspNet.Security +namespace Microsoft.AspNet.Builder { /// /// Provides extensions methods for app.Property values that are only needed by implementations of authentication middleware. /// - public static class AppBuilderSecurityExtensions + public static class BuilderSecurityExtensions { /// /// Returns the previously set AuthenticationType that external sign in middleware should use when the @@ -18,7 +17,7 @@ namespace Microsoft.AspNet.Security /// /// App builder passed to the application startup code /// - public static string GetDefaultSignInAsAuthenticationType([NotNull] this IAppBuilder app) + public static string GetDefaultSignInAsAuthenticationType([NotNull] this IBuilder app) { object value; if (app.Properties.TryGetValue(Constants.DefaultSignInAsAuthenticationType, out value)) @@ -38,10 +37,9 @@ namespace Microsoft.AspNet.Security /// /// App builder passed to the application startup code /// AuthenticationType that external middleware should sign in as. - public static void SetDefaultSignInAsAuthenticationType([NotNull] this IAppBuilder app, [NotNull] string authenticationType) + public static void SetDefaultSignInAsAuthenticationType([NotNull] this IBuilder app, [NotNull] string authenticationType) { app.Properties[Constants.DefaultSignInAsAuthenticationType] = authenticationType; } } -} -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security/Constants.cs b/src/Microsoft.AspNet.Security/Constants.cs index 1758c84800..d1bc8f01e1 100644 --- a/src/Microsoft.AspNet.Security/Constants.cs +++ b/src/Microsoft.AspNet.Security/Constants.cs @@ -12,6 +12,6 @@ namespace Microsoft.AspNet.Security /// /// Used by middleware extension methods to coordinate the default value Options property SignInAsAuthenticationType /// - public const string DefaultSignInAsAuthenticationType = "Microsoft.AspNet.Security.Constants.DefaultSignInAsAuthenticationType"; + internal const string DefaultSignInAsAuthenticationType = "Microsoft.AspNet.Security.DefaultSignInAsAuthenticationType"; } } diff --git a/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs b/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs index 42f71728c4..050f9d24ee 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs +++ b/src/Microsoft.AspNet.Security/DataHandler/PropertiesDataFormat.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security.DataHandler.Encoder; using Microsoft.AspNet.Security.DataHandler.Serializer; diff --git a/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs new file mode 100644 index 0000000000..8538948783 --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/Facebook/FacebookMiddlewareTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.TestHost; +using Microsoft.Framework.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Security.Facebook +{ + public class FacebookMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var options = new FacebookAuthenticationOptions() + { + AppId = "Test App Id", + AppSecret = "Test App Secret", + Notifications = new FacebookAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + } + }; + var server = CreateServer( + app => app.UseFacebookAuthentication(options), + context => + { + context.Response.Challenge("Facebook"); + return true; + }); + var transaction = await SendAsync(server, "http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var query = transaction.Response.Headers.Location.Query; + query.ShouldContain("custom=test"); + } + + [Fact] + public async Task ChallengeWillTriggerRedirection() + { + var server = CreateServer( + app => app.UseFacebookAuthentication("Test App Id", "Test App Secret"), + context => + { + context.Response.Challenge("Facebook"); + return true; + }); + var transaction = await SendAsync(server, "http://example.com/challenge"); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + var location = transaction.Response.Headers.Location.AbsoluteUri; + location.ShouldContain("https://www.facebook.com/dialog/oauth"); + location.ShouldContain("?response_type=code"); + location.ShouldContain("&client_id="); + location.ShouldContain("&redirect_uri="); + location.ShouldContain("&scope="); + location.ShouldContain("&state="); + } + + private static TestServer CreateServer(Action configure, Func handler) + { + return TestServer.Create(app => + { + app.SetDefaultSignInAsAuthenticationType("External"); + app.UseCookieAuthentication(new CookieAuthenticationOptions() + { + AuthenticationType = "External" + }); + if (configure != null) + { + configure(app); + } + app.Use(async (context, next) => + { + if (handler == null || !handler(context)) + { + await next(); + } + }); + }); + } + + private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + return transaction; + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + public IList SetCookie { get; set; } + public string ResponseText { get; set; } + } + } +} diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json index b46b261dec..d45c84b1ff 100644 --- a/test/Microsoft.AspNet.Security.Test/project.json +++ b/test/Microsoft.AspNet.Security.Test/project.json @@ -3,9 +3,12 @@ "warningsAsErrors": true }, "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.Security" : "", "Microsoft.AspNet.Security.Cookies" : "", + "Microsoft.AspNet.Security.Facebook" : "", "Microsoft.AspNet.TestHost": "1.0.0-*", + "Microsoft.Framework.DependencyInjection": "1.0.0-*", "System.Net.Http": "4.0.0.0", "Moq": "4.2.1312.1622", "Xunit.KRunner": "1.0.0-*"