Get user's email from Twitter API for External Login Authentication ASP.NET MVC C#

After almost going bald from pulling all my hairs out of my head, I finally got it to work. I found out that the Signature base string was slightly different from the one generated with my code. After little tweaks, I was able to generate a valid signature base string.

In Startup.cs, I added access_token and access_secret as claims. I did not use the one found on my app because the users need to invoke a new one as they attempt to login or register:

var twitterOptions = new Microsoft.Owin.Security.Twitter.TwitterAuthenticationOptions()
{
   ConsumerKey = ConfigurationManager.AppSettings["consumer_key"],
   ConsumerSecret = ConfigurationManager.AppSettings["consumer_secret"],
   Provider = new Microsoft.Owin.Security.Twitter.TwitterAuthenticationProvider
   {
      OnAuthenticated = (context) =>
      {
         context.Identity.AddClaim(new System.Security.Claims.Claim("urn:twitter:access_token", context.AccessToken));
         context.Identity.AddClaim(new System.Security.Claims.Claim("urn:twitter:access_secret", context.AccessTokenSecret));
         return Task.FromResult(0);
      }
   },
   BackchannelCertificateValidator = new Microsoft.Owin.Security.CertificateSubjectKeyIdentifierValidator(new[]
   {
      "A5EF0B11CEC04103A34A659048B21CE0572D7D47", // VeriSign Class 3 Secure Server CA - G2
      "0D445C165344C1827E1D20AB25F40163D8BE79A5", // VeriSign Class 3 Secure Server CA - G3
      "7FD365A7C2DDECBBF03009F34339FA02AF333133", // VeriSign Class 3 Public Primary Certification Authority - G5
      "39A55D933676616E73A761DFA16A7E59CDE66FAD", // Symantec Class 3 Secure Server CA - G4
      "‎add53f6680fe66e383cbac3e60922e3b4c412bed", // Symantec Class 3 EV SSL CA - G3
      "4eb6d578499b1ccf5f581ead56be3d9b6744a5e5", // VeriSign Class 3 Primary CA - G5
      "5168FF90AF0207753CCCD9656462A212B859723B", // DigiCert SHA2 High Assurance Server C‎A 
      "B13EC36903F8BF4701D498261A0802EF63642BC3" // DigiCert High Assurance EV Root CA
    }),
    CallbackPath = new PathString("/twitter/account/ExternalLoginCallback")
};

 app.UseTwitterAuthentication(twitterOptions);

And finally in my controller, I just called my helper class to get the name and email from twitter:

    if (loginInfo.Login.LoginProvider.ToLower() == "twitter")
    {
        string access_token = loginInfo.ExternalIdentity.Claims.Where(x => x.Type == "urn:twitter:access_token").Select(x => x.Value).FirstOrDefault();
        string access_secret = loginInfo.ExternalIdentity.Claims.Where(x => x.Type == "urn:twitter:access_secret").Select(x => x.Value).FirstOrDefault();
        TwitterDto response = MyHelper.TwitterLogin(access_token, access_secret, ConfigurationManager.AppSettings["consumer_key"], ConfigurationManager.AppSettings["consumer_secret"]);
       // by now response.email should possess the email value you need
    }

Helper class method:

This was the section I tweaked in order to make a valid request:

baseString = string.Concat("GET&", Uri.EscapeDataString(resource_url) + "&" + Uri.EscapeDataString(request_query), "%26", Uri.EscapeDataString(baseString));

public static TwitterDto TwitterLogin(string oauth_token, string oauth_token_secret, string oauth_consumer_key, string oauth_consumer_secret)
        {
            // oauth implementation details
            var oauth_version = "1.0";
            var oauth_signature_method = "HMAC-SHA1";

            // unique request details
            var oauth_nonce = Convert.ToBase64String(
                new ASCIIEncoding().GetBytes(DateTime.Now.Ticks.ToString()));
            var timeSpan = DateTime.UtcNow
                - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            var oauth_timestamp = Convert.ToInt64(timeSpan.TotalSeconds).ToString();

            var resource_url = "https://api.twitter.com/1.1/account/verify_credentials.json";
            var request_query = "include_email=true";
            // create oauth signature
            var baseFormat = "oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method={2}" +
                            "&oauth_timestamp={3}&oauth_token={4}&oauth_version={5}";

            var baseString = string.Format(baseFormat,
                                        oauth_consumer_key,
                                        oauth_nonce,
                                        oauth_signature_method,
                                        oauth_timestamp,
                                        oauth_token,
                                        oauth_version
                                        );

            baseString = string.Concat("GET&", Uri.EscapeDataString(resource_url) + "&" + Uri.EscapeDataString(request_query), "%26", Uri.EscapeDataString(baseString));

            var compositeKey = string.Concat(Uri.EscapeDataString(oauth_consumer_secret),
                                    "&", Uri.EscapeDataString(oauth_token_secret));

            string oauth_signature;
            using (HMACSHA1 hasher = new HMACSHA1(ASCIIEncoding.ASCII.GetBytes(compositeKey)))
            {
                oauth_signature = Convert.ToBase64String(
                    hasher.ComputeHash(ASCIIEncoding.ASCII.GetBytes(baseString)));
            }

            // create the request header
            var headerFormat = "OAuth oauth_consumer_key=\"{0}\", oauth_nonce=\"{1}\", oauth_signature=\"{2}\", oauth_signature_method=\"{3}\", oauth_timestamp=\"{4}\", oauth_token=\"{5}\", oauth_version=\"{6}\"";

            var authHeader = string.Format(headerFormat,
                                    Uri.EscapeDataString(oauth_consumer_key),
                                    Uri.EscapeDataString(oauth_nonce),
                                    Uri.EscapeDataString(oauth_signature),
                                    Uri.EscapeDataString(oauth_signature_method),
                                    Uri.EscapeDataString(oauth_timestamp),
                                    Uri.EscapeDataString(oauth_token),
                                    Uri.EscapeDataString(oauth_version)
                            );


            // make the request

            ServicePointManager.Expect100Continue = false;
            resource_url += "?include_email=true";
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(resource_url);
            request.Headers.Add("Authorization", authHeader);
            request.Method = "GET";

            WebResponse response = request.GetResponse();
            return JsonConvert.DeserializeObject<TwitterDto>(new StreamReader(response.GetResponseStream()).ReadToEnd());
        }
    }

    public class TwitterDto
    {
        public string name { get; set; }
        public string email { get; set; }
    }

This is all you need to get the twitter user's email. I hope it helps someone struggling with this. Please note that the steps mentioned in the question is also very important.

Updates version .netcore 3.1

It is very simple to implement the twitter API in .netcore compared to the solution above. First, you need to create an app on Twitter Create Twitter App

Supply all necessary information such as app name, description, websiteURL (https://example.com will do for local development), and so on. For your callback url, provide the local url you are using. In my case, https://localhost:44318/signin-twitter and ensure that you tick "request email address from users" save and then regenerate the consumer API Keys under the "Keys and tokens" tab see image below: enter image description here

After you are done with the Twitter administration, Install the nuget package in your solution in Visual Studio:

Install-Package Microsoft.AspNetCore.Authentication.Twitter

Update your .NetCore Application Startup Class (ConfigureServices method) in visual studio to initialize the Twitter Authentication mechanism with the code below:

services.AddAuthentication().AddTwitter(options => 
        {
            options.ConsumerKey = twitterConsumerApiKey;
            options.ConsumerSecret = twitterConsumerSecretKey;
            options.RetrieveUserDetails = true;
        }); // twitterConsumerApiKey and twitterConsumerSecretkey can be found under the "Keys and tokens" tab of the Twitter App previously created. 

The process is complete and you should be able to get emails of users upon authentication. For more information check out Twitter external sign-in setup with ASP.NET Core