#38 - Port the Twitter middleware from katana

This commit is contained in:
Chris Ross 2014-08-25 13:50:34 -07:00
parent 5577159453
commit b9eb7ba282
25 changed files with 1552 additions and 2 deletions

View File

@ -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

View File

@ -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-*"
},

View File

@ -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 =>
{

View File

@ -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
{
/// <summary>
/// The Twitter access token retrieved from the access token endpoint.
/// </summary>
public class AccessToken : RequestToken
{
/// <summary>
/// Gets or sets the Twitter User ID.
/// </summary>
public string UserId { get; set; }
/// <summary>
/// Gets or sets the Twitter screen name.
/// </summary>
public string ScreenName { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// The Twitter request token obtained from the request token endpoint.
/// </summary>
public class RequestToken
{
/// <summary>
/// Gets or sets the Twitter request token.
/// </summary>
public string Token { get; set; }
/// <summary>
/// Gets or sets the Twitter token secret.
/// </summary>
public string TokenSecret { get; set; }
public bool CallbackConfirmed { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties.
/// </summary>
public AuthenticationProperties Properties { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Serializes and deserializes Twitter request and access tokens so that they can be used by other application components.
/// </summary>
public class RequestTokenSerializer : IDataSerializer<RequestToken>
{
private const int FormatVersion = 1;
/// <summary>
/// Serialize a request token.
/// </summary>
/// <param name="model">The token to serialize</param>
/// <returns>A byte array containing the serialized token</returns>
[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();
}
}
}
/// <summary>
/// Deserializes a request token.
/// </summary>
/// <param name="data">A byte array containing the serialized token</param>
/// <returns>The Twitter request token</returns>
[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);
}
}
}
/// <summary>
/// Writes a Twitter request token as a series of bytes. Used by the <see cref="Serialize"/> method.
/// </summary>
/// <param name="writer">The writer to use in writing the token</param>
/// <param name="token">The token to write</param>
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);
}
/// <summary>
/// Reads a Twitter request token from a series of bytes. Used by the <see cref="Deserialize"/> method.
/// </summary>
/// <param name="reader">The reader to use in reading the token bytes</param>
/// <returns>The token</returns>
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 };
}
}
}

View File

@ -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
{
/// <summary>
/// Provides access to a request token serializer.
/// </summary>
public static class Serializers
{
static Serializers()
{
RequestToken = new RequestTokenSerializer();
}
/// <summary>
/// Gets or sets a statically-avaliable serializer object. The value for this property will be <see cref="RequestTokenSerializer"/> by default.
/// </summary>
public static IDataSerializer<RequestToken> RequestToken { get; private set; }
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="__ToolsVersion__" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>c96b77ea-4078-4c31-bdb2-878f11c5e061</ProjectGuid>
<OutputType>Library</OutputType>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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
{
}
}

View File

@ -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
{
/// <summary>
/// Specifies callback methods which the <see cref="TwitterAuthenticationMiddleware"></see> invokes to enable developer control over the authentication process. />
/// </summary>
public interface ITwitterAuthenticationNotifications
{
/// <summary>
/// Invoked whenever Twitter succesfully authenticates a user
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task Authenticated(TwitterAuthenticatedContext context);
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
Task ReturnEndpoint(TwitterReturnEndpointContext context);
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge </param>
void ApplyRedirect(TwitterApplyRedirectContext context);
}
}

View File

@ -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
{
/// <summary>
/// Context passed when a Challenge causes a redirect to authorize endpoint in the Twitter middleware
/// </summary>
public class TwitterApplyRedirectContext : BaseContext<TwitterAuthenticationOptions>
{
/// <summary>
/// Creates a new context object.
/// </summary>
/// <param name="context">The HTTP request context</param>
/// <param name="options">The Facebook middleware options</param>
/// <param name="properties">The authenticaiton properties of the challenge</param>
/// <param name="redirectUri">The initial redirect URI</param>
public TwitterApplyRedirectContext(HttpContext context, TwitterAuthenticationOptions options,
AuthenticationProperties properties, string redirectUri)
: base(context, options)
{
RedirectUri = redirectUri;
Properties = properties;
}
/// <summary>
/// Gets the URI used for the redirect operation.
/// </summary>
public string RedirectUri { get; private set; }
/// <summary>
/// Gets the authenticaiton properties of the challenge
/// </summary>
public AuthenticationProperties Properties { get; private set; }
}
}

View File

@ -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
{
/// <summary>
/// Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public class TwitterAuthenticatedContext : BaseContext
{
/// <summary>
/// Initializes a <see cref="TwitterAuthenticatedContext"/>
/// </summary>
/// <param name="context">The HTTP environment</param>
/// <param name="userId">Twitter user ID</param>
/// <param name="screenName">Twitter screen name</param>
/// <param name="accessToken">Twitter access token</param>
/// <param name="accessTokenSecret">Twitter access token secret</param>
public TwitterAuthenticatedContext(
HttpContext context,
string userId,
string screenName,
string accessToken,
string accessTokenSecret)
: base(context)
{
UserId = userId;
ScreenName = screenName;
AccessToken = accessToken;
AccessTokenSecret = accessTokenSecret;
}
/// <summary>
/// Gets the Twitter user ID
/// </summary>
public string UserId { get; private set; }
/// <summary>
/// Gets the Twitter screen name
/// </summary>
public string ScreenName { get; private set; }
/// <summary>
/// Gets the Twitter access token
/// </summary>
public string AccessToken { get; private set; }
/// <summary>
/// Gets the Twitter access token secret
/// </summary>
public string AccessTokenSecret { get; private set; }
/// <summary>
/// Gets the <see cref="ClaimsIdentity"/> representing the user
/// </summary>
public ClaimsIdentity Identity { get; set; }
/// <summary>
/// Gets or sets a property bag for common authentication properties
/// </summary>
public AuthenticationProperties Properties { get; set; }
}
}

View File

@ -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
{
/// <summary>
/// Default <see cref="ITwitterAuthenticationNotifications"/> implementation.
/// </summary>
public class TwitterAuthenticationNotifications : ITwitterAuthenticationNotifications
{
/// <summary>
/// Initializes a <see cref="TwitterAuthenticationNotifications"/>
/// </summary>
public TwitterAuthenticationNotifications()
{
OnAuthenticated = context => Task.FromResult<object>(null);
OnReturnEndpoint = context => Task.FromResult<object>(null);
OnApplyRedirect = context => context.Response.Redirect(context.RedirectUri);
}
/// <summary>
/// Gets or sets the function that is invoked when the Authenticated method is invoked.
/// </summary>
public Func<TwitterAuthenticatedContext, Task> OnAuthenticated { get; set; }
/// <summary>
/// Gets or sets the function that is invoked when the ReturnEndpoint method is invoked.
/// </summary>
public Func<TwitterReturnEndpointContext, Task> OnReturnEndpoint { get; set; }
/// <summary>
/// Gets or sets the delegate that is invoked when the ApplyRedirect method is invoked.
/// </summary>
public Action<TwitterApplyRedirectContext> OnApplyRedirect { get; set; }
/// <summary>
/// Invoked whenever Twitter succesfully authenticates a user
/// </summary>
/// <param name="context">Contains information about the login session as well as the user <see cref="System.Security.Claims.ClaimsIdentity"/>.</param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task Authenticated(TwitterAuthenticatedContext context)
{
return OnAuthenticated(context);
}
/// <summary>
/// Invoked prior to the <see cref="System.Security.Claims.ClaimsIdentity"/> being saved in a local cookie and the browser being redirected to the originally requested URL.
/// </summary>
/// <param name="context"></param>
/// <returns>A <see cref="Task"/> representing the completed operation.</returns>
public virtual Task ReturnEndpoint(TwitterReturnEndpointContext context)
{
return OnReturnEndpoint(context);
}
/// <summary>
/// Called when a Challenge causes a redirect to authorize endpoint in the Twitter middleware
/// </summary>
/// <param name="context">Contains redirect URI and <see cref="AuthenticationProperties"/> of the challenge </param>
public virtual void ApplyRedirect(TwitterApplyRedirectContext context)
{
OnApplyRedirect(context);
}
}
}

View File

@ -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
{
/// <summary>
/// Provides context information to middleware providers.
/// </summary>
public class TwitterReturnEndpointContext : ReturnEndpointContext
{
/// <summary>
/// Initializes a new <see cref="TwitterReturnEndpointContext"/>.
/// </summary>
/// <param name="context">HTTP environment</param>
/// <param name="ticket">The authentication ticket</param>
public TwitterReturnEndpointContext(
HttpContext context,
AuthenticationTicket ticket)
: base(context, ticket)
{
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -0,0 +1,81 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Microsoft.AspNet.Security.Twitter {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to The &apos;{0}&apos; option must be provided..
/// </summary>
internal static string Exception_OptionMustBeProvided {
get {
return ResourceManager.GetString("Exception_OptionMustBeProvided", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler..
/// </summary>
internal static string Exception_ValidatorHandlerMismatch {
get {
return ResourceManager.GetString("Exception_ValidatorHandlerMismatch", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Exception_OptionMustBeProvided" xml:space="preserve">
<value>The '{0}' option must be provided.</value>
</data>
<data name="Exception_ValidatorHandlerMismatch" xml:space="preserve">
<value>An ICertificateValidator cannot be specified at the same time as an HttpMessageHandler unless it is a WebRequestHandler.</value>
</data>
</root>

View File

@ -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";
}
}

View File

@ -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
{
/// <summary>
/// Extension methods for using <see cref="TwitterAuthenticationMiddleware"/>
/// </summary>
public static class TwitterAuthenticationExtensions
{
/// <summary>
/// Authenticate users using Twitter
/// </summary>
/// <param name="app">The <see cref="IBuilder"/> passed to the configure method</param>
/// <param name="consumerKey">The Twitter-issued consumer key</param>
/// <param name="consumerSecret">The Twitter-issued consumer secret</param>
/// <returns>The updated <see cref="IBuilder"/></returns>
public static IBuilder UseTwitterAuthentication([NotNull] this IBuilder app, [NotNull] string consumerKey, [NotNull] string consumerSecret)
{
return app.UseTwitterAuthentication(
new TwitterAuthenticationOptions
{
ConsumerKey = consumerKey,
ConsumerSecret = consumerSecret,
});
}
/// <summary>
/// Authenticate users using Twitter
/// </summary>
/// <param name="app">The <see cref="IBuilder"/> passed to the configure method</param>
/// <param name="options">Middleware configuration options</param>
/// <returns>The updated <see cref="IBuilder"/></returns>
public static IBuilder UseTwitterAuthentication([NotNull] this IBuilder app, [NotNull] TwitterAuthenticationOptions options)
{
if (string.IsNullOrEmpty(options.SignInAsAuthenticationType))
{
options.SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType();
}
return app.UseMiddleware<TwitterAuthenticationMiddleware>(options);
}
}
}

View File

@ -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<TwitterAuthenticationOptions>
{
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<bool> 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<AuthenticationTicket> 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<bool> 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<RequestToken> ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties)
{
_logger.WriteVerbose("ObtainRequestToken");
string nonce = Guid.NewGuid().ToString("N");
var authorizationParts = new SortedDictionary<string, string>
{
{ "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<AccessToken> 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<string, string>
{
{ "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<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("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.
}
}
}

View File

@ -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
{
/// <summary>
/// ASP.NET middleware for authenticating users using Twitter
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Middleware are not disposable.")]
public class TwitterAuthenticationMiddleware : AuthenticationMiddleware<TwitterAuthenticationOptions>
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
/// <summary>
/// Initializes a <see cref="TwitterAuthenticationMiddleware"/>
/// </summary>
/// <param name="next">The next middleware in the HTTP pipeline to invoke</param>
/// <param name="dataProtectionProvider"></param>
/// <param name="loggerFactory"></param>
/// <param name="options">Configuration options for the middleware</param>
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<RequestToken>(
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;
}
/// <summary>
/// Provides the <see cref="AuthenticationHandler"/> object for processing authentication-related requests.
/// </summary>
/// <returns>An <see cref="AuthenticationHandler"/> configured with the <see cref="TwitterAuthenticationOptions"/> supplied to the constructor.</returns>
protected override AuthenticationHandler<TwitterAuthenticationOptions> 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;
}
}
}

View File

@ -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
{
/// <summary>
/// Options for the Twitter authentication middleware.
/// </summary>
public class TwitterAuthenticationOptions : AuthenticationOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="TwitterAuthenticationOptions"/> class.
/// </summary>
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
}
/// <summary>
/// Gets or sets the consumer key used to communicate with Twitter.
/// </summary>
/// <value>The consumer key used to communicate with Twitter.</value>
public string ConsumerKey { get; set; }
/// <summary>
/// Gets or sets the consumer secret used to sign requests to Twitter.
/// </summary>
/// <value>The consumer secret used to sign requests to Twitter.</value>
public string ConsumerSecret { get; set; }
/// <summary>
/// Gets or sets timeout value in milliseconds for back channel communications with Twitter.
/// </summary>
/// <value>
/// The back channel timeout.
/// </value>
public TimeSpan BackchannelTimeout { get; set; }
#if NET45
/// <summary>
/// Gets or sets the a pinned certificate validator to use to validate the endpoints used
/// in back channel communications belong to Twitter.
/// </summary>
/// <value>
/// The pinned certificate validator.
/// </value>
/// <remarks>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.</remarks>
public ICertificateValidator BackchannelCertificateValidator { get; set; }
#endif
/// <summary>
/// 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.
/// </summary>
public HttpMessageHandler BackchannelHttpHandler { get; set; }
/// <summary>
/// Get or sets the text that the user can display on a sign in user interface.
/// </summary>
public string Caption
{
get { return Description.Caption; }
set { Description.Caption = value; }
}
/// <summary>
/// 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".
/// </summary>
public PathString CallbackPath { get; set; }
/// <summary>
/// Gets or sets the name of another authentication middleware which will be responsible for actually issuing a user <see cref="System.Security.Claims.ClaimsIdentity"/>.
/// </summary>
public string SignInAsAuthenticationType { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>
public ISecureDataFormat<RequestToken> StateDataFormat { get; set; }
/// <summary>
/// Gets or sets the <see cref="ITwitterAuthenticationProvider"/> used to handle authentication events.
/// </summary>
public ITwitterAuthenticationNotifications Notifications { get; set; }
}
}

View File

@ -14,8 +14,8 @@ namespace Microsoft.AspNet.Security.DataHandler.Serializer
Ticket = new TicketSerializer();
}
public static IDataSerializer<AuthenticationProperties> Properties { get; set; }
public static IDataSerializer<AuthenticationProperties> Properties { get; private set; }
public static IDataSerializer<AuthenticationTicket> Ticket { get; set; }
public static IDataSerializer<AuthenticationTicket> Ticket { get; private set; }
}
}

View File

@ -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<HttpResponseMessage>(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<HttpResponseMessage>(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<IBuilder> configure, Func<HttpContext, bool> 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<Transaction> 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<HttpResponseMessage> 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<HttpRequestMessage, Task<HttpResponseMessage>> Sender { get; set; }
protected override async Task<HttpResponseMessage> 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<string> SetCookie { get; set; }
public string ResponseText { get; set; }
}
}
}

View File

@ -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",