diff --git a/Security.sln b/Security.sln index 5ae54786fc..976cfd4ea6 100644 --- a/Security.sln +++ b/Security.sln @@ -28,6 +28,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SocialSample", "samples\Soc EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.Google", "src\Microsoft.AspNet.Security.Google\Microsoft.AspNet.Security.Google.kproj", "{89BF8535-A849-458E-868A-A68FCF620486}" 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -108,6 +110,16 @@ Global {89BF8535-A849-458E-868A-A68FCF620486}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {89BF8535-A849-458E-868A-A68FCF620486}.Release|Mixed Platforms.Build.0 = Release|Any CPU {89BF8535-A849-458E-868A-A68FCF620486}.Release|x86.ActiveCfg = Release|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Debug|x86.ActiveCfg = Debug|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C96B77EA-4078-4C31-BDB2-878F11C5E061}.Release|Any CPU.Build.0 = Release|Any CPU + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -120,5 +132,6 @@ Global {3984651C-FD44-4394-8793-3D14EE348C04} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652} {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} EndGlobalSection EndGlobal diff --git a/samples/SocialSample/Project.json b/samples/SocialSample/Project.json index 525d9dd6a6..1af9beef0a 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.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 68dd967e40..bc2e967b3c 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.Twitter; namespace CookieSample { @@ -32,6 +33,12 @@ namespace CookieSample ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f", }); + app.UseTwitterAuthentication(new TwitterAuthenticationOptions() + { + ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g", + ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI", + }); + // Choose an authentication type app.Map("/login", signoutApp => { diff --git a/src/Microsoft.AspNet.Security.Twitter/Messages/AccessToken.cs b/src/Microsoft.AspNet.Security.Twitter/Messages/AccessToken.cs new file mode 100644 index 0000000000..f935da72a7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Messages/AccessToken.cs @@ -0,0 +1,21 @@ +// 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.Twitter.Messages +{ + /// + /// The Twitter access token retrieved from the access token endpoint. + /// + public class AccessToken : RequestToken + { + /// + /// Gets or sets the Twitter User ID. + /// + public string UserId { get; set; } + + /// + /// Gets or sets the Twitter screen name. + /// + public string ScreenName { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Messages/RequestToken.cs b/src/Microsoft.AspNet.Security.Twitter/Messages/RequestToken.cs new file mode 100644 index 0000000000..f801e555f7 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Messages/RequestToken.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http.Security; + +namespace Microsoft.AspNet.Security.Twitter.Messages +{ + /// + /// The Twitter request token obtained from the request token endpoint. + /// + public class RequestToken + { + /// + /// Gets or sets the Twitter request token. + /// + public string Token { get; set; } + + /// + /// Gets or sets the Twitter token secret. + /// + public string TokenSecret { get; set; } + + public bool CallbackConfirmed { get; set; } + + /// + /// Gets or sets a property bag for common authentication properties. + /// + public AuthenticationProperties Properties { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Messages/RequestTokenSerializer.cs b/src/Microsoft.AspNet.Security.Twitter/Messages/RequestTokenSerializer.cs new file mode 100644 index 0000000000..725dcc341f --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Messages/RequestTokenSerializer.cs @@ -0,0 +1,93 @@ +// 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.IO; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.DataHandler.Serializer; + +namespace Microsoft.AspNet.Security.Twitter.Messages +{ + /// + /// Serializes and deserializes Twitter request and access tokens so that they can be used by other application components. + /// + public class RequestTokenSerializer : IDataSerializer + { + private const int FormatVersion = 1; + + /// + /// Serialize a request token. + /// + /// The token to serialize + /// A byte array containing the serialized token + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] + public virtual byte[] Serialize(RequestToken model) + { + using (var memory = new MemoryStream()) + { + using (var writer = new BinaryWriter(memory)) + { + Write(writer, model); + writer.Flush(); + return memory.ToArray(); + } + } + } + + /// + /// Deserializes a request token. + /// + /// A byte array containing the serialized token + /// The Twitter request token + [SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times", Justification = "Dispose is idempotent")] + public virtual RequestToken Deserialize(byte[] data) + { + using (var memory = new MemoryStream(data)) + { + using (var reader = new BinaryReader(memory)) + { + return Read(reader); + } + } + } + + /// + /// Writes a Twitter request token as a series of bytes. Used by the method. + /// + /// The writer to use in writing the token + /// The token to write + public static void Write([NotNull] BinaryWriter writer, [NotNull] RequestToken token) + { + writer.Write(FormatVersion); + writer.Write(token.Token); + writer.Write(token.TokenSecret); + writer.Write(token.CallbackConfirmed); + PropertiesSerializer.Write(writer, token.Properties); + } + + /// + /// Reads a Twitter request token from a series of bytes. Used by the method. + /// + /// The reader to use in reading the token bytes + /// The token + public static RequestToken Read([NotNull] BinaryReader reader) + { + if (reader.ReadInt32() != FormatVersion) + { + return null; + } + + string token = reader.ReadString(); + string tokenSecret = reader.ReadString(); + bool callbackConfirmed = reader.ReadBoolean(); + AuthenticationProperties properties = PropertiesSerializer.Read(reader); + if (properties == null) + { + return null; + } + + return new RequestToken { Token = token, TokenSecret = tokenSecret, CallbackConfirmed = callbackConfirmed, Properties = properties }; + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Messages/Serializers.cs b/src/Microsoft.AspNet.Security.Twitter/Messages/Serializers.cs new file mode 100644 index 0000000000..3d2e1a458d --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Messages/Serializers.cs @@ -0,0 +1,23 @@ +// 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.DataHandler.Serializer; + +namespace Microsoft.AspNet.Security.Twitter.Messages +{ + /// + /// Provides access to a request token serializer. + /// + public static class Serializers + { + static Serializers() + { + RequestToken = new RequestTokenSerializer(); + } + + /// + /// Gets or sets a statically-avaliable serializer object. The value for this property will be by default. + /// + public static IDataSerializer RequestToken { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Microsoft.AspNet.Security.Twitter.kproj b/src/Microsoft.AspNet.Security.Twitter/Microsoft.AspNet.Security.Twitter.kproj new file mode 100644 index 0000000000..48dfc30697 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Microsoft.AspNet.Security.Twitter.kproj @@ -0,0 +1,28 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + c96b77ea-4078-4c31-bdb2-878f11c5e061 + Library + + + + ConsoleDebugger + + + WebDebugger + + + + + + + + 2.0 + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Twitter/NotNullAttribute.cs b/src/Microsoft.AspNet.Security.Twitter/NotNullAttribute.cs new file mode 100644 index 0000000000..0d6e98224d --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/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.Twitter +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Twitter/Notifications/ITwitterAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Twitter/Notifications/ITwitterAuthenticationNotifications.cs new file mode 100644 index 0000000000..55fd548926 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Notifications/ITwitterAuthenticationNotifications.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.Twitter +{ + /// + /// Specifies callback methods which the invokes to enable developer control over the authentication process. /> + /// + public interface ITwitterAuthenticationNotifications + { + /// + /// Invoked whenever Twitter succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + Task Authenticated(TwitterAuthenticatedContext 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(TwitterReturnEndpointContext context); + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware + /// + /// Contains redirect URI and of the challenge + void ApplyRedirect(TwitterApplyRedirectContext context); + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterApplyRedirectContext.cs b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterApplyRedirectContext.cs new file mode 100644 index 0000000000..3d5ab80ffc --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterApplyRedirectContext.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.Twitter +{ + /// + /// 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 + public TwitterApplyRedirectContext(HttpContext context, TwitterAuthenticationOptions 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 authenticaiton properties of the challenge + /// + public AuthenticationProperties Properties { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterAuthenticatedContext.cs b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterAuthenticatedContext.cs new file mode 100644 index 0000000000..0c050d9e8d --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterAuthenticatedContext.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.Security.Claims; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Security; +using Microsoft.AspNet.Security.Notifications; + +namespace Microsoft.AspNet.Security.Twitter +{ + /// + /// Contains information about the login session as well as the user . + /// + public class TwitterAuthenticatedContext : BaseContext + { + /// + /// Initializes a + /// + /// The HTTP environment + /// Twitter user ID + /// Twitter screen name + /// Twitter access token + /// Twitter access token secret + public TwitterAuthenticatedContext( + HttpContext context, + string userId, + string screenName, + string accessToken, + string accessTokenSecret) + : base(context) + { + UserId = userId; + ScreenName = screenName; + AccessToken = accessToken; + AccessTokenSecret = accessTokenSecret; + } + + /// + /// Gets the Twitter user ID + /// + public string UserId { get; private set; } + + /// + /// Gets the Twitter screen name + /// + public string ScreenName { get; private set; } + + /// + /// Gets the Twitter access token + /// + public string AccessToken { get; private set; } + + /// + /// Gets the Twitter access token secret + /// + public string AccessTokenSecret { 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; } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterAuthenticationNotifications.cs b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterAuthenticationNotifications.cs new file mode 100644 index 0000000000..2c6ff19d5e --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterAuthenticationNotifications.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.Twitter +{ + /// + /// Default implementation. + /// + public class TwitterAuthenticationNotifications : ITwitterAuthenticationNotifications + { + /// + /// Initializes a + /// + public TwitterAuthenticationNotifications() + { + OnAuthenticated = context => Task.FromResult(null); + OnReturnEndpoint = context => Task.FromResult(null); + OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri); + } + + /// + /// Gets or sets the function that is invoked when the Authenticated method is invoked. + /// + public Func OnAuthenticated { get; set; } + + /// + /// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked. + /// + public Func OnReturnEndpoint { get; set; } + + /// + /// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked. + /// + public Action OnApplyRedirect { get; set; } + + /// + /// Invoked whenever Twitter succesfully authenticates a user + /// + /// Contains information about the login session as well as the user . + /// A representing the completed operation. + public virtual Task Authenticated(TwitterAuthenticatedContext context) + { + return OnAuthenticated(context); + } + + /// + /// Invoked prior to the being saved in a local cookie and the browser being redirected to the originally requested URL. + /// + /// + /// A representing the completed operation. + public virtual Task ReturnEndpoint(TwitterReturnEndpointContext context) + { + return OnReturnEndpoint(context); + } + + /// + /// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware + /// + /// Contains redirect URI and of the challenge + public virtual void ApplyRedirect(TwitterApplyRedirectContext context) + { + OnApplyRedirect(context); + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterReturnEndpointContext.cs b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterReturnEndpointContext.cs new file mode 100644 index 0000000000..e420b5d1fb --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Notifications/TwitterReturnEndpointContext.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.Twitter +{ + /// + /// Provides context information to middleware providers. + /// + public class TwitterReturnEndpointContext : ReturnEndpointContext + { + /// + /// Initializes a new . + /// + /// HTTP environment + /// The authentication ticket + public TwitterReturnEndpointContext( + HttpContext context, + AuthenticationTicket ticket) + : base(context, ticket) + { + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Project.json b/src/Microsoft.AspNet.Security.Twitter/Project.json new file mode 100644 index 0000000000..9f19015e3d --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Project.json @@ -0,0 +1,44 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Microsoft.AspNet.Http": "1.0.0-*", + "Microsoft.AspNet.RequestContainer": "1.0.0-*", + "Microsoft.AspNet.Security": "1.0.0-*", + "Microsoft.AspNet.Security.DataProtection": "1.0.0-*", + "Microsoft.AspNet.WebUtilities": "1.0.0-*", + "Microsoft.Framework.Logging": "1.0.0-*", + "Newtonsoft.Json": "5.0.8", + "System.Net.Http": "4.0.0.0" + }, + "frameworks": { + "net45": { + "dependencies": { + "System.Net.Http.WebRequest": "" + } + }, + "k10": { + "dependencies": { + "System.Collections": "4.0.10.0", + "System.ComponentModel": "4.0.0.0", + "System.Console": "4.0.0.0", + "System.Diagnostics.Debug": "4.0.10.0", + "System.Diagnostics.Tools": "4.0.0.0", + "System.Globalization": "4.0.10.0", + "System.IO": "4.0.10.0", + "System.IO.Compression": "4.0.0.0", + "System.Linq": "4.0.0.0", + "System.Net.Http.WinHttpHandler": "4.0.0.0", + "System.Reflection": "4.0.10.0", + "System.Resources.ResourceManager": "4.0.0.0", + "System.Runtime": "4.0.20.0", + "System.Runtime.Extensions": "4.0.10.0", + "System.Runtime.InteropServices": "4.0.20.0", + "System.Security.Claims": "1.0.0-*", + "System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0", + "System.Security.Principal": "4.0.0.0", + "System.Threading": "4.0.0.0", + "System.Threading.Tasks": "4.0.10.0" + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Resources.Designer.cs b/src/Microsoft.AspNet.Security.Twitter/Resources.Designer.cs new file mode 100644 index 0000000000..deda3cc773 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Resources.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.32559 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNet.Security.Twitter { + 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.Twitter.Resources", System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(Resources)).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The '{0}' option must be provided.. + /// + internal static string Exception_OptionMustBeProvided { + get { + return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.. + /// + internal static string Exception_ValidatorHandlerMismatch { + get { + return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/Resources.resx b/src/Microsoft.AspNet.Security.Twitter/Resources.resx new file mode 100644 index 0000000000..2a19bea96a --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/Resources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The '{0}' option must be provided. + + + An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationDefaults.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationDefaults.cs new file mode 100644 index 0000000000..1f29a04b1a --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationDefaults.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.Twitter +{ + public static class TwitterAuthenticationDefaults + { + public const string AuthenticationType = "Twitter"; + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationExtensions.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationExtensions.cs new file mode 100644 index 0000000000..250c721cba --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationExtensions.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.Twitter; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for using + /// + public static class TwitterAuthenticationExtensions + { + /// + /// Authenticate users using Twitter + /// + /// The passed to the configure method + /// The Twitter-issued consumer key + /// The Twitter-issued consumer secret + /// The updated + public static IBuilder UseTwitterAuthentication([NotNull] this IBuilder app, [NotNull] string consumerKey, [NotNull] string consumerSecret) + { + return app.UseTwitterAuthentication( + new TwitterAuthenticationOptions + { + ConsumerKey = consumerKey, + ConsumerSecret = consumerSecret, + }); + } + + /// + /// Authenticate users using Twitter + /// + /// The passed to the configure method + /// Middleware configuration options + /// The updated + public static IBuilder UseTwitterAuthentication([NotNull] this IBuilder app, [NotNull] TwitterAuthenticationOptions options) + { + if (string.IsNullOrEmpty(options.SignInAsAuthenticationType)) + { + options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(); + } + return app.UseMiddleware(options); + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs new file mode 100644 index 0000000000..b24aa71c30 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationHandler.cs @@ -0,0 +1,384 @@ +// 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.Security.Twitter.Messages; +using Microsoft.AspNet.WebUtilities; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Security.Twitter +{ + internal class TwitterAuthenticationHandler : AuthenticationHandler + { + private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const string StateCookie = "__TwitterState"; + private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; + private const string AuthenticationEndpoint = "https://twitter.com/oauth/authenticate?oauth_token="; + private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; + + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public TwitterAuthenticationHandler(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 + { + IReadableStringCollection query = Request.Query; + string protectedRequestToken = Request.Cookies[StateCookie]; + + RequestToken requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); + + if (requestToken == null) + { + _logger.WriteWarning("Invalid state"); + return null; + } + + properties = requestToken.Properties; + + string returnedToken = query.Get("oauth_token"); + if (string.IsNullOrWhiteSpace(returnedToken)) + { + _logger.WriteWarning("Missing oauth_token"); + return new AuthenticationTicket(null, properties); + } + + if (returnedToken != requestToken.Token) + { + _logger.WriteWarning("Unmatched token"); + return new AuthenticationTicket(null, properties); + } + + string oauthVerifier = query.Get("oauth_verifier"); + if (string.IsNullOrWhiteSpace(oauthVerifier)) + { + _logger.WriteWarning("Missing or blank oauth_verifier"); + return new AuthenticationTicket(null, properties); + } + + AccessToken accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); + + var context = new TwitterAuthenticatedContext(Context, accessToken.UserId, accessToken.ScreenName, accessToken.Token, accessToken.TokenSecret); + + context.Identity = new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType), + new Claim(ClaimTypes.Name, accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType), + new Claim("urn:twitter:userid", accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType), + new Claim("urn:twitter:screenname", accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationType) + }, + Options.AuthenticationType, + ClaimsIdentity.DefaultNameClaimType, + ClaimsIdentity.DefaultRoleClaimType); + context.Properties = requestToken.Properties; + + Response.Cookies.Delete(StateCookie); + + 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() + { + ApplyResponseChallengeAsync().Wait(); + } + + protected override async Task ApplyResponseChallengeAsync() + { + 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 requestPrefix = Request.Scheme + "://" + Request.Host; + string callBackUrl = requestPrefix + RequestPathBase + Options.CallbackPath; + + AuthenticationProperties properties; + if (ChallengeContext == null) + { + properties = new AuthenticationProperties(); + } + else + { + properties = new AuthenticationProperties(ChallengeContext.Properties); + } + if (string.IsNullOrEmpty(properties.RedirectUri)) + { + properties.RedirectUri = requestPrefix + Request.PathBase + Request.Path + Request.QueryString; + } + + RequestToken requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, callBackUrl, properties); + + if (requestToken.CallbackConfirmed) + { + string twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; + + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Secure = Request.IsSecure + }; + + Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions); + + var redirectContext = new TwitterApplyRedirectContext( + Context, Options, + properties, twitterAuthenticationEndpoint); + Options.Notifications.ApplyRedirect(redirectContext); + } + else + { + _logger.WriteError("requestToken CallbackConfirmed!=true"); + } + } + + 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 TwitterReturnEndpointContext(Context, model) + { + SignInAsAuthenticationType = Options.SignInAsAuthenticationType, + 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 async Task ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties) + { + _logger.WriteVerbose("ObtainRequestToken"); + + string nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_callback", callBackUri }, + { "oauth_consumer_key", consumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_version", "1.0" } + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", Uri.EscapeDataString(authorizationKey.Key), Uri.EscapeDataString(authorizationKey.Value)); + } + parameterBuilder.Length--; + string parameterString = parameterBuilder.ToString(); + + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(Uri.EscapeDataString(RequestTokenEndpoint)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(Uri.EscapeDataString(parameterString)); + + string signature = ComputeSignature(consumerSecret, null, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, Uri.EscapeDataString(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Post, RequestTokenEndpoint); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + HttpResponseMessage response = await _httpClient.SendAsync(request, Context.RequestAborted); + response.EnsureSuccessStatusCode(); + string responseText = await response.Content.ReadAsStringAsync(); + + IFormCollection responseParameters = FormHelpers.ParseForm(responseText); + if (string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) + { + return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; + } + + return new RequestToken(); + } + + private async Task ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier) + { + // https://dev.twitter.com/docs/api/1/post/oauth/access_token + + _logger.WriteVerbose("ObtainAccessToken"); + + string nonce = Guid.NewGuid().ToString("N"); + + var authorizationParts = new SortedDictionary + { + { "oauth_consumer_key", consumerKey }, + { "oauth_nonce", nonce }, + { "oauth_signature_method", "HMAC-SHA1" }, + { "oauth_token", token.Token }, + { "oauth_timestamp", GenerateTimeStamp() }, + { "oauth_verifier", verifier }, + { "oauth_version", "1.0" }, + }; + + var parameterBuilder = new StringBuilder(); + foreach (var authorizationKey in authorizationParts) + { + parameterBuilder.AppendFormat("{0}={1}&", Uri.EscapeDataString(authorizationKey.Key), Uri.EscapeDataString(authorizationKey.Value)); + } + parameterBuilder.Length--; + string parameterString = parameterBuilder.ToString(); + + var canonicalizedRequestBuilder = new StringBuilder(); + canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(Uri.EscapeDataString(AccessTokenEndpoint)); + canonicalizedRequestBuilder.Append("&"); + canonicalizedRequestBuilder.Append(Uri.EscapeDataString(parameterString)); + + string signature = ComputeSignature(consumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString()); + authorizationParts.Add("oauth_signature", signature); + authorizationParts.Remove("oauth_verifier"); + + var authorizationHeaderBuilder = new StringBuilder(); + authorizationHeaderBuilder.Append("OAuth "); + foreach (var authorizationPart in authorizationParts) + { + authorizationHeaderBuilder.AppendFormat( + "{0}=\"{1}\", ", authorizationPart.Key, Uri.EscapeDataString(authorizationPart.Value)); + } + authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; + + var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint); + request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); + + var formPairs = new List>() + { + new KeyValuePair("oauth_verifier", verifier) + }; + + request.Content = new FormUrlEncodedContent(formPairs); + + HttpResponseMessage response = await _httpClient.SendAsync(request, Context.RequestAborted); + + if (!response.IsSuccessStatusCode) + { + _logger.WriteError("AccessToken request failed with a status code of " + response.StatusCode); + response.EnsureSuccessStatusCode(); // throw + } + + string responseText = await response.Content.ReadAsStringAsync(); + + IFormCollection responseParameters = FormHelpers.ParseForm(responseText); + + return new AccessToken + { + Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), + TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), + UserId = Uri.UnescapeDataString(responseParameters["user_id"]), + ScreenName = Uri.UnescapeDataString(responseParameters["screen_name"]) + }; + } + + private static string GenerateTimeStamp() + { + TimeSpan secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch; + return Convert.ToInt64(secondsSinceUnixEpocStart.TotalSeconds).ToString(CultureInfo.InvariantCulture); + } + + private static string ComputeSignature(string consumerSecret, string tokenSecret, string signatureData) + { + using (var algorithm = new HMACSHA1()) + { + algorithm.Key = Encoding.ASCII.GetBytes( + string.Format(CultureInfo.InvariantCulture, + "{0}&{1}", + Uri.EscapeDataString(consumerSecret), + string.IsNullOrEmpty(tokenSecret) ? string.Empty : Uri.EscapeDataString(tokenSecret))); + byte[] hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData)); + return Convert.ToBase64String(hash); + } + } + + protected override void ApplyResponseGrant() + { + // N/A - No SignIn or SignOut support. + } + } +} diff --git a/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationMiddleware.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationMiddleware.cs new file mode 100644 index 0000000000..421a685575 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationMiddleware.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.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net.Http; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Security.DataHandler; +using Microsoft.AspNet.Security.DataHandler.Encoder; +using Microsoft.AspNet.Security.DataProtection; +using Microsoft.AspNet.Security.Infrastructure; +using Microsoft.AspNet.Security.Twitter.Messages; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Security.Twitter +{ + /// + /// ASP.NET middleware for authenticating users using Twitter + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")] + public class TwitterAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + + /// + /// Initializes a + /// + /// The next middleware in the HTTP pipeline to invoke + /// + /// + /// Configuration options for the middleware + public TwitterAuthenticationMiddleware( + RequestDelegate next, + IDataProtectionProvider dataProtectionProvider, + ILoggerFactory loggerFactory, + TwitterAuthenticationOptions options) + : base(next, options) + { + if (string.IsNullOrWhiteSpace(Options.ConsumerSecret)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ConsumerSecret")); + } + if (string.IsNullOrWhiteSpace(Options.ConsumerKey)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Exception_OptionMustBeProvided, "ConsumerKey")); + } + + _logger = loggerFactory.Create(typeof(TwitterAuthenticationMiddleware).FullName); + + if (Options.Notifications == null) + { + Options.Notifications = new TwitterAuthenticationNotifications(); + } + if (Options.StateDataFormat == null) + { + IDataProtector dataProtector = DataProtectionHelpers.CreateDataProtector(dataProtectionProvider, + typeof(TwitterAuthenticationMiddleware).FullName, options.AuthenticationType, "v1"); + Options.StateDataFormat = new SecureDataFormat( + Serializers.RequestToken, + dataProtector, + TextEncodings.Base64Url); + } + + _httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); + _httpClient.Timeout = Options.BackchannelTimeout; + _httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB + _httpClient.DefaultRequestHeaders.Accept.ParseAdd("*/*"); + _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Microsoft ASP.NET Twitter middleware"); + _httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + + /// + /// Provides the object for processing authentication-related requests. + /// + /// An configured with the supplied to the constructor. + protected override AuthenticationHandler CreateHandler() + { + return new TwitterAuthenticationHandler(_httpClient, _logger); + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Managed by caller")] + private static HttpMessageHandler ResolveHttpMessageHandler(TwitterAuthenticationOptions 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.Twitter/TwitterAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationOptions.cs new file mode 100644 index 0000000000..fcf307d5c4 --- /dev/null +++ b/src/Microsoft.AspNet.Security.Twitter/TwitterAuthenticationOptions.cs @@ -0,0 +1,107 @@ +// 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.Net.Http; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Twitter.Messages; + +namespace Microsoft.AspNet.Security.Twitter +{ + /// + /// Options for the Twitter authentication middleware. + /// + public class TwitterAuthenticationOptions : AuthenticationOptions + { + /// + /// Initializes a new instance of the class. + /// + public TwitterAuthenticationOptions() + : base(TwitterAuthenticationDefaults.AuthenticationType) + { + Caption = TwitterAuthenticationDefaults.AuthenticationType; + CallbackPath = new PathString("/signin-twitter"); + AuthenticationMode = AuthenticationMode.Passive; + BackchannelTimeout = TimeSpan.FromSeconds(60); +#if NET45 + // Twitter lists its valid Subject Key Identifiers at https://dev.twitter.com/docs/security/using-ssl + BackchannelCertificateValidator = new CertificateSubjectKeyIdentifierValidator( + new[] + { + "A5EF0B11CEC04103A34A659048B21CE0572D7D47", // VeriSign Class 3 Secure Server CA - G2 + "0D445C165344C1827E1D20AB25F40163D8BE79A5", // VeriSign Class 3 Secure Server CA - G3 + "5F60CF619055DF8443148A602AB2F57AF44318EF", // Symantec Class 3 Secure Server CA - G4 + }); +#endif + } + + /// + /// Gets or sets the consumer key used to communicate with Twitter. + /// + /// The consumer key used to communicate with Twitter. + public string ConsumerKey { get; set; } + + /// + /// Gets or sets the consumer secret used to sign requests to Twitter. + /// + /// The consumer secret used to sign requests to Twitter. + public string ConsumerSecret { get; set; } + + /// + /// Gets or sets timeout value in milliseconds for back channel communications with Twitter. + /// + /// + /// The back channel timeout. + /// + public TimeSpan BackchannelTimeout { 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 Twitter. + /// + /// + /// 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 + /// + /// The HttpMessageHandler used to communicate with Twitter. + /// 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-twitter". + /// + 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 type used to secure data handled by the middleware. + /// + public ISecureDataFormat StateDataFormat { get; set; } + + /// + /// Gets or sets the used to handle authentication events. + /// + public ITwitterAuthenticationNotifications Notifications { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs b/src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs index 476fe7bb7d..a1864b44d0 100644 --- a/src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs +++ b/src/Microsoft.AspNet.Security/DataHandler/Serializer/DataSerializers.cs @@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Security.DataHandler.Serializer Ticket = new TicketSerializer(); } - public static IDataSerializer Properties { get; set; } + public static IDataSerializer Properties { get; private set; } - public static IDataSerializer Ticket { get; set; } + public static IDataSerializer Ticket { get; private set; } } } diff --git a/test/Microsoft.AspNet.Security.Test/Twitter/TwitterMiddlewareTests.cs b/test/Microsoft.AspNet.Security.Test/Twitter/TwitterMiddlewareTests.cs new file mode 100644 index 0000000000..d5eb56896b --- /dev/null +++ b/test/Microsoft.AspNet.Security.Test/Twitter/TwitterMiddlewareTests.cs @@ -0,0 +1,183 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.Security.Twitter; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Shouldly; +using Xunit; + +namespace Microsoft.AspNet.Security.Twitter +{ + public class TwitterMiddlewareTests + { + [Fact] + public async Task ChallengeWillTriggerApplyRedirectEvent() + { + var options = new TwitterAuthenticationOptions() + { + ConsumerKey = "Test Consumer Key", + ConsumerSecret = "Test Consumer Secret", + Notifications = new TwitterAuthenticationNotifications + { + OnApplyRedirect = context => + { + context.Response.Redirect(context.RedirectUri + "&custom=test"); + } + }, + BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = + new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", + Encoding.UTF8, + "application/x-www-form-urlencoded") + }); + } + return Task.FromResult(null); + } + }, + BackchannelCertificateValidator = null + }; + var server = CreateServer( + app => app.UseTwitterAuthentication(options), + context => + { + context.Response.Challenge("Twitter"); + 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 options = new TwitterAuthenticationOptions() + { + ConsumerKey = "Test Consumer Key", + ConsumerSecret = "Test Consumer Secret", + BackchannelHttpHandler = new TestHttpMessageHandler + { + Sender = req => + { + if (req.RequestUri.AbsoluteUri == "https://api.twitter.com/oauth/request_token") + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = + new StringContent("oauth_callback_confirmed=true&oauth_token=test_oauth_token&oauth_token_secret=test_oauth_token_secret", + Encoding.UTF8, + "application/x-www-form-urlencoded") + }); + } + return Task.FromResult(null); + } + }, + BackchannelCertificateValidator = null + }; + var server = CreateServer( + app => app.UseTwitterAuthentication(options), + context => + { + context.Response.Challenge("Twitter"); + 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://twitter.com/oauth/authenticate?oauth_token="); + } + + private static TestServer CreateServer(Action configure, Func handler) + { + return TestServer.Create(app => + { + app.SetDefaultSignInAsAuthenticationType("External"); + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = "External" + }); + if (configure != null) + { + configure(app); + } + app.Use(async (context, next) => + { + if (handler == null || !handler(context)) + { + await next(); + } + }); + }); + } + + private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + var transaction = new Transaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + + return transaction; + } + + private 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 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; } + } + } +} diff --git a/test/Microsoft.AspNet.Security.Test/project.json b/test/Microsoft.AspNet.Security.Test/project.json index 542d5ee259..976f4ef7a1 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.Twitter" : "1.0.0-*", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", "System.Net.Http": "4.0.0.0",