Intermittent ASP.NET oAuth issue with Google, Auth

2019-02-04 04:42发布

问题:

I am trying to fix an intermittent issue when using Google as an external login provider.

When attempting to login, the user is redirected back to the login page rather than being authenticated.

The problem occurs on this line (line 55 of link below), GetExternalIdentityAsync returns null.

var externalIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

The full code is:

[Authorize]
public abstract class GoogleAccountController<TUser> : Controller where TUser : Microsoft.AspNet.Identity.IUser
{
    public IAuthenticationManager AuthenticationManager
    {
        get
        {
            return HttpContext.GetOwinContext().Authentication;
        }
    }

    public abstract UserManager<TUser> UserManager { get; set; }

    [AllowAnonymous]
    [HttpGet]
    [Route("login")]
    public ActionResult Login(string returnUrl)
    {
        ViewData.Model = new LoginModel()
        {
            Message = TempData["message"] as string,
            Providers = HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes(),
            ReturnUrl = returnUrl
        };

        return View();
    }

    [AllowAnonymous]
    [HttpPost]
    [ValidateAntiForgeryToken]
    [Route("login")]
    public ActionResult Login(string provider, string returnUrl)
    {
        return new ChallengeResult(provider, Url.Action("Callback", "Account", new { ReturnUrl = returnUrl }));
    }

    [AllowAnonymous]
    [Route("authenticate")]
    public async Task<ActionResult> Callback(string returnUrl)
    {
        var externalIdentity = await AuthenticationManager.GetExternalIdentityAsync(DefaultAuthenticationTypes.ExternalCookie);

        if (externalIdentity == null)
        {
            return RedirectToAction("Login", new { ReturnUrl = returnUrl });
        }

        var emailAddress = externalIdentity.FindFirstValue(ClaimTypes.Email);
        var user = await UserManager.FindByNameAsync(emailAddress);

        if (user != null)
        {
            await SignInAsync(user, false);

            return RedirectToLocal(returnUrl);
        }
        else
        {
            TempData.Add("message", string.Format("The account {0} is not approved.", emailAddress));

            return RedirectToAction("Login", new { ReturnUrl = returnUrl });
        }
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    [Route("logout")]
    public ActionResult Logout(string returnUrl)
    {
        AuthenticationManager.SignOut();

        return RedirectToLocal(returnUrl);
    }

    private async Task SignInAsync(TUser user, bool isPersistent)
    {
        AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

        var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
        var authenticationProperties = new AuthenticationProperties()
        {
            IsPersistent = isPersistent
        };

        AuthenticationManager.SignIn(authenticationProperties, identity);
    }

    private ActionResult RedirectToLocal(string returnUrl)
    {
        if (Url.IsLocalUrl(returnUrl))
        {
            return Redirect(returnUrl);
        }
        else
        {
            return RedirectToAction("Index", "Home");
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && UserManager != null)
        {
            UserManager.Dispose();
            UserManager = null;
        }

        base.Dispose(disposing);
    }
}

Which is also here.

This is very much an intermittent problem, and redeploying the app will often get it to work temporarily.

Looking in Fiddler I can see a call is made to sign-google just previous to the authenticate method in which it can't find the cookie.

The app uses the following code to initialize the google login

app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
        LoginPath = new PathString("/login")
    });
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
app.UseGoogleAuthentication();

I have set the authentication mode to non in the web.config, and removed the forms authentication module.

<system.web>
    <authentication mode="None" />
</system.web>    
<system.webServer>
    <validation validateIntegratedModeConfiguration="false" />    
    <modules runAllManagedModulesForAllRequests="true">
      <remove name="FormsAuthenticationModule" />
    </modules>
</system.webServer>

The sites are hosted on Azure, some running on 1 instance, some 2. They have custom domains, although still fail on both custom domain and azurewebsites domain, and http / https.

Can anyone help with why this might be happening?

Update

Version 3.0 of Microsoft.Owin.Security.Google was released last night. Going to switch over and see if this fixes the issue.

https://www.nuget.org/packages/Microsoft.Owin.Security.Google

回答1:

I forgot to enable "Google + API" in the google developer console. Google login appears to be fine, but GetExternalLoginInfoAsync returns null.

You can follow this link https://stackoverflow.com/a/27631109/657926



回答2:

Tom I am using google-oauth in my asp.net application by using REST API. it is working fine and i am not facing any connection issues.

The following steps i am performing:

1.I have created one project in google developer console in that i have created settings "Client ID for web application" which will contains the following parameters.

a)Client ID => It will be automatically generated by google b)Email address=> It will be automatically generated by google c)Client secret=> It will be automatically generated by google d)Redirect URIs => Need to specify url of web page which will be used to handle authentication process. In this page we can authenticate and we can get user's basic information.

my url: "http://localhost:1822/WebForm1.aspx/code"

My Usage:

  1. I have created one sample project which will contains "Webpage1.aspx" and "Webpage2.aspx".

I have set "Webpage2.aspx" startup page and I am forming open auth url in the "Webpage2.aspx" and redirecting to google login page.

After login, it will redirect to "Webpage1.aspx" along with access code. By passing this access code to "https://accounts.google.com/o/oauth2/token" url, i am getting access token along with token type and token expiry time. After that by passing this access to the "https://www.googleapis.com/oauth2/v2/userinfo" url, i am getting user basic information like "email,Name, Gender, Photo, etc...)

Example Code

    public class GoogleAuthorizationData
    {
        public string access_token { get; set; }
        public int expires_in { get; set; }
        public string token_type { get; set; }

    }

  public class GoogleUserInfo
    {
        public string name { get; set; }
        public string family_name { get; set; }
        public string gender { get; set; }
        public string email { get; set; }
        public string given_name { get; set; }
        public string picture { get; set; }
        public string link { get; set; }
        public string id { get; set; }

    }

  Webpage1.aspx
  ============
 protected void Page_Load(object sender, EventArgs e)
        {
            string code = Request.QueryString["code"].ToString();
            string scope = Request.QueryString["scope"].ToString();
            string url = "https://accounts.google.com/o/oauth2/token";
            string postString = "code=" + code + "&client_id=" + ConfigurationManager.AppSettings["GoogleClientID"].ToString() + "&client_secret=" + ConfigurationManager.AppSettings["GoogleSecretKey"].ToString() + "&redirect_uri=" + ConfigurationManager.AppSettings["ResponseUrl"].ToString() + "&grant_type=authorization_code";

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url.ToString());
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";

            UTF8Encoding utfenc = new UTF8Encoding();
            byte[] bytes = utfenc.GetBytes(postString);
            Stream os = null;
            try
            {
                request.ContentLength = bytes.Length;
                os = request.GetRequestStream();
                os.Write(bytes, 0, bytes.Length);
            }
            catch
            { }

            try
            {
                HttpWebResponse webResponse = (HttpWebResponse)request.GetResponse();
                Stream responseStream = webResponse.GetResponseStream();
                StreamReader responseStreamReader = new StreamReader(responseStream);
                var result = responseStreamReader.ReadToEnd();//
                var json = new JavaScriptSerializer();

                GoogleAuthorizationData authData = json.Deserialize<GoogleAuthorizationData>(result);

                HttpWebRequest request1 = (HttpWebRequest)WebRequest.Create("https://www.googleapis.com/oauth2/v2/userinfo");
                request1.Method = "GET";
                request1.ContentLength = 0;
                request1.Headers.Add("Authorization", string.Format("{0} {1}", authData.token_type, authData.access_token));
                HttpWebResponse webResponse1 = (HttpWebResponse)request1.GetResponse();
                Stream responseStream1 = webResponse1.GetResponseStream();
                StreamReader responseStreamReader1 = new StreamReader(responseStream1);
                GoogleUserInfo userinfo = json.Deserialize<GoogleUserInfo>(responseStreamReader1.ReadToEnd());
               Response.Write(userinfo.email);

            }
            catch (Exception eX)
            {
                throw eX;
            }





        }


回答3:

I believe you should't be using app.UseGoogleAuthentication(); as that's a call which will try to use OpenID 2.0, which has been deprecated.
What you should be using instead is OAuth 2.0 for Login (OpenID Connect).
So:

  1. register your app in Google Developers Console
  2. enable it to access Google+ API (even though you do not intend to use Google+ directly - it's now used as a mean of authentication)
  3. enable ASP.NET Identity's Google authentication this way
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()  
{  
    ClientId = "YOUR_CLIENT_ID",  
    ClientSecret = "YOUR_CLIENT_SECRET",  
});


回答4:

I have the same problem. I am using Visual Studio 2013 and website is on Azure. The social log in that had been working without issue stopping working and LinkLoginCallback was receiving null in loginInfo. I republished the project without code change or rebuilding and then loginInfo received correct data and all works fine. Does not make sense but there you go.



回答5:

I got this problem yesterday and I was COMPLETELY STUCK ! And, as you described, I got it just like that with no reason.

I managed to fix it (after hours of comparing 2 projects - 1 test project that worked no problem every time and another that was a more serious project - and they had the exact same code, but different dll versions)

The problem was with the DLLs - the referenced packages from Nuget. Make sure you have the latest packages and also check the runtime section in your web.config.

After I updated all Owin related packages and Microsoft.Owin and added:

<assemblyBinding>
  <dependentAssembly>
    <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
  </dependentAssembly>
  <dependentAssembly>
    <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
  </dependentAssembly>
  <dependentAssembly>
    <assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
  </dependentAssembly>
  <dependentAssembly>
    <assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
    <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
  </dependentAssembly>
</assemblyBinding>

... it worked again ! They might vary based on your used packages, but that's how it worked for me.



回答6:

There is a bug in Microsoft's Owin implementation for System.Web. The one that is being used when running Owin applications on IIS. Which is what probably 99% of us do, if we're using the new Owin-based authentication handling with ASP.NET MVC5.

The bug makes cookies set by Owin mysteriously disappear on some occasions.

Put this nuget before https://github.com/KentorIT/owin-cookie-saver before app.UseGoogleAuthentication(...)



回答7:

Make sure that 3rd party cookies are enabled. I have found out that if you're not logged into google when you try to 'Register' a user with your application, it redirects to the login page as it looks for this cookie that isn't there, but still manages to do what it needs with the external provider. The next time you try to 'Register', because it's done part of the process it doesn't need to look for the external cookie anymore and so succeeds second time around.