#765 Retrieve the email address from Twitter.
This commit is contained in:
parent
f38ac2afff
commit
0bce133ee4
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue