Google OAuth on MVC5 ExternalLoginCallback?error=access_denied - oauth

I have set up my Google OAuth
And I have added the code into Startup.Auth.cs
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
// LRC
ClientId = "xxxxxxxxx",
ClientSecret = "xxxxx"
//CallbackPath = new PathString("/signin-google")
});
But after I chose a google account to log in, it redirected me to the login page again,
I checked the network via Chrome and found that the access was denied.
http://www.liferunningclub.com.au/Account/ExternalLoginCallback?error=access_denied
I cannot figure it out.
Update
Now I did something else:
I added an annotation ([RequireHttps]) on the Account Controller
I enabled the SSL for my project.
I updated the url and re-direct url in Google Console to https
Tried to log in with Google, after I selected my Google account, it returned the same access_denied.
It would be better if the response from Google could give more detailed information.

I had the same problem using the latest ASP.Net MVC template with "Individual Accounts" selected.
The solution was to enable the Google+ API for my project in the Google Developer console.
I found my answer here (scroll down to "Changes to Google OAuth 2.0...").

The same error happened to me for Facebook provider.
Turns out the solution was as simple as updating the nuget package to 3.1.
It turns out that Facebook did a "force upgrade" of their graph API
from version 2.2 to 2.3 on 27th March 2017
For the record I'm using the following:
http://localhost:58364 in iisexpress with NO https
In Facebook I have the following settings configured for a test app:
In addition if you're using a sample template the error parameter returned isn't being consumed which can be misleading. You should add string error to ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl, string error)
{
if (error != null)
{
return View("Error");
}

I had this problem as well. After I enabled the Google+ API the problem is not solved yet. Turns out I haven't set the 'Authorized JavaScript origins' in my google API console. So I set the authorized javascript origins, and the problem solved.

I had the same issue. I had Google+ API active and set JavaScript providers. Turns out that my version of Microsoft.Owin 3.1 was too old. I've updated every single nugget which had Microsoft.Owin.(whatever) in it's name and it started working fine (version 4.1)
Hope it helps!

This is most likely because you have not enabled the Google + API in the developer console.
So when your account trys to get the details about the Google Account, it says access_denied.
Simply go to the developer console and enable the Google + API

None of the above solution worked for me. Turns out In my case I was tweaking with Google OAuth Playground and I added https://developers.google.com/oauthplayground this url in Authorized Redirect Uris section of my Google Credentials for Client ID and Secrets.
When I removed it and retried, it worked fine.
PS: I had to reset the OAuth Playground settings that I had modified too.
EDIT
The other issue was, my code threw an Exception when the user was OnAthenticated EventHandler was triggered. Turns out a null reference which was resulting in access_denied status being returned.
GoogleOAuth2AuthenticationOptions googleOptions = new GoogleOAuth2AuthenticationOptions()
{
ClientId = "xxxxx.apps.googleusercontent.com",
ClientSecret = "XXXX",
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = (context) =>
{
try
{
TokenHelper tokenHelper = new TokenHelper();
// Any exception here will result in 'loginInfo == null' in AccountController.ExternalLoginCallback.
// Be sure to add exception handling here in case of production code.
context.Identity.AddClaim(new Claim(tokenHelper.AccessToken, context.AccessToken)); // From This line and onwards. tokenHelper's properties were null.
// For clarity, we don't check most values for null but RefreshToken is another kind of thing. It's usually
// not set unless we specially request it. Typically, you receive the refresh token only on the initial request,
// store it permanently and reuse it when you need to refresh the access token.
if (context.RefreshToken != null)
{
context.Identity.AddClaim(new Claim(tokenHelper.RefreshToken, context.RefreshToken));
}
// We want to use the e-mail account of the external identity (for which we doing OAuth). For that we save
// the external identity's e-mail address separately as it can be different from the main e-mail address
// of the current user.
context.Identity.AddClaim(new Claim(tokenHelper.Email, context.Email));
context.Identity.AddClaim(new Claim(tokenHelper.Name, context.Name));
context.Identity.AddClaim(new Claim(tokenHelper.IssuedOn, DateTime.Now.ToString()));
context.Identity.AddClaim(new Claim(tokenHelper.ExpiresIn,
((long)context.ExpiresIn.Value.TotalSeconds).ToString()));
return Task.FromResult(0);
}
catch (Exception ex)
{
throw;
}
},
},
AccessType = "offline",
UserInformationEndpoint= "https://www.googleapis.com/oauth2/v2/userinfo"
};

Default Google authentication no longer works, you can add updated Owin.Security.Provider.Google package through NuGet or find it here

Try to use https:// instead of http:

Related

MSAL.NET redirect loop when using graphApi in MVC & blazor with multiple instances

I have created a blazor component that aims to simplify managing users and group of an enterprise application in my ASP.NET MVC website. When I run the code locally, everything works just fine. However, when I deploy my code on the dev environment (in AKS) the code only works if I run one replica.
When I use multiple instances and I try to access the page that calls my blazor component, the page ends up in a redirect loop, and finally shows the Microsoft login interface with an error mentioning that the login was not valid.
This is how my code looks like:
# program.cs
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
var cacheOptions = builder.Configuration.GetSection("AzureTableStorageCacheOptions").Get<AzureTableStorageCacheOptions>();
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
builder.Services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
{
options.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24);
});
builder.Services.AddDistributedAzureTableStorageCache(options =>
{
options.ConnectionString = cacheOptions.ConnectionString;
options.TableName = cacheOptions.TableName;
options.PartitionKey = cacheOptions.PartitionKey;
options.CreateTableIfNotExists = true;
options.ExpiredItemsDeletionInterval = TimeSpan.FromHours(24);
});
builder.Services.AddSession();
...
# The controller that calls the blazor component
[AuthorizeForScopes(Scopes = new[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" })]
public async Task<IActionResult> UserManagement()
{
string[] scopes = new string[] { "Application.ReadWrite.All", "Directory.Read.All", "Directory.ReadWrite.All" };
try
{
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
}
catch (Exception ex)
{
_telemetryClient.TrackException(ex);
}
return View();
}
And this is what happens:
If the page loads, I can see this exception in the pod logs:
What am I doing wrong?
The tenant actually needs to provide admin consent to your web API for the scopes you want to use for replicas for the token taken from cache.
Also when AuthorizeForScopes attribute is specified with scopes ,this needs the exact scopes that is required by that api. MsalUiRequiredException gets thrown in case of incorrect scopes for that api and results in a challenge to user.
This error may also occur even when the acquiretokensilent call will not have a valid cookie anymore for authentication in cache .Please check how acquiretokensilent call works from here in msal net acquire token silently | microsoft docs
When valid scopes are given , please make sure the permissions are granted consent by the admin directly from portal or during user login authentication.
Also as a work around try to use use httpContextAccessor to access
token after authentication .
Reference: c# - Error : No account or login hint was passed to the AcquireTokenSilent call - Stack Overflow
So, the culprit was:
#my controller
await _tokenAcquisition
.GetAccessTokenForUserAsync(scopes)
.ConfigureAwait(false);
Which we were using initially to reauthenticate the graph api component when we were using InMemoryCache.
There is no need to get the access token again when using DistributedTokenCache, and actually that was causing the token to get saved / invalidated in an infinite loop.
Also, in my blazor component, I had to do use the consent handler to force a login:
private async Task<ServicePrincipal> GetPrincipal(AzureAdConfiguration addConfiguration)
{
try
{
return await GraphClient.ServicePrincipals[addConfiguration.PrincipalId].Request()
.Select("id,appRoles, appId")
.GetAsync();
}
catch (Exception ex)
{
ConsentHandler.HandleException(ex);
throw;
}
}

Azure AD B2C ASP.NET redirect loop

We've implemented Azure AD B2C in Umbraco on the front end using Microsofts webapp sample https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi
Most of the time this is generally working, but after a while everyone starts getting hit by a redirect loop. Restating the website then clears the issue.
It seems to be something causing the .AspNet.Cookies cookie to stop being set when the user is redirected back to the site with an id token.
Any ideas?
For the folks that will run into the same problem and find this question, I wanted to share what caused this in my case and how I resolved it.
The AD B2C App Registration expects to have a RedirectURI. I forgot to put signin-oidc
So changing:
https://localhost:5000
To
https://localhost:5000/signin-oidc
resolved my problem.
This is the default value - /signin-oidc - unless something else is explicitly set.
I had infinite loop issue at logout and it was because of missing support of Razor pages. The default Microsoft.Identity.Web.UI SignOut action uses /Account/SignedOut Razor page as callback url.
var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
I added Razor support in my Asp.Net core web app and it fixed the issue.
services.AddRazorPages();
and
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
Thanks.
Please ensure that your Reply URL in your application registration matches your Redirect URI in the web.config. Try setting both of these to your main homepage URL to ensure that your app is registered properly. Also make sure that the Application ID and the Client ID are matching and the right tenant is set in your web config. This needs to be the onmicrosoft.com tenant. Also, ensure that your users have the right permissions for the application.
Please follow the instructions in my blog and video to ensure that these are set properly.
https://medium.com/#marilee.turscak/reply-urls-vs-postlogoutredirecturis-in-azure-active-directory-aad-20f57a03267b
https://www.youtube.com/watch?v=A9U1VGyztEM
You can also try deleting the application and republishing it. If none of these things work, it may actually be an issue with the platform itself.
enabled HTTPS only under TLS/SSL settings in web app .
For me, it was because I didn't have the scope defined in my b2c configuration settings, like this:
"Resources": {
"myApi": {
"ResourceUri": "https://localhost:44361",//"https://my.ui.com",
"ResourceScopes": [
"https://myapp.onmicrosoft.com/my-api/Admin.Read.Write" // this was wrong, which caused my looping
]
}
}
I was also getting a logout redirect loop. It would actually log out, but just get stuck in a loop. In my case, the redirect URL I had configured in Azure was fine (I had /signin-oidc).
I followed the guide on adding my own account controller action rather than using the built in 'MicrosoftIdentity/Account/SignOut' (while also adding the 'id_token' validation to secure the logout): https://learn.microsoft.com/en-us/azure/active-directory-b2c/enable-authentication-web-application-options#secure-your-logout-redirect
My startup.cs code is per the documentation, my controller code looks like this (the documentation code is missing 'AuthenticationProperties' variable):
namespace Cosmos.WebPortal.Controllers;
[AllowAnonymous]
[Area("MicrosoftIdentity")]
[Route("[area]/[controller]/[action]")]
public class MyAccountController : Controller
{
[HttpGet("{scheme?}")]
public async Task<IActionResult> SignOutAsync([FromRoute] string scheme)
{
scheme ??= OpenIdConnectDefaults.AuthenticationScheme;
var redirectUrl = Url.Content("~/");
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
//obtain the id_token
var idToken = await HttpContext.GetTokenAsync("id_token");
//send the id_token value to the authentication middleware
properties.Items["id_token_hint"] = idToken;
return SignOut(properties, CookieAuthenticationDefaults.AuthenticationScheme, scheme);
}
}
So my logout link is now to this controller instead e.g. 'MicrosoftIdentity/MyAccount/SignOut'
That seems to work fine, no infinite loop. A bit frustrating as I don't really understand the cause or difference, but it works.
For me, it was an expired secret/certificate in Azure B2C. It's important to look at the network log to see if any message, thankfully there was message telling me exactly where to look

Microsoft Account Authentication - AuthenticationManager.GetExternalLoginInfoAsync() always null

I have created a MVC web application and trying to use Microsoft Account authentication.
I have created the App on https://apps.dev.microsoft.com
After obtaining the App-ID and App-Secret I pasted both in my web.config
In my StartupAuth.cs I have configured Microsoft authentication with this code snippet:
var microsoftOptions = new MicrosoftAccountAuthenticationOptions()
{
ClientId = Config.MSAppId,
ClientSecret = Config.MSAppSecret,
};
app.UseMicrosoftAccountAuthentication(microsoftOptions);
It is possible to authenticate with my Windows account
In the account controller I am trying to create the account from data returned from the authentication provider. The snippet below only shows the important part.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var userName = string.Empty;
var eMail = string.Empty;
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Index", "Home");
}
// Benutzer mit diesem externen Anmeldeanbieter anmelden, wenn der Benutzer bereits eine Anmeldung besitzt
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
...
await AuthenticationManager.GetExternalLoginInfoAsync() always returns null.
The configuration in the developer portal looks like this:
I have tried to add scopes for e-mail and basic information, but this seems to be deprecated and the authentication page shows an error.
In the debug console in Visual Studio I get the following error:
Microsoft.Owin.Security.MicrosoftAccount.MicrosoftAccountAuthenticationMiddleware Error: 0 : Authentication failed
System.Net.Http.HttpRequestException: Der Antwortstatuscode gibt keinen Erfolg an: 400 (Bad Request).
bei System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
bei Microsoft.Owin.Security.MicrosoftAccount.MicrosoftAccountAuthenticationHandler.d__4.MoveNext()
I have created a clean test project from scratch, to find out what the cause for the problem is. This is what I have found out:
After the initial configuration of the authentication all Microsoft.Owin.Security.* components are on Version 3.0.1 and Microsoft Account authentication (not Windows Azure authentication - this is not part of the problem) works fine
After updating Microsoft.Owin.Security.* to Version 3.1.0 some changes happen with Microsoft Account authentication. The logon experience changes. After clicking he login button, it redirects to the azure logon experience, unlike before, where I was redirected directly to the Microsoft Account logon experience and authentication stops working.
After this I left all Microsoft.Owin.Security.* components on Version 3.1.0 and downgraded Microsoft.Owin.Security.MicrosoftAccount back to 3.0.1. And voila, I got the "old" logon experience back, with the result, that authentication worked again.
The Problem must be in Microsoft.Owin.Security.MicrosoftAccount.
Does anyone know - except from using Version 3.0.1 - how to solve the issue or is this a problem for Microsoft support?

LinkedIn Authentication via Oauth2 returns null result (error=access_denied)

I moved my ASP.NET MVC web application from membership to Identity authentication and since that I cannot authenticate on LinkedIn anymore.
The Facebook authentication is still working fine but the LinkedIn is always returning a null loginInfo after the GetExternalLoginInfo call.
For the LinkedIn I'm using the Owin LinkedIn provider: LinkedIn APIs for .NET. I also unsuccessful tried to follow this post from Jerrie Pelser.
The Application calls the ExternalLogin Action that executes the ExecuteResult method and calls back the ExternalLoginCallback (after I allow access to the application). As I stated before, the method AuthenticationManager.GetExternalLoginInfoAsync() always returns a null loginInfo.
I checked the application settings in the LinkedIn and everything seems to be OK.
Ops! I almost forgot to say that the LinkedIn is returning back the URL with a generic error message: "GET /Account/ExternalLoginCallback?error=access_denied HTTP/1.1"
I can Authenticate using the DotNetOpenAuth.Clients (hosted github) but I'd like to just use the Identity.
Startup.Auth.cs
var linkedInOptions = new LinkedInAuthenticationOptions();
linkedInOptions.ClientId = "Xxxxx";
linkedInOptions.ClientSecret = "Yyyyyyy";
linkedInOptions.Scope.Add("r_fullprofile");
linkedInOptions.Provider = new LinkedInAuthenticationProvider()
{
OnAuthenticated = async context =>
{
context.Identity.AddClaim(new System.Security.Claims.Claim("LinkedIn_AccessToken", context.AccessToken));
}
};
linkedInOptions.SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie;
app.UseLinkedInAuthentication(linkedInOptions);
ExternalLogin
public ActionResult ExternalLogin(string provider, string returnUrl)
{
// Request a redirect to the external login provider
return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }));
}
CallBack Action
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
LinkedIn CallBack URI
http://localhost:3279/signin-linkedin
After some researches and a visit the NuGet package repository I found a prerelease version of Owin.Security.Providers that worked like a charm. I just had to install it from package manager console and the issue with the null return from the LinkedIn External Login has gone.
Install-Package Owin.Security.Providers -Pre
Caution: Please be aware that the use of pre release packages may cause unexpected problems.

ASP.Net MVC 5 Google Authentication with Scope

I'm trying to get ASP.Net MVC 5 Google OAuth2 authentication working correctly.
When I set pass in a GoogleOauth2AuthenticationOptions without any scope, then I'm able to log in successfully.
var googlePlusOptions = new GoogleOAuth2AuthenticationOptions
{
ClientId = googleClientId,
ClientSecret = googleClientSecret,
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = async ctx =>
{
ctx.Identity.AddClaim(new Claim("urn:tokens:googleplus:accesstoken", ctx.AccessToken));
}
},
};
app.UseGoogleAuthentication(googlePlusOptions);
Then this call will return an ExternalLoginInfo object with all the properties set
ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
When I add any scope though, then I don't get any login info returned. It's just null.
var googlePlusOptions = new GoogleOAuth2AuthenticationOptions
{
ClientId = googleClientId,
ClientSecret = googleClientSecret,
SignInAsAuthenticationType = DefaultAuthenticationTypes.ExternalCookie,
Provider = new GoogleOAuth2AuthenticationProvider()
{
OnAuthenticated = async ctx =>
{
ctx.Identity.AddClaim(new Claim("urn:tokens:googleplus:accesstoken", ctx.AccessToken));
}
},
};
googlePlusOptions.Scope.Add(YouTubeService.Scope.Youtube);
app.UseGoogleAuthentication(googlePlusOptions);
Then the call to get external info just returns null.
ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
In the Google dev console, I have the following APIs turned on..
Analytics API
BigQuery API
Google Cloud SQL
Google Cloud Storage
Google Cloud Storage JSON API
Google+ API
Google+ Domains API
Identity Toolkit API
YouTube Analytics API
YouTube Data API v3
Something about adding scope to the options is breaking GetExternalLoginInfoAsync.
If anyone's still having trouble with this with the latest Microsoft
OWIN middleware (3.0.0+)...
I noticed from Fiddler that by default, the following scope is sent to accounts.google.com:
scope=openid%20profile%20email
If you add your own scope(s) via GoogleOAuth2AuthenticationOptions.Scope.Add(...), then the scope becomes:
scope=YOUR_SCOPES_ONLY
Therefore, you need to add the default scopes too (or at least, this fixed the issue for me):
var googlePlusOptions = new GoogleOAuth2AuthenticationOptions {
...
};
// default scopes
googlePlusOptions.Scope.Add("openid");
googlePlusOptions.Scope.Add("profile");
googlePlusOptions.Scope.Add("email");
// additional scope(s)
googlePlusOptions.Scope.Add("https://www.googleapis.com/auth/youtube.readonly");
So, I figured this out, with a lot of help from http://www.beabigrockstar.com/blog/google-oauth-sign-asp-net-identity. It turns out that the built in Google authentication provider for MVC is openId only. That's why adding a scope broke it. Using Fiddler, I was able to see the GET request to accounts.google.com, which included "scope=openid" in the querystring.
By switching to the GooglePlusOAuth2 provider in the link above, or on Nuget https://www.nuget.org/packages/Owin.Security.GooglePlus and using the provider name of "GooglePlus", I was able to succesfully add the scopes and still get back the login info from GetExternalLoginInfoAsync.
The changes Google has made to their auth mechanisms have been reflected in version 3.0.0 of Microsoft Owin middleware. As you have identified correctly, one of the changes have been moving the OAuth endpoint to Google+ (https://www.googleapis.com/plus/v1/people/me).
So, the key is to:
upgrade the OWIN middleware to version 3.0.0
enable Google+ API for your app in Google Developers Console

Resources