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