#765 Retrieve the email address from Twitter.

This commit is contained in:
Chris R 2016-04-28 10:18:01 -07:00
parent f38ac2afff
commit 0bce133ee4
7 changed files with 150 additions and 22 deletions

View File

@ -125,9 +125,18 @@ namespace SocialSample
{
ConsumerKey = Configuration["twitter:consumerkey"],
ConsumerSecret = Configuration["twitter:consumersecret"],
// http://stackoverflow.com/questions/22627083/can-we-get-email-id-from-twitter-oauth-api/32852370#32852370
// http://stackoverflow.com/questions/36330675/get-users-email-from-twitter-api-for-external-login-authentication-asp-net-mvc?lq=1
RetrieveUserDetails = true,
SaveTokens = true,
Events = new TwitterEvents()
{
OnCreatingTicket = ctx =>
{
var profilePic = ctx.User.Value<string>("profile_image_url");
ctx.Principal.Identities.First().AddClaim(new Claim("urn:twitter:profilepicture", profilePic, ClaimTypes.Uri, ctx.Options.ClaimsIssuer));
return Task.FromResult(0);
},
OnRemoteFailure = ctx =>
{
ctx.Response.Redirect("/error?FailureMessage=" + UrlEncoder.Default.Encode(ctx.Failure.Message));
@ -139,7 +148,7 @@ namespace SocialSample
/* Azure AD app model v2 has restrictions that prevent the use of plain HTTP for redirect URLs.
Therefore, to authenticate through microsoft accounts, tryout the sample using the following URL:
https://localhost:54541/
https://localhost:44318/
*/
// See config.json
// https://apps.dev.microsoft.com/
@ -232,6 +241,14 @@ namespace SocialSample
ClaimValueTypes.String, context.Options.ClaimsIssuer));
}
var email = user.Value<string>("email");
if (!string.IsNullOrEmpty(email))
{
context.Identity.AddClaim(new Claim(
ClaimTypes.Email, email,
ClaimValueTypes.Email, context.Options.ClaimsIssuer));
}
var link = user.Value<string>("url");
if (!string.IsNullOrEmpty(link))
{

View File

@ -1,8 +1,8 @@
{
"google:clientid": "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com",
"google:clientsecret": "n2Q-GEw9RQjzcRbU3qhfTj8f",
"twitter:consumerkey": "6XaCTaLbMqfj6ww3zvZ5g",
"twitter:consumersecret": "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI",
"twitter:consumerkey": "VvNJRyGeqYBByN694UHudI2cv",
"twitter:consumersecret": "V2xEqWgmphPdlUXX4ARWsozl9lfbvr5wbAYw2LN8m6kZV7pt20",
"github:clientid": "49e302895d8b09ea5656",
"github:clientsecret": "98f1bf028608901e9df91d64ee61536fe562064b",
"github-token:clientid": "8c0c5a572abe8fe89588",

View File

@ -5,6 +5,7 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Authentication;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNetCore.Authentication.Twitter
{
@ -22,40 +23,49 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
/// <param name="screenName">Twitter screen name</param>
/// <param name="accessToken">Twitter access token</param>
/// <param name="accessTokenSecret">Twitter access token secret</param>
/// <param name="user">User details</param>
public TwitterCreatingTicketContext(
HttpContext context,
TwitterOptions options,
string userId,
string screenName,
string accessToken,
string accessTokenSecret)
string accessTokenSecret,
JObject user)
: base(context, options)
{
UserId = userId;
ScreenName = screenName;
AccessToken = accessToken;
AccessTokenSecret = accessTokenSecret;
User = user ?? new JObject();
}
/// <summary>
/// Gets the Twitter user ID
/// </summary>
public string UserId { get; private set; }
public string UserId { get; }
/// <summary>
/// Gets the Twitter screen name
/// </summary>
public string ScreenName { get; private set; }
public string ScreenName { get; }
/// <summary>
/// Gets the Twitter access token
/// </summary>
public string AccessToken { get; private set; }
public string AccessToken { get; }
/// <summary>
/// Gets the Twitter access token secret
/// </summary>
public string AccessTokenSecret { get; private set; }
public string AccessTokenSecret { get; }
/// <summary>
/// Gets the JSON-serialized user or an empty
/// <see cref="JObject"/> if it is not available.
/// </summary>
public JObject User { get; }
/// <summary>
/// Gets the <see cref="ClaimsPrincipal"/> representing the user

View File

@ -9,6 +9,7 @@ namespace Microsoft.Extensions.Logging
{
private static Action<ILogger, Exception> _obtainRequestToken;
private static Action<ILogger, Exception> _obtainAccessToken;
private static Action<ILogger, Exception> _retrieveUserDetails;
static LoggingExtensions()
{
@ -20,6 +21,10 @@ namespace Microsoft.Extensions.Logging
eventId: 2,
logLevel: LogLevel.Debug,
formatString: "ObtainAccessToken");
_retrieveUserDetails = LoggerMessage.Define(
eventId: 3,
logLevel: LogLevel.Debug,
formatString: "RetrieveUserDetails");
}
@ -32,5 +37,10 @@ namespace Microsoft.Extensions.Logging
{
_obtainRequestToken(logger, null);
}
public static void RetrieveUserDetails(this ILogger logger)
{
_retrieveUserDetails(logger, null);
}
}
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json.Linq;
namespace Microsoft.AspNetCore.Authentication.Twitter
{
@ -76,7 +77,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
Response.Cookies.Delete(StateCookie, cookieOptions);
var accessToken = await ObtainAccessTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, requestToken, oauthVerifier);
var accessToken = await ObtainAccessTokenAsync(requestToken, oauthVerifier);
var identity = new ClaimsIdentity(new[]
{
@ -87,6 +88,12 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
},
Options.ClaimsIssuer);
JObject user = null;
if (Options.RetrieveUserDetails)
{
user = await RetrieveUserDetailsAsync(accessToken, identity);
}
if (Options.SaveTokens)
{
properties.StoreTokens(new [] {
@ -95,12 +102,13 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
});
}
return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, accessToken));
return AuthenticateResult.Success(await CreateTicketAsync(identity, properties, accessToken, user));
}
protected virtual async Task<AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token)
protected virtual async Task<AuthenticationTicket> CreateTicketAsync(
ClaimsIdentity identity, AuthenticationProperties properties, AccessToken token, JObject user)
{
var context = new TwitterCreatingTicketContext(Context, Options, token.UserId, token.ScreenName, token.Token, token.TokenSecret)
var context = new TwitterCreatingTicketContext(Context, Options, token.UserId, token.ScreenName, token.Token, token.TokenSecret, user)
{
Principal = new ClaimsPrincipal(identity),
Properties = properties
@ -134,7 +142,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
}
// If CallbackConfirmed is false, this will throw
var requestToken = await ObtainRequestTokenAsync(Options.ConsumerKey, Options.ConsumerSecret, BuildRedirectUri(Options.CallbackPath), properties);
var requestToken = await ObtainRequestTokenAsync(BuildRedirectUri(Options.CallbackPath), properties);
var twitterAuthenticationEndpoint = AuthenticationEndpoint + requestToken.Token;
var cookieOptions = new CookieOptions
@ -152,7 +160,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
return true;
}
private async Task<RequestToken> ObtainRequestTokenAsync(string consumerKey, string consumerSecret, string callBackUri, AuthenticationProperties properties)
private async Task<RequestToken> ObtainRequestTokenAsync(string callBackUri, AuthenticationProperties properties)
{
Logger.ObtainRequestToken();
@ -161,7 +169,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
var authorizationParts = new SortedDictionary<string, string>
{
{ "oauth_callback", callBackUri },
{ "oauth_consumer_key", consumerKey },
{ "oauth_consumer_key", Options.ConsumerKey },
{ "oauth_nonce", nonce },
{ "oauth_signature_method", "HMAC-SHA1" },
{ "oauth_timestamp", GenerateTimeStamp() },
@ -183,7 +191,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
canonicalizedRequestBuilder.Append("&");
canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString));
var signature = ComputeSignature(consumerSecret, null, canonicalizedRequestBuilder.ToString());
var signature = ComputeSignature(Options.ConsumerSecret, null, canonicalizedRequestBuilder.ToString());
authorizationParts.Add("oauth_signature", signature);
var authorizationHeaderBuilder = new StringBuilder();
@ -200,7 +208,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
var response = await _httpClient.SendAsync(request, Context.RequestAborted);
response.EnsureSuccessStatusCode();
string responseText = await response.Content.ReadAsStringAsync();
var responseText = await response.Content.ReadAsStringAsync();
var responseParameters = new FormCollection(new FormReader(responseText).ReadForm());
if (!string.Equals(responseParameters["oauth_callback_confirmed"], "true", StringComparison.Ordinal))
@ -211,7 +219,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
return new RequestToken { Token = Uri.UnescapeDataString(responseParameters["oauth_token"]), TokenSecret = Uri.UnescapeDataString(responseParameters["oauth_token_secret"]), CallbackConfirmed = true, Properties = properties };
}
private async Task<AccessToken> ObtainAccessTokenAsync(string consumerKey, string consumerSecret, RequestToken token, string verifier)
private async Task<AccessToken> ObtainAccessTokenAsync(RequestToken token, string verifier)
{
// https://dev.twitter.com/docs/api/1/post/oauth/access_token
@ -221,7 +229,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
var authorizationParts = new SortedDictionary<string, string>
{
{ "oauth_consumer_key", consumerKey },
{ "oauth_consumer_key", Options.ConsumerKey },
{ "oauth_nonce", nonce },
{ "oauth_signature_method", "HMAC-SHA1" },
{ "oauth_token", token.Token },
@ -245,7 +253,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
canonicalizedRequestBuilder.Append("&");
canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString));
var signature = ComputeSignature(consumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString());
var signature = ComputeSignature(Options.ConsumerSecret, token.TokenSecret, canonicalizedRequestBuilder.ToString());
authorizationParts.Add("oauth_signature", signature);
authorizationParts.Remove("oauth_verifier");
@ -288,6 +296,76 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
};
}
// https://dev.twitter.com/rest/reference/get/account/verify_credentials
private async Task<JObject> RetrieveUserDetailsAsync(AccessToken accessToken, ClaimsIdentity identity)
{
Logger.RetrieveUserDetails();
var nonce = Guid.NewGuid().ToString("N");
var authorizationParts = new SortedDictionary<string, string>
{
{ "oauth_consumer_key", Options.ConsumerKey },
{ "oauth_nonce", nonce },
{ "oauth_signature_method", "HMAC-SHA1" },
{ "oauth_timestamp", GenerateTimeStamp() },
{ "oauth_token", accessToken.Token },
{ "oauth_version", "1.0" }
};
var parameterBuilder = new StringBuilder();
foreach (var authorizationKey in authorizationParts)
{
parameterBuilder.AppendFormat("{0}={1}&", UrlEncoder.Encode(authorizationKey.Key), UrlEncoder.Encode(authorizationKey.Value));
}
parameterBuilder.Length--;
var parameterString = parameterBuilder.ToString();
var resource_url = "https://api.twitter.com/1.1/account/verify_credentials.json";
var resource_query = "include_email=true";
var canonicalizedRequestBuilder = new StringBuilder();
canonicalizedRequestBuilder.Append(HttpMethod.Get.Method);
canonicalizedRequestBuilder.Append("&");
canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_url));
canonicalizedRequestBuilder.Append("&");
canonicalizedRequestBuilder.Append(UrlEncoder.Encode(resource_query));
canonicalizedRequestBuilder.Append("%26");
canonicalizedRequestBuilder.Append(UrlEncoder.Encode(parameterString));
var signature = ComputeSignature(Options.ConsumerSecret, accessToken.TokenSecret, 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.Encode(authorizationPart.Value));
}
authorizationHeaderBuilder.Length = authorizationHeaderBuilder.Length - 2;
var request = new HttpRequestMessage(HttpMethod.Get, resource_url + "?include_email=true");
request.Headers.Add("Authorization", authorizationHeaderBuilder.ToString());
var response = await _httpClient.SendAsync(request, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Logger.LogError("Email request failed with a status code of " + response.StatusCode);
response.EnsureSuccessStatusCode(); // throw
}
var responseText = await response.Content.ReadAsStringAsync();
var result = JObject.Parse(responseText);
var email = result.Value<string>("email");
if (!string.IsNullOrEmpty(email))
{
identity.AddClaim(new Claim(ClaimTypes.Email, email, ClaimValueTypes.Email, Options.ClaimsIssuer));
}
return result;
}
private static string GenerateTimeStamp()
{
var secondsSinceUnixEpocStart = DateTime.UtcNow - Epoch;

View File

@ -38,6 +38,14 @@ namespace Microsoft.AspNetCore.Builder
/// <value>The consumer secret used to sign requests to Twitter.</value>
public string ConsumerSecret { get; set; }
/// <summary>
/// Enables the retrieval user details during the authentication process, including
/// e-mail addresses. Retrieving e-mail addresses requires special permissions
/// from Twitter Support on a per application basis. The default is false.
/// See https://dev.twitter.com/rest/reference/get/account/verify_credentials
/// </summary>
public bool RetrieveUserDetails { get; set; }
/// <summary>
/// Gets or sets the type used to secure data handled by the middleware.
/// </summary>

View File

@ -19,10 +19,15 @@
"xmlDoc": true
},
"dependencies": {
"Microsoft.AspNetCore.Authentication": "1.0.0-*"
"Microsoft.AspNetCore.Authentication": "1.0.0-*",
"Newtonsoft.Json": "8.0.3"
},
"frameworks": {
"net451": {},
"netstandard1.3": {}
"netstandard1.3": {
"imports": [
"portable-net451+win8"
]
}
}
}