// 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.Collections; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.Authentication.Twitter.Messages; using Microsoft.AspNet.WebUtilities; using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Authentication.Twitter { internal class TwitterAuthenticationHandler : AuthenticationHandler { private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private const string StateCookie = "__TwitterState"; private const string RequestTokenEndpoint = "https://api.twitter.com/oauth/request_token"; private const string AuthenticationEndpoint = "https://twitter.com/oauth/authenticate?oauth_token="; private const string AccessTokenEndpoint = "https://api.twitter.com/oauth/access_token"; private readonly HttpClient _httpClient; public TwitterAuthenticationHandler(HttpClient httpClient) { _httpClient = httpClient; } public override async Task InvokeAsync() { if (Options.CallbackPath.HasValue && Options.CallbackPath == Request.Path) { return await InvokeReturnPathAsync(); } return false; } protected override AuthenticationTicket AuthenticateCore() { return AuthenticateCoreAsync().GetAwaiter().GetResult(); } protected override async Task AuthenticateCoreAsync() { AuthenticationProperties properties = null; try { var query = Request.Query; var protectedRequestToken = Request.Cookies[StateCookie]; var requestToken = Options.StateDataFormat.Unprotect(protectedRequestToken); if (requestToken == null) { Logger.LogWarning("Invalid state"); return null; } properties = requestToken.Properties; var returnedToken = query.Get("oauth_token"); if (string.IsNullOrWhiteSpace(returnedToken)) { Logger.LogWarning("Missing oauth_token"); return new AuthenticationTicket(properties, Options.AuthenticationScheme); } if (returnedToken != requestToken.Token) { Logger.LogWarning("Unmatched token"); return new AuthenticationTicket(properties, Options.AuthenticationScheme); } string oauthVerifier = query.Get("oauth_verifier"); if (string.IsNullOrWhiteSpace(oauthVerifier)) { Logger.LogWarning("Missing or blank oauth_verifier"); return new AuthenticationTicket(properties, Options.AuthenticationScheme); } var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier); var context = new TwitterAuthenticatedContext(Context, accessToken.UserId, accessToken.ScreenName, accessToken.Token, accessToken.TokenSecret); context.Principal = new ClaimsPrincipal( new ClaimsIdentity( new[] { new Claim(ClaimTypes.NameIdentifier, accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationScheme), new Claim(ClaimTypes.Name, accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationScheme), new Claim("urn:twitter:userid", accessToken.UserId, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationScheme), new Claim("urn:twitter:screenname", accessToken.ScreenName, "http://www.w3.org/2001/XMLSchema#string", Options.AuthenticationScheme) }, Options.AuthenticationScheme, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType)); context.Properties = requestToken.Properties; var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }; Response.Cookies.Delete(StateCookie, cookieOptions); await Options.Notifications.Authenticated(context); return new AuthenticationTicket(context.Principal, context.Properties, Options.AuthenticationScheme); } catch (Exception ex) { Logger.LogError("Authentication failed", ex); return new AuthenticationTicket(properties, Options.AuthenticationScheme); } } protected override void ApplyResponseChallenge() { ApplyResponseChallengeAsync().GetAwaiter().GetResult(); } protected override async Task ApplyResponseChallengeAsync() { if (ShouldConvertChallengeToForbidden()) { Response.StatusCode = 403; return; } if (Response.StatusCode != 401) { return; } // When Automatic should redirect on 401 even if there wasn't an explicit challenge. if (ChallengeContext == null && !Options.AutomaticAuthentication) { return; } var requestPrefix = Request.Scheme + "://" + Request.Host; var 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; } var requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, callBackUrl, properties); if (requestToken.CallbackConfirmed) { var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token; var cookieOptions = new CookieOptions { HttpOnly = true, Secure = Request.IsHttps }; Response.Cookies.Append(StateCookie, Options.StateDataFormat.Protect(requestToken), cookieOptions); var redirectContext = new TwitterApplyRedirectContext( Context, Options, properties, twitterAuthenticationEndpoint); Options.Notifications.ApplyRedirect(redirectContext); } else { Logger.LogError("requestToken CallbackConfirmed!=true"); } } public async Task InvokeReturnPathAsync() { var model = await AuthenticateAsync(); if (model == null) { Logger.LogWarning("Invalid return state, unable to redirect."); Response.StatusCode = 500; return true; } var context = new TwitterReturnEndpointContext(Context, model) { SignInScheme = Options.SignInScheme, RedirectUri = model.Properties.RedirectUri }; model.Properties.RedirectUri = null; await Options.Notifications.ReturnEndpoint(context); if (context.SignInScheme != null && context.Principal != null) { Context.Authentication.SignIn(context.SignInScheme, context.Principal, context.Properties); } if (!context.IsRequestCompleted && context.RedirectUri != null) { if (context.Principal == null) { // add a redirect hint that sign-in failed in some way context.RedirectUri = QueryHelpers.AddQueryString(context.RedirectUri, "error", "access_denied"); } Response.Redirect(context.RedirectUri); context.RequestCompleted(); } return context.IsRequestCompleted; } private async Task ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties) { Logger.LogVerbose("ObtainRequestToken"); var nonce = Guid.NewGuid().ToString("N"); var authorizationParts = new SortedDictionary { { "oauth_callback", callBackUri }, { "oauth_consumer_key", consumerKey }, { "oauth_nonce", nonce }, { "oauth_signature_method", "HMAC-SHA1" }, { "oauth_timestamp", GenerateTimeStamp() }, { "oauth_version", "1.0" } }; var parameterBuilder = new StringBuilder(); foreach (var authorizationKey in authorizationParts) { parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.UrlEncode(authorizationKey.Key), UrlEncoder.UrlEncode(authorizationKey.Value)); } parameterBuilder.Length--; var parameterString = parameterBuilder.ToString(); var canonicalizedRequestBuilder = new StringBuilder(); canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); canonicalizedRequestBuilder.Append("&"); canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(RequestTokenEndpoint)); canonicalizedRequestBuilder.Append("&"); canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(parameterString)); var 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, UrlEncoder.UrlEncode(authorizationPart.Value)); } authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; var request = new HttpRequestMessage(HttpMethod.Post, RequestTokenEndpoint); request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); var response = await _httpClient.SendAsync(request, Context.RequestAborted); response.EnsureSuccessStatusCode(); string responseText = await response.Content.ReadAsStringAsync(); var responseParameters = new FormCollection(FormReader.ReadForm(responseText)); if (string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal)) { return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties }; } return new RequestToken(); } private async Task ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier) { // https://dev.twitter.com/docs/api/1/post/oauth/access_token Logger.LogVerbose("ObtainAccessToken"); var nonce = Guid.NewGuid().ToString("N"); var authorizationParts = new SortedDictionary { { "oauth_consumer_key", consumerKey }, { "oauth_nonce", nonce }, { "oauth_signature_method", "HMAC-SHA1" }, { "oauth_token", token.Token }, { "oauth_timestamp", GenerateTimeStamp() }, { "oauth_verifier", verifier }, { "oauth_version", "1.0" }, }; var parameterBuilder = new StringBuilder(); foreach (var authorizationKey in authorizationParts) { parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.UrlEncode(authorizationKey.Key), UrlEncoder.UrlEncode(authorizationKey.Value)); } parameterBuilder.Length--; var parameterString = parameterBuilder.ToString(); var canonicalizedRequestBuilder = new StringBuilder(); canonicalizedRequestBuilder.Append(HttpMethod.Post.Method); canonicalizedRequestBuilder.Append("&"); canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(AccessTokenEndpoint)); canonicalizedRequestBuilder.Append("&"); canonicalizedRequestBuilder.Append(UrlEncoder.UrlEncode(parameterString)); var 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, UrlEncoder.UrlEncode(authorizationPart.Value)); } authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2; var request = new HttpRequestMessage(HttpMethod.Post, AccessTokenEndpoint); request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString()); var formPairs = new Dictionary() { { "oauth_verifier", verifier }, }; request.Content = new FormUrlEncodedContent(formPairs); var response = await _httpClient.SendAsync(request, Context.RequestAborted); if (!response.IsSuccessStatusCode) { Logger.LogError("AccessToken request failed with a status code of " + response.StatusCode); response.EnsureSuccessStatusCode(); // throw } var responseText = await response.Content.ReadAsStringAsync(); var responseParameters = new FormCollection(FormReader.ReadForm(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() { var secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch; return Convert.ToInt64(secondsSinceUnixEpocStart.TotalSeconds).ToString(CultureInfo.InvariantCulture); } private string ComputeSignature(string consumerSecret, string tokenSecret, string signatureData) { using (var algorithm = new HMACSHA1()) { algorithm.Key = Encoding.ASCII.GetBytes( string.Format(CultureInfo.InvariantCulture, "{0}&{1}", UrlEncoder.UrlEncode(consumerSecret), string.IsNullOrEmpty(tokenSecret) ? string.Empty : UrlEncoder.UrlEncode(tokenSecret))); var hash = algorithm.ComputeHash(Encoding.ASCII.GetBytes(signatureData)); return Convert.ToBase64String(hash); } } protected override void ApplyResponseGrant() { // N/A - No SignIn or SignOut support. } } }