#38 - Port the Twitter middleware from katana
This commit is contained in:
parent
5577159453
commit
b9eb7ba282
13
Security.sln
13
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
|
||||
|
|
|
|||
|
|
@ -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-*"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 '{0}' 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue