diff --git a/Security.sln b/Security.sln index 976cfd4ea6..be6636fe28 100644 --- a/Security.sln +++ b/Security.sln @@ -30,6 +30,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.G EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.Twitter", "src\Microsoft.AspNet.Security.Twitter\Microsoft.AspNet.Security.Twitter.kproj", "{C96B77EA-4078-4C31-BDB2-878F11C5E061}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.MicrosoftAccount", "src\Microsoft.AspNet.Security.MicrosoftAccount\Microsoft.AspNet.Security.MicrosoftAccount.kproj", "{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -120,6 +122,16 @@ Global {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Release|Mixed Platforms.Build.0 = Release|Any CPU {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Release|x86.ActiveCfg = Release|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Debug|x86.ActiveCfg = Debug|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|Any CPU.Build.0 = Release|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -133,5 +145,6 @@ Global {8C73D216-332D-41D8-BFD0-45BC4BC36552} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF} {89BF8535-A849-458E-868A-A68FCF620486} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {C96B77EA-4078-4C31-BDB2-878F11C5E061} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} + {1FCF26C2-A3C7-4308-B698-4AFC3560BC0C} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} EndGlobalSection EndGlobal diff --git a/samples/SocialSample/Project.json b/samples/SocialSample/Project.json index 1dc7c69443..8703a24bf7 100644 --- a/samples/SocialSample/Project.json +++ b/samples/SocialSample/Project.json @@ -7,6 +7,7 @@ "Microsoft.AspNet.Security.Cookies": "1.0.0-*", "Microsoft.AspNet.Security.Facebook": "1.0.0-*", "Microsoft.AspNet.Security.Google": "1.0.0-*", + "Microsoft.AspNet.Security.MicrosoftAccount": "1.0.0-*", "Microsoft.AspNet.Security.Twitter": "1.0.0-*", "Microsoft.AspNet.Server.WebListener": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*" diff --git a/samples/SocialSample/Startup.cs b/samples/SocialSample/Startup.cs index bc2e967b3c..03435fe0f6 100644 --- a/samples/SocialSample/Startup.cs +++ b/samples/SocialSample/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNet.Http.Security; using Microsoft.AspNet.Security.Cookies; using Microsoft.AspNet.Security.Facebook; using Microsoft.AspNet.Security.Google; +using Microsoft.AspNet.Security.MicrosoftAccount; using Microsoft.AspNet.Security.Twitter; namespace CookieSample @@ -39,6 +40,30 @@ namespace CookieSample ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI", }); + /* + The MicrosoftAccount service has restrictions that prevent the use of http://localhost:12345/ for test applications. + As such, here is how to change this sample to uses http://mssecsample.localhost.this:12345/ instead. + + Edit the Project.json file and replace http://localhost:12345/ with http://mssecsample.localhost.this:12345/. + + From an admin command console first enter: + notepad C:\Windows\System32\drivers\etc\hosts + and add this to the file, save, and exit (and reboot?): + 127.0.0.1 MsSecSample.localhost.this + + Then you can choose to run the app as admin (see below) or add the following ACL as admin: + netsh http add urlacl url=http://mssecsample.localhost.this:12345/ user=[domain\user] + + The sample app can then be run via: + k web + */ + app.UseMicrosoftAccountAuthentication(new MicrosoftAccountAuthenticationOptions() + { + Caption = "MicrosoftAccount - Requires project changes", + ClientId = "00000000480FF62E", + ClientSecret = "bLw2JIvf8Y1TaToipPEqxTVlOeJwCUsr", + }); + // Choose an authentication type app.Map("/login", signoutApp => { diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs index 423ec040b8..6f2a5dec04 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationMiddleware.cs @@ -12,7 +12,7 @@ using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Security.Cookies { - internal class CookieAuthenticationMiddleware : AuthenticationMiddleware + public class CookieAuthenticationMiddleware : AuthenticationMiddleware { private readonly ILogger _logger; diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs index 60565c812f..8499fa33f6 100644 --- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs @@ -125,7 +125,7 @@ namespace Microsoft.AspNet.Security.Cookies /// /// The TicketDataFormat is used to protect and unprotect the identity and other properties which are stored in the /// cookie value. If it is not provided a default data handler is created using the data protection service contained - /// in the IAppBuilder.Properties. The default data protection service is based on machine key when running on ASP.NET, + /// in the IBuilder.Properties. The default data protection service is based on machine key when running on ASP.NET, /// and on DPAPI when running in a different process. /// public ISecureDataFormat TicketDataFormat { get; set; } diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs index cdaade1205..7f7cb4ebd9 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationExtensions.cs @@ -6,17 +6,17 @@ using Microsoft.AspNet.Security.Facebook; namespace Microsoft.AspNet.Builder { /// - /// Extension methods for using + /// Extension methods for using . /// public static class FacebookAuthenticationExtensions { /// - /// Authenticate users using Facebook + /// Authenticate users using Facebook. /// - /// The passed to the configure method - /// The appId assigned by Facebook - /// The appSecret assigned by Facebook - /// The updated + /// 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() @@ -27,11 +27,11 @@ namespace Microsoft.AspNet.Builder } /// - /// Authenticate users using Facebook + /// Authenticate users using Facebook. /// - /// The passed to the configure method - /// Middleware configuration options - /// The updated + /// The passed to the configure method. + /// The middleware configuration options. + /// The updated . public static IBuilder UseFacebookAuthentication([NotNull] this IBuilder app, [NotNull] FacebookAuthenticationOptions options) { if (string.IsNullOrEmpty(options.SignInAsAuthenticationType)) diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs index 5a3ef41e63..af1da5c300 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationMiddleware.cs @@ -14,7 +14,7 @@ using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Security.Facebook { /// - /// ASP.NET middleware for authenticating users using Facebook + /// An ASP.NET middleware for authenticating users using Facebook. /// [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware is not disposable.")] public class FacebookAuthenticationMiddleware : AuthenticationMiddleware @@ -23,12 +23,12 @@ namespace Microsoft.AspNet.Security.Facebook private readonly HttpClient _httpClient; /// - /// Initializes a + /// Initializes a new . /// - /// The next middleware in the application pipeline to invoke + /// The next middleware in the application pipeline to invoke. /// /// - /// Configuration options for the middleware + /// Configuration options for the middleware. public FacebookAuthenticationMiddleware( RequestDelegate next, IDataProtectionProvider dataProtectionProvider, diff --git a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs index 9832734460..1b9d2c145e 100644 --- a/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.Facebook/FacebookAuthenticationOptions.cs @@ -11,12 +11,12 @@ using Microsoft.AspNet.Http.Security; namespace Microsoft.AspNet.Security.Facebook { /// - /// Configuration options for + /// Configuration options for . /// public class FacebookAuthenticationOptions : AuthenticationOptions { /// - /// Initializes a new + /// 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.")] @@ -32,12 +32,12 @@ namespace Microsoft.AspNet.Security.Facebook } /// - /// Gets or sets the Facebook-assigned appId + /// Gets or sets the Facebook-assigned appId. /// public string AppId { get; set; } /// - /// Gets or sets the Facebook-assigned app secret + /// Gets or sets the Facebook-assigned app secret. /// public string AppSecret { get; set; } #if NET45 diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs index 020e2d32ee..e3e1b35b3e 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookApplyRedirectContext.cs @@ -8,17 +8,17 @@ 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 + /// The 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 + /// The http request context. + /// The Facebook middleware options. + /// The authentication 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, @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Security.Facebook public string RedirectUri { get; private set; } /// - /// Gets the authentication properties of the challenge + /// 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 index c173dfde89..6797ed4959 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticatedContext.cs @@ -17,12 +17,12 @@ namespace Microsoft.AspNet.Security.Facebook public class FacebookAuthenticatedContext : BaseContext { /// - /// Initializes a + /// Initializes a new . /// - /// The http environment - /// The JSON-serialized user - /// Facebook Access token - /// Seconds until expiration + /// The HTTP environment. + /// The JSON-serialized user. + /// The Facebook Access token. + /// Seconds until expiration. public FacebookAuthenticatedContext(HttpContext context, JObject user, string accessToken, string expires) : base(context) { @@ -43,49 +43,52 @@ namespace Microsoft.AspNet.Security.Facebook } /// - /// Gets the JSON-serialized user + /// Gets the JSON-serialized user. /// public JObject User { get; private set; } /// - /// Gets the Facebook access token + /// Gets the Facebook access token. /// public string AccessToken { get; private set; } /// - /// Gets the Facebook access token expiration time + /// Gets the Facebook access token expiration time. /// public TimeSpan? ExpiresIn { get; set; } /// - /// Gets the Facebook user ID + /// Gets the Facebook user ID. /// public string Id { get; private set; } /// - /// Gets the user's name + /// Gets the user's name. /// public string Name { get; private set; } + /// + /// Gets the user's link. + /// public string Link { get; private set; } /// - /// Gets the Facebook username + /// Gets the Facebook username. /// public string UserName { get; private set; } /// - /// Gets the Facebook email + /// Gets the Facebook email. /// public string Email { get; private set; } /// - /// Gets the representing the user + /// Gets the representing the user. /// public ClaimsIdentity Identity { get; set; } /// - /// Gets or sets a property bag for common authentication properties + /// Gets or sets a property bag for common authentication properties. /// public AuthenticationProperties Properties { get; set; } diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs index 22dbcfcc02..b9b33b1257 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookAuthenticationNotifications.cs @@ -7,12 +7,12 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Security.Facebook { /// - /// Default implementation. + /// The default implementation. /// public class FacebookAuthenticationNotifications : IFacebookAuthenticationNotifications { /// - /// Initializes a + /// Initializes a new . /// public FacebookAuthenticationNotifications() { @@ -38,7 +38,7 @@ namespace Microsoft.AspNet.Security.Facebook public Action OnApplyRedirect { get; set; } /// - /// Invoked whenever Facebook succesfully authenticates a user + /// Invoked whenever Facebook succesfully authenticates a user. /// /// Contains information about the login session as well as the user . /// A representing the completed operation. @@ -58,9 +58,9 @@ namespace Microsoft.AspNet.Security.Facebook } /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware + /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware. /// - /// Contains redirect URI and of the challenge + /// 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 index e32cad4ee2..29786a9068 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/FacebookReturnEndpointContext.cs @@ -7,15 +7,15 @@ using Microsoft.AspNet.Security.Notifications; namespace Microsoft.AspNet.Security.Facebook { /// - /// Provides context information to middleware providers. + /// Provides context information for notifications. /// public class FacebookReturnEndpointContext : ReturnEndpointContext { /// /// Creates a new context object. /// - /// The http environment - /// The authentication ticket + /// The http environment. + /// The authentication ticket. public FacebookReturnEndpointContext( HttpContext context, AuthenticationTicket ticket) diff --git a/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs index 530ea65057..236cba3ac5 100644 --- a/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Facebook/Notifications/IFacebookAuthenticationNotifications.cs @@ -6,12 +6,12 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Security.Facebook { /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /// public interface IFacebookAuthenticationNotifications { /// - /// Invoked whenever Facebook succesfully authenticates a user + /// Invoked when Facebook succesfully authenticates a user. /// /// Contains information about the login session as well as the user . /// A representing the completed operation. @@ -25,9 +25,9 @@ namespace Microsoft.AspNet.Security.Facebook Task ReturnEndpoint(FacebookReturnEndpointContext context); /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware + /// Called when a Challenge causes a redirect to authorize endpoint in the Facebook middleware. /// - /// Contains redirect URI and of the challenge + /// Contains redirect URI and of the challenge. void ApplyRedirect(FacebookApplyRedirectContext context); } } diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs index 1203735352..cfac477731 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationExtensions.cs @@ -6,17 +6,17 @@ using Microsoft.AspNet.Security.Google; namespace Microsoft.AspNet.Builder { /// - /// Extension methods for using + /// Extension methods for using . /// public static class GoogleAuthenticationExtensions { /// - /// Authenticate users using Google OAuth 2.0 + /// Authenticate users using Google OAuth 2.0. /// - /// The passed to the configure method - /// The google assigned client id - /// The google assigned client secret - /// The updated + /// The passed to the configure method. + /// The google assigned client id. + /// The google assigned client secret. + /// The updated . public static IBuilder UseGoogleAuthentication([NotNull] this IBuilder app, [NotNull] string clientId, [NotNull] string clientSecret) { return app.UseGoogleAuthentication( @@ -28,11 +28,11 @@ namespace Microsoft.AspNet.Builder } /// - /// Authenticate users using Google OAuth 2.0 + /// Authenticate users using Google OAuth 2.0. /// - /// The passed to the configure method - /// Middleware configuration options - /// The updated + /// The passed to the configure method. + /// Middleware configuration options. + /// The updated . public static IBuilder UseGoogleAuthentication([NotNull] this IBuilder app, [NotNull] GoogleAuthenticationOptions options) { if (string.IsNullOrEmpty(options.SignInAsAuthenticationType)) diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs index 15c5c2a79c..1105b149ba 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationMiddleware.cs @@ -14,7 +14,7 @@ using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Security.Google { /// - /// ASP.NET middleware for authenticating users using Google OAuth 2.0 + /// An ASP.NET middleware for authenticating users using Google OAuth 2.0. /// [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] public class GoogleAuthenticationMiddleware : AuthenticationMiddleware @@ -23,12 +23,12 @@ namespace Microsoft.AspNet.Security.Google private readonly HttpClient _httpClient; /// - /// Initializes a + /// Initializes a new . /// - /// The next middleware in the HTTP pipeline to invoke + /// The next middleware in the HTTP pipeline to invoke. /// /// - /// Configuration options for the middleware + /// Configuration options for the middleware. public GoogleAuthenticationMiddleware( RequestDelegate next, IDataProtectionProvider dataProtectionProvider, diff --git a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs index fd7b069990..f03b7d2efc 100644 --- a/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs +++ b/src/Microsoft.AspNet.Security.Google/GoogleAuthenticationOptions.cs @@ -10,12 +10,12 @@ using Microsoft.AspNet.Http.Security; namespace Microsoft.AspNet.Security.Google { /// - /// Configuration options for + /// Configuration options for . /// public class GoogleAuthenticationOptions : AuthenticationOptions { /// - /// Initializes a new + /// Initializes a new . /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "Microsoft.AspNet.Security.Google.GoogleAuthenticationOptions.set_Caption(System.String)", @@ -31,12 +31,12 @@ namespace Microsoft.AspNet.Security.Google } /// - /// Gets or sets the Google-assigned client id + /// Gets or sets the Google-assigned client id. /// public string ClientId { get; set; } /// - /// Gets or sets the Google-assigned client secret + /// Gets or sets the Google-assigned client secret. /// public string ClientSecret { get; set; } #if NET45 diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs index ac020b5639..7df6358ddd 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleApplyRedirectContext.cs @@ -8,17 +8,17 @@ using Microsoft.AspNet.Security.Notifications; namespace Microsoft.AspNet.Security.Google { /// - /// Context passed when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware + /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware. /// public class GoogleApplyRedirectContext : BaseContext { /// /// Creates a new context object. /// - /// The HTTP request context - /// The Google OAuth 2.0 middleware options - /// The authenticaiton properties of the challenge - /// The initial redirect URI + /// The HTTP request context. + /// The Google OAuth 2.0 middleware options. + /// The authentication properties of the challenge. + /// The initial redirect URI. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "3#", Justification = "Represents header value")] public GoogleApplyRedirectContext(HttpContext context, GoogleAuthenticationOptions options, @@ -36,7 +36,7 @@ namespace Microsoft.AspNet.Security.Google public string RedirectUri { get; private set; } /// - /// Gets the authenticaiton properties of the challenge + /// Gets the authentication properties of the challenge. /// public AuthenticationProperties Properties { get; private set; } } diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs index ca89962769..84f535fb8d 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticatedContext.cs @@ -17,13 +17,13 @@ namespace Microsoft.AspNet.Security.Google public class GoogleAuthenticatedContext : BaseContext { /// - /// Initializes a + /// Initializes a new . /// - /// The HTTP environment - /// The JSON-serialized Google user info - /// Google OAuth 2.0 access token - /// Goolge OAuth 2.0 refresh token - /// Seconds until expiration + /// The HTTP environment. + /// The JSON-serialized Google user info. + /// Google OAuth 2.0 access token. + /// Goolge OAuth 2.0 refresh token. + /// Seconds until expiration. public GoogleAuthenticatedContext(HttpContext context, JObject user, string accessToken, string refreshToken, string expires) : base(context) @@ -47,20 +47,20 @@ namespace Microsoft.AspNet.Security.Google } /// - /// Gets the JSON-serialized user + /// Gets the JSON-serialized user. /// /// - /// Contains the Google user obtained from the endpoint https://www.googleapis.com/oauth2/v3/userinfo + /// Contains the Google user obtained from the userinfo endpoint. /// public JObject User { get; private set; } /// - /// Gets the Google access token + /// Gets the Google access token. /// public string AccessToken { get; private set; } /// - /// Gets the Google refresh token + /// Gets the Google refresh token. /// /// /// This value is not null only when access_type authorize parameter is offline. @@ -68,47 +68,47 @@ namespace Microsoft.AspNet.Security.Google public string RefreshToken { get; private set; } /// - /// Gets the Google access token expiration time + /// Gets the Google access token expiration time. /// public TimeSpan? ExpiresIn { get; set; } /// - /// Gets the Google user ID + /// Gets the Google user ID. /// public string Id { get; private set; } /// - /// Gets the user's name + /// Gets the user's name. /// public string Name { get; private set; } /// - /// Gets the user's given name + /// Gets the user's given name. /// public string GivenName { get; set; } /// - /// Gets the user's family name + /// Gets the user's family name. /// public string FamilyName { get; set; } /// - /// Gets the user's profile link + /// Gets the user's profile link. /// public string Profile { get; private set; } /// - /// Gets the user's email + /// Gets the user's email. /// public string Email { get; private set; } /// - /// Gets the representing the user + /// Gets the representing the user. /// public ClaimsIdentity Identity { get; set; } /// - /// Gets or sets a property bag for common authentication properties + /// Gets or sets a property bag for common authentication properties. /// public AuthenticationProperties Properties { get; set; } diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs index af93619ed4..36f9368953 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleAuthenticationNotifications.cs @@ -7,19 +7,18 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Security.Google { /// - /// Default implementation. + /// The default implementation. /// public class GoogleAuthenticationNotifications : IGoogleAuthenticationNotifications { /// - /// Initializes a + /// Initializes a new . /// public GoogleAuthenticationNotifications() { OnAuthenticated = context => Task.FromResult(null); OnReturnEndpoint = context => Task.FromResult(null); - OnApplyRedirect = context => - context.Response.Redirect(context.RedirectUri); + OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri); } /// @@ -38,7 +37,7 @@ namespace Microsoft.AspNet.Security.Google public Action OnApplyRedirect { get; set; } /// - /// Invoked whenever Google succesfully authenticates a user + /// Invoked whenever Google succesfully authenticates a user. /// /// Contains information about the login session as well as the user . /// A representing the completed operation. @@ -58,9 +57,9 @@ namespace Microsoft.AspNet.Security.Google } /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware + /// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware. /// - /// Contains redirect URI and of the challenge + /// Contains redirect URI and of the challenge. public virtual void ApplyRedirect(GoogleApplyRedirectContext context) { OnApplyRedirect(context); diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs index 7172455ad2..ce6a13742e 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/GoogleReturnEndpointContext.cs @@ -12,10 +12,10 @@ namespace Microsoft.AspNet.Security.Google public class GoogleReturnEndpointContext : ReturnEndpointContext { /// - /// Initialize a + /// Initialize a . /// - /// HTTP environment - /// The authentication ticket + /// The HTTP environment. + /// The authentication ticket. public GoogleReturnEndpointContext( HttpContext context, AuthenticationTicket ticket) diff --git a/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs index 7bf9a34ab3..13930346c8 100644 --- a/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs +++ b/src/Microsoft.AspNet.Security.Google/Notifications/IGoogleAuthenticationNotifications.cs @@ -6,12 +6,12 @@ using System.Threading.Tasks; namespace Microsoft.AspNet.Security.Google { /// - /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /// public interface IGoogleAuthenticationNotifications { /// - /// Invoked whenever Google succesfully authenticates a user + /// Invoked whenever Google succesfully authenticates a user. /// /// Contains information about the login session as well as the user . /// A representing the completed operation. @@ -25,9 +25,9 @@ namespace Microsoft.AspNet.Security.Google Task ReturnEndpoint(GoogleReturnEndpointContext context); /// - /// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware + /// Called when a Challenge causes a redirect to authorize endpoint in the Google OAuth 2.0 middleware. /// - /// Contains redirect URI and of the challenge + /// Contains redirect URI and of the challenge. void ApplyRedirect(GoogleApplyRedirectContext context); } } diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Microsoft.AspNet.Security.MicrosoftAccount.kproj b/src/Microsoft.AspNet.Security.MicrosoftAccount/Microsoft.AspNet.Security.MicrosoftAccount.kproj new file mode 100644 index 0000000000..dbdeeb34c6 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Microsoft.AspNet.Security.MicrosoftAccount.kproj @@ -0,0 +1,28 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1fcf26c2-a3c7-4308-b698-4afc3560bc0c + Library + + + + ConsoleDebugger + + + WebDebugger + + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.cs new file mode 100644 index 0000000000..8f83dac3e4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationDefaults.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.MicrosoftAccount +{ + public static class MicrosoftAccountAuthenticationDefaults + { + public const string AuthenticationType = "Microsoft"; + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationExtensions.cs new file mode 100644 index 0000000000..468d279078 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationExtensions.cs @@ -0,0 +1,45 @@ +// 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.MicrosoftAccount; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class MicrosoftAccountAuthenticationExtensions + { + /// + /// Authenticate users using Microsoft Account. + /// + /// The passed to the configure method. + /// The application client ID assigned by the Microsoft authentication service. + /// The application client secret assigned by the Microsoft authentication service. + /// The updated . + public static IBuilder UseMicrosoftAccountAuthentication([NotNull] this IBuilder app, [NotNull] string clientId, [NotNull] string clientSecret) + { + return app.UseMicrosoftAccountAuthentication( + new MicrosoftAccountAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret, + }); + } + + /// + /// Authenticate users using Microsoft Account. + /// + /// The passed to the configure method. + /// The middleware configuration options. + /// The updated . + public static IBuilder UseMicrosoftAccountAuthentication([NotNull] this IBuilder app, [NotNull] MicrosoftAccountAuthenticationOptions options) + { + if (string.IsNullOrEmpty(options.SignInAsAuthenticationType)) + { + options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + } + return app.UseMiddleware(options); + } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs new file mode 100644 index 0000000000..6ee11c1d8f --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs @@ -0,0 +1,257 @@ +// 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.Net.Http; +using System.Security.Claims; +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.MicrosoftAccount +{ + internal class MicrosoftAccountAuthenticationHandler : AuthenticationHandler + { + private const string TokenEndpoint = "https://login.live.com/oauth20_token.srf"; + private const string GraphApiEndpoint = "https://apis.live.net/v5.0/me"; + + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + public MicrosoftAccountAuthenticationHandler(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + } + + public override async Task InvokeAsync() + { + if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) + { + return await InvokeReturnPathAsync(); + } + return false; + } + + 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("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); + } + + var tokenRequestParameters = new List>() + { + new KeyValuePair("client_id", Options.ClientId), + new KeyValuePair("redirect_uri", GenerateRedirectUri()), + new KeyValuePair("client_secret", Options.ClientSecret), + new KeyValuePair("code", code), + new KeyValuePair("grant_type", "authorization_code"), + }; + + var requestContent = new FormUrlEncodedContent(tokenRequestParameters); + + HttpResponseMessage response = await _httpClient.PostAsync(TokenEndpoint, requestContent, Context.RequestAborted); + response.EnsureSuccessStatusCode(); + string oauthTokenResponse = await response.Content.ReadAsStringAsync(); + + JObject oauth2Token = JObject.Parse(oauthTokenResponse); + var accessToken = oauth2Token["access_token"].Value(); + + // Refresh token is only available when wl.offline_access is request. + // Otherwise, it is null. + var refreshToken = oauth2Token.Value("refresh_token"); + var expire = oauth2Token.Value("expires_in"); + + if (string.IsNullOrWhiteSpace(accessToken)) + { + _logger.WriteWarning("Access token was not found"); + return new AuthenticationTicket(null, properties); + } + + HttpResponseMessage graphResponse = await _httpClient.GetAsync( + GraphApiEndpoint + "?access_token=" + Uri.EscapeDataString(accessToken), Context.RequestAborted); + graphResponse.EnsureSuccessStatusCode(); + string accountString = await graphResponse.Content.ReadAsStringAsync(); + JObject accountInformation = JObject.Parse(accountString); + + var context = new MicrosoftAccountAuthenticatedContext(Context, accountInformation, accessToken, + refreshToken, expire); + context.Identity = new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.NameIdentifier, context.Id, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType), + new Claim(ClaimTypes.Name, context.Name, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType), + new Claim("urn:microsoftaccount:id", context.Id, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType), + new Claim("urn:microsoftaccount:name", context.Name, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType) + }, + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + if (!string.IsNullOrWhiteSpace(context.Email)) + { + context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType)); + } + + await Options.Notifications.Authenticated(context); + + context.Properties = properties; + + 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); + + // OAuth2 3.3 space separated + string scope = string.Join(" ", Options.Scope); + // LiveID requires a scope string, so if the user didn't set one we go for the least possible. + if (string.IsNullOrWhiteSpace(scope)) + { + scope = "wl.basic"; + } + + string state = Options.StateDataFormat.Protect(properties); + + string authorizationEndpoint = + "https://login.live.com/oauth20_authorize.srf" + + "?client_id=" + Uri.EscapeDataString(Options.ClientId) + + "&scope=" + Uri.EscapeDataString(scope) + + "&response_type=code" + + "&redirect_uri=" + Uri.EscapeDataString(redirectUri) + + "&state=" + Uri.EscapeDataString(state); + + var redirectContext = new MicrosoftAccountApplyRedirectContext( + Context, Options, + properties, authorizationEndpoint); + Options.Notifications.ApplyRedirect(redirectContext); + } + + public async Task InvokeReturnPathAsync() + { + AuthenticationTicket model = await AuthenticateAsync(); + if (model == null) + { + _logger.WriteWarning("Invalid return state, unable to redirect."); + Response.StatusCode = 500; + return true; + } + + var context = new MicrosoftAccountReturnEndpointContext(Context, model); + context.SignInAsAuthenticationType = Options.SignInAsAuthenticationType; + context.RedirectUri = model.Properties.RedirectUri; + model.Properties.RedirectUri = null; + + await Options.Notifications.ReturnEndpoint(context); + + if (context.SignInAsAuthenticationType != null && context.Identity != null) + { + ClaimsIdentity signInIdentity = context.Identity; + if (!string.Equals(signInIdentity.AuthenticationType, context.SignInAsAuthenticationType, StringComparison.Ordinal)) + { + signInIdentity = new ClaimsIdentity(signInIdentity.Claims, context.SignInAsAuthenticationType, signInIdentity.NameClaimType, signInIdentity.RoleClaimType); + } + Context.Response.SignIn(context.Properties, signInIdentity); + } + + if (!context.IsRequestCompleted && context.RedirectUri != null) + { + if (context.Identity == null) + { + // add a redirect hint that sign-in failed in some way + context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied"); + } + Response.Redirect(context.RedirectUri); + context.RequestCompleted(); + } + + return context.IsRequestCompleted; + } + + private string GenerateRedirectUri() + { + string requestPrefix = Request.Scheme + "://" + Request.Host; + + return requestPrefix + RequestPathBase + Options.CallbackPath; + } + + protected override void ApplyResponseGrant() + { + // N/A - No SignIn or SignOut support. + } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.cs new file mode 100644 index 0000000000..0bd8f4c3e0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationMiddleware.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.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.MicrosoftAccount +{ + /// + /// An ASP.NET middleware for authenticating users using the Microsoft Account service. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] + public class MicrosoftAccountAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + /// + /// Initializes a new . + /// + /// The next middleware in the HTTP pipeline to invoke. + /// + /// + /// Configuration options for the middleware. + public MicrosoftAccountAuthenticationMiddleware( + RequestDelegate next, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + MicrosoftAccountAuthenticationOptions options) + : base(next, options) + { + if (string.IsNullOrWhiteSpace(Options.ClientId)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientId")); + } + if (string.IsNullOrWhiteSpace(Options.ClientSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ClientSecret")); + } + + _logger = loggerFactory.Create(typeof(MicrosoftAccountAuthenticationMiddleware).FullName); + + if (Options.Notifications == null) + { + Options.Notifications = new MicrosoftAccountAuthenticationNotifications(); + } + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider, + typeof(MicrosoftAccountAuthenticationMiddleware).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 MicrosoftAccountAuthenticationHandler(_httpClient, _logger); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(MicrosoftAccountAuthenticationOptions 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.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs new file mode 100644 index 0000000000..47ee6fe0e7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs @@ -0,0 +1,106 @@ +// 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.MicrosoftAccount +{ + /// + /// Configuration options for . + /// + public class MicrosoftAccountAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new . + /// + public MicrosoftAccountAuthenticationOptions() + : base(MicrosoftAccountAuthenticationDefaults.AuthenticationType) + { + Caption = MicrosoftAccountAuthenticationDefaults.AuthenticationType; + CallbackPath = new PathString("/signin-microsoft"); + AuthenticationMode = AuthenticationMode.Passive; + Scope = new List(); + BackchannelTimeout = TimeSpan.FromSeconds(60); + } +#if NET45 + /// + /// Gets or sets the a pinned certificate validator to use to validate the endpoints used + /// in back channel communications belong to Microsoft Account. + /// + /// + /// 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 + /// + /// Get or sets the text that the user can display on a sign in user interface. + /// + /// + /// The default value is 'Microsoft'. + /// + public string Caption + { + get { return Description.Caption; } + set { Description.Caption = value; } + } + + /// + /// The application client ID assigned by the Microsoft authentication service. + /// + public string ClientId { get; set; } + + /// + /// The application client secret assigned by the Microsoft authentication service. + /// + public string ClientSecret { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Microsoft. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { get; set; } + + /// + /// The HttpMessageHandler used to communicate with Microsoft. + /// This cannot be set at the same time as BackchannelCertificateValidator unless the value + /// can be downcast to a WebRequestHandler. + /// + public HttpMessageHandler BackchannelHttpHandler { get; set; } + + /// + /// A list of permissions to request. + /// + public IList Scope { get; private set; } + + /// + /// 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-microsoft". + /// + 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 IMicrosoftAccountAuthenticationNotifications Notifications { get; set; } + + /// + /// Gets or sets the type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/NotNullAttribute.cs new file mode 100644 index 0000000000..f3900accc6 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/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.MicrosoftAccount +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.cs new file mode 100644 index 0000000000..d7c43a4634 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/IMicrosoftAccountAuthenticationNotifications.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.MicrosoftAccount +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. + /// + public interface IMicrosoftAccountAuthenticationNotifications + { + /// + /// Invoked whenever Microsoft succesfully authenticates a user. + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(MicrosoftAccountAuthenticatedContext 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(MicrosoftAccountReturnEndpointContext context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft middleware. + /// + /// Contains redirect URI and of the challenge. + void ApplyRedirect(MicrosoftAccountApplyRedirectContext context); + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs new file mode 100644 index 0000000000..f61a9eee43 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountApplyRedirectContext.cs @@ -0,0 +1,40 @@ +// 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.MicrosoftAccount +{ + /// + /// Context passed when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware. + /// + public class MicrosoftAccountApplyRedirectContext : BaseContext + { + /// + /// Creates a new context object. + /// + /// The HTTP request context. + /// The Microsoft account middleware options. + /// The authentication properties of the challenge. + /// The initial redirect URI. + public MicrosoftAccountApplyRedirectContext(HttpContext context, MicrosoftAccountAuthenticationOptions options, + AuthenticationProperties properties, string redirectUri) + : base(context, options) + { + RedirectUri = redirectUri; + Properties = properties; + } + + /// + /// Gets the URI used for the redirect operation. + /// + 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.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs new file mode 100644 index 0000000000..ef2b0b50f0 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticatedContext.cs @@ -0,0 +1,130 @@ +// 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.Linq; +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.MicrosoftAccount +{ + /// + /// Contains information about the login session as well as the user . + /// + public class MicrosoftAccountAuthenticatedContext : BaseContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The JSON-serialized user. + /// The access token provided by the Microsoft authentication service. + /// The refresh token provided by Microsoft authentication service. + /// Seconds until expiration. + public MicrosoftAccountAuthenticatedContext(HttpContext context, [NotNull] JObject user, string accessToken, + string refreshToken, string expires) + : base(context) + { + IDictionary userAsDictionary = user; + + User = user; + AccessToken = accessToken; + RefreshToken = refreshToken; + + int expiresValue; + if (Int32.TryParse(expires, NumberStyles.Integer, CultureInfo.InvariantCulture, out expiresValue)) + { + ExpiresIn = TimeSpan.FromSeconds(expiresValue); + } + + JToken userId = User["id"]; + if (userId == null) + { + throw new ArgumentException(Resources.Exception_MissingId, "user"); + } + + Id = userId.ToString(); + Name = PropertyValueIfExists("name", userAsDictionary); + FirstName = PropertyValueIfExists("first_name", userAsDictionary); + LastName = PropertyValueIfExists("last_name", userAsDictionary); + + if (userAsDictionary.ContainsKey("emails")) + { + JToken emailsNode = user["emails"]; + foreach (var childAsProperty in emailsNode.OfType().Where(childAsProperty => childAsProperty.Name == "preferred")) + { + Email = childAsProperty.Value.ToString(); + } + } + } + + /// + /// Gets the JSON-serialized user. + /// + public JObject User { get; private set; } + + /// + /// Gets the access token provided by the Microsoft authenication service. + /// + public string AccessToken { get; private set; } + + /// + /// Gets the refresh token provided by Microsoft authentication service. + /// + /// + /// Refresh token is only available when wl.offline_access is request. + /// Otherwise, it is null. + /// + public string RefreshToken { get; private set; } + + /// + /// Gets the Microsoft access token expiration time. + /// + public TimeSpan? ExpiresIn { get; set; } + + /// + /// Gets the Microsoft Account user ID. + /// + public string Id { get; private set; } + + /// + /// Gets the user's name. + /// + public string Name { get; private set; } + + /// + /// Gets the user's first name. + /// + public string FirstName { get; private set; } + + /// + /// Gets the user's last name. + /// + public string LastName { get; private set; } + + /// + /// Gets the user's email address. + /// + 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 PropertyValueIfExists(string property, IDictionary dictionary) + { + return dictionary.ContainsKey(property) ? dictionary[property].ToString() : null; + } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs new file mode 100644 index 0000000000..deb561b941 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountAuthenticationNotifications.cs @@ -0,0 +1,68 @@ +// 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.MicrosoftAccount +{ + /// + /// Default implementation. + /// + public class MicrosoftAccountAuthenticationNotifications : IMicrosoftAccountAuthenticationNotifications + { + /// + /// Initializes a new + /// + public MicrosoftAccountAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(0); + OnReturnEndpoint = context => Task.FromResult(0); + 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 Microsoft succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(MicrosoftAccountAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// Contains information about the login session as well as the user + /// A representing the completed operation. + public virtual Task ReturnEndpoint(MicrosoftAccountReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Microsoft account middleware. + /// + /// Contains redirect URI and of the challenge. + public virtual void ApplyRedirect(MicrosoftAccountApplyRedirectContext context) + { + OnApplyRedirect(context); + } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.cs new file mode 100644 index 0000000000..93e7670248 --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Notifications/MicrosoftAccountReturnEndpointContext.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.MicrosoftAccount +{ + /// + /// Provides context information to middleware providers. + /// + public class MicrosoftAccountReturnEndpointContext : ReturnEndpointContext + { + /// + /// Initializes a new . + /// + /// The HTTP environment. + /// The authentication ticket. + public MicrosoftAccountReturnEndpointContext( + HttpContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/src/Microsoft.AspNet.Security.MicrosoftAccount/Resources.Designer.cs b/src/Microsoft.AspNet.Security.MicrosoftAccount/Resources.Designer.cs new file mode 100644 index 0000000000..62f1707efb --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Resources.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.33440 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Security.MicrosoftAccount { + 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.MicrosoftAccount.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 user does not have an id.. + /// + internal static string Exception_MissingId { + get { + return ResourceManager.GetString("Exception_MissingId", resourceCulture); + } + } + + /// + /// 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.MicrosoftAccount/Resources.resx b/src/Microsoft.AspNet.Security.MicrosoftAccount/Resources.resx new file mode 100644 index 0000000000..26eb43888e --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/Resources.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 user does not have an id. + + + 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.MicrosoftAccount/project.json b/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json new file mode 100644 index 0000000000..b583f1ae8c --- /dev/null +++ b/src/Microsoft.AspNet.Security.MicrosoftAccount/project.json @@ -0,0 +1,46 @@ +{ + "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": "" + } + }, + "aspnetcore50": { + "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.Dynamic.Runtime": "4.0.0.0", + "System.Globalization": "4.0.10.0", + "System.IO": "4.0.10.0", + "System.IO.Compression": "4.0.0.0", + "System.Linq": "4.0.0.0", + "System.Net.Http.WinHttpHandler": "4.0.0.0", + "System.ObjectModel": "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.Twitter/Notifications/TwitterApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterApplyRedirectContext.cs index 3d5ab80ffc..a328730ded 100644 --- a/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterApplyRedirectContext.cs +++ b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterApplyRedirectContext.cs @@ -8,17 +8,17 @@ using Microsoft.AspNet.Security.Notifications; namespace Microsoft.AspNet.Security.Twitter { /// - /// Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware + /// The Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware. /// public class TwitterApplyRedirectContext : BaseContext { /// /// Creates a new context object. /// - /// The HTTP request context - /// The Facebook middleware options - /// The authenticaiton properties of the challenge - /// The initial redirect URI + /// The HTTP request context. + /// The Twitter middleware options. + /// The authentication properties of the challenge. + /// The initial redirect URI. public TwitterApplyRedirectContext(HttpContext context, TwitterAuthenticationOptions options, AuthenticationProperties properties, string redirectUri) : base(context, options) @@ -33,7 +33,7 @@ namespace Microsoft.AspNet.Security.Twitter public string RedirectUri { get; private set; } /// - /// Gets the authenticaiton properties of the challenge + /// Gets the authentication properties of the challenge. /// public AuthenticationProperties Properties { get; private set; } } diff --git a/src/Microsoft.AspNet.Security/Resources.Designer.cs b/src/Microsoft.AspNet.Security/Resources.Designer.cs index 26e74bb6c0..d535265888 100644 --- a/src/Microsoft.AspNet.Security/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Security/Resources.Designer.cs @@ -70,7 +70,7 @@ namespace Microsoft.AspNet.Security { } /// - /// Looks up a localized string similar to The default data protection provider may only be used when the IAppBuilder.Properties contains an appropriate 'host.AppName' key.. + /// Looks up a localized string similar to The default data protection provider may only be used when the IBuilder.Properties contains an appropriate 'host.AppName' key.. /// internal static string Exception_DefaultDpapiRequiresAppNameKey { get { @@ -79,7 +79,7 @@ namespace Microsoft.AspNet.Security { } /// - /// Looks up a localized string similar to A default value for SignInAsAuthenticationType was not found in IAppBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing.. + /// Looks up a localized string similar to A default value for SignInAsAuthenticationType was not found in IBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing.. /// internal static string Exception_MissingDefaultSignInAsAuthenticationType { get { diff --git a/src/Microsoft.AspNet.Security/Resources.resx b/src/Microsoft.AspNet.Security/Resources.resx index fdd0980662..5a02ab7725 100644 --- a/src/Microsoft.AspNet.Security/Resources.resx +++ b/src/Microsoft.AspNet.Security/Resources.resx @@ -118,13 +118,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The default data protection provider may only be used when the IAppBuilder.Properties contains an appropriate 'host.AppName' key. + The default data protection provider may only be used when the IBuilder.Properties contains an appropriate 'host.AppName' key. The state passed to UnhookAuthentication may only be the return value from HookAuthentication. - A default value for SignInAsAuthenticationType was not found in IAppBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing. + A default value for SignInAsAuthenticationType was not found in IBuilder Properties. This can happen if your authentication middleware are added in the wrong order, or if one is missing. The AuthenticationTokenProvider's required synchronous events have not been registered. diff --git a/test/Microsoft.AspNet.Security.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs new file mode 100644 index 0000000000..2c1da025fd --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.Security.MicrosoftAccount; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Security.Tests.MicrosoftAccount +{ + public class MicrosoftAccountMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var options = new MicrosoftAccountAuthenticationOptions() + { + ClientId = "Test Client Id", + ClientSecret = "Test Client Secret", + Notifications = new MicrosoftAccountAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + } + }; + var server = CreateServer( + app => app.UseMicrosoftAccountAuthentication(options), + context => + { + context.Response.Challenge("Microsoft"); + 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.UseMicrosoftAccountAuthentication("Test Client Id", "Test Client Secret"), + context => + { + context.Response.Challenge("Microsoft"); + 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://login.live.com/oauth20_authorize.srf"); + location.ShouldContain("response_type=code"); + location.ShouldContain("client_id="); + location.ShouldContain("redirect_uri="); + location.ShouldContain("scope="); + location.ShouldContain("state="); + } + + [Fact] + public async Task AuthenticatedEventCanGetRefreshToken() + { + var options = new MicrosoftAccountAuthenticationOptions() + { + ClientId = "Test Client Id", + ClientSecret = "Test Client Secret", + BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = async req => + { + if (req.RequestUri.AbsoluteUri == "https://login.live.com/oauth20_token.srf") + { + return await ReturnJsonResponse(new + { + access_token = "Test Access Token", + expire_in = 3600, + token_type = "Bearer", + refresh_token = "Test Refresh Token" + }); + } + else if (req.RequestUri.GetLeftPart(UriPartial.Path) == "https://apis.live.net/v5.0/me") + { + return await ReturnJsonResponse(new + { + id = "Test User ID", + name = "Test Name", + first_name = "Test Given Name", + last_name = "Test Family Name", + emails = new + { + preferred = "Test email" + } + }); + } + + return null; + } + }, + Notifications = new MicrosoftAccountAuthenticationNotifications + { + OnAuthenticated = context => + { + var refreshToken = context.RefreshToken; + context.Identity.AddClaim(new Claim("RefreshToken", refreshToken)); + return Task.FromResult(null); + } + } + }; + var server = CreateServer( + app => app.UseMicrosoftAccountAuthentication(options), + context => + { + Describe(context.Response, (ClaimsIdentity)context.User.Identity); + return true; + }); + var properties = new AuthenticationProperties(); + var correlationKey = ".AspNet.Correlation.Microsoft"; + var correlationValue = "TestCorrelationId"; + properties.Dictionary.Add(correlationKey, correlationValue); + properties.RedirectUri = "/me"; + var state = options.StateDataFormat.Protect(properties); + var transaction = await SendAsync(server, + "https://example.com/signin-microsoft?code=TestCode&state=" + Uri.EscapeDataString(state), + correlationKey + "=" + correlationValue); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); + transaction.Response.Headers.Location.ToString().ShouldBe("/me"); + transaction.SetCookie.Count.ShouldBe(2); + transaction.SetCookie[0].ShouldContain(correlationKey); + transaction.SetCookie[1].ShouldContain(".AspNet.External"); + + var authCookie = transaction.AuthenticationCookieValue; + transaction = await SendAsync(server, "https://example.com/me", authCookie); + transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); + transaction.FindClaimValue("RefreshToken").ShouldBe("Test Refresh Token"); + } + + private static TestServer CreateServer(Action configure, Func handler) + { + return TestServer.Create(app => + { + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = "External" + }); + app.SetDefaultSignInAsAuthenticationType("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(); + + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + return transaction; + } + + private static async Task ReturnJsonResponse(object content) + { + var res = new HttpResponseMessage(HttpStatusCode.OK); + var text = await JsonConvert.SerializeObjectAsync(content); + res.Content = new StringContent(text, Encoding.UTF8, "application/json"); + return res; + } + + private static void Describe(HttpResponse res, ClaimsIdentity identity) + { + res.StatusCode = 200; + res.ContentType = "text/xml"; + var xml = new XElement("xml"); + if (identity != null) + { + xml.Add(identity.Claims.Select(claim => new XElement("claim", new XAttribute("type", claim.Type), new XAttribute("value", claim.Value)))); + } + using (var memory = new MemoryStream()) + { + using (var writer = new XmlTextWriter(memory, Encoding.UTF8)) + { + xml.WriteTo(writer); + } + res.Body.Write(memory.ToArray(), 0, memory.ToArray().Length); + } + } + + private class TestHttpMessageHandler : HttpMessageHandler + { + public Func> Sender { get; set; } + + protected override async Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + if (Sender != null) + { + return await Sender(request); + } + + return null; + } + } + + private class Transaction + { + public HttpRequestMessage Request { get; set; } + public HttpResponseMessage Response { get; set; } + public IList SetCookie { get; set; } + public string ResponseText { get; set; } + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNet.External=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + + public string FindClaimValue(string claimType) + { + XElement claim = ResponseElement.Elements("claim").SingleOrDefault(elt => elt.Attribute("type").Value == claimType); + if (claim == null) + { + return null; + } + return claim.Attribute("value").Value; + } + } + } +} diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json index 4da6822916..4cfa4fb96a 100644 --- a/test/Microsoft.AspNet.Security.Test/project.json +++ b/test/Microsoft.AspNet.Security.Test/project.json @@ -8,6 +8,7 @@ "Microsoft.AspNet.Security.Cookies" : "1.0.0-*", "Microsoft.AspNet.Security.Facebook" : "1.0.0-*", "Microsoft.AspNet.Security.Google" : "1.0.0-*", + "Microsoft.AspNet.Security.MicrosoftAccount" : "1.0.0-*", "Microsoft.AspNet.Security.Twitter" : "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*",