Google SignIn return access denied - asp.net-mvc

I'm adding external logins to my MVC app. I've successfully added "Login with Facebook" but I have an issue with Google because my callback endpoint receives
access denied
Here is what I've done:
In console.developers.google.com I've added my URLs to Authorised JavaScript origins and Authorised redirect URIs and I've enabled Google+API
I've added my GoogleClientId and GoogleClientSecret to my Startup file
I've updated Microsoft.Owin.Security.Google to the lastest version 4.0.1
What I'm missing here?

Just to compare with what I have on a project:
startup - init with client Id and Secret (see below)
On google console:
JS origins is blank
Authorized redirect URIs: [my website url]/signin-google (don't forget to add http and https versions here if you're not strictly on one of those.
Oauth Consent Screen - Authorized domains - add in the used domain names (no http/s needed here)
APIs - Google+ API
This works for me. The only other thing I'd check is that your startup has values for the client id and secret...for example if it can't find them via whichever config setup you're using.
My Startup:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = Configuration["YOUR-CLIENT-ID"];
googleOptions.ClientSecret = Configuration["YOUR-SECRET"];
googleOptions.Scope.Add("...I have some additional scopes here...");
googleOptions.SaveTokens = true; //additional to my setup
googleOptions.AccessType = "offline"; //additional to my setup
googleOptions.AuthorizationEndpoint += "?prompt=consent"; //additional to my setup
});
...
}

I've managed to fix the problem by implementing my own provider.
First, I've been on github to get the files of Microsoft.Owin.Security.Google
I've added those files to my project, changed the userInfoEndPoint:
//private const string UserInfoEndpoint = "https://www.googleapis.com/plus/v1/people/me";
private const string UserInfoEndpoint = "https://www.googleapis.com/userinfo/v2/me";
It is actually helpful as you can add/remove any scope/fields that you want and decide which type the claim will have. In my case, I've edited each handler (facebook, google, linkedin and microsoft) to populate the claims the same way. So if I want the firstName of the user I know that the claim type will be ClaimTypes.GivenName no matter what the provider is. Hopefully it makes sense :D

Related

Sustainsys SAML2 Sample for ASP.NET Core WebAPI without Identity

Does anyone have a working sample for Sustainsys Saml2 library for ASP.NET Core WebAPI only project (no Mvc) and what's more important without ASP Identity? The sample provided on github strongly relies on MVC and SignInManager which I do not need nor want to use.
I added Saml2 authentication and at first it worked fine with my IdP (I also checked the StubIdP provided by Sustainsys) for first few steps so:
IdP metadata get properly loaded
My API properly redirects to sign-in page
Sign-in page redirects to /Saml2/Acs page, and I see in the logs that it parses the result successfully
However I don't know how to move forward from there and extract user login and additional claims (my IdP provided also an e-mail, and it is included in SAML response which I confirmed in the logs).
Following some samples found on the web and modyfing a little bit the MVC Sample from GitHub I did the following:
In Startup.cs:
...
.AddSaml2(Saml2Defaults.Scheme,
options =>
{
options.SPOptions.EntityId = new EntityId("...");
options.SPOptions.ServiceCertificates.Add(...));
options.SPOptions.Logger = new SerilogSaml2Adapter();
options.SPOptions.ReturnUrl = new Uri(Culture.Invariant($"https://localhost:44364/Account/Callback?returnUrl=%2F"));
var idp =
new IdentityProvider(new EntityId("..."), options.SPOptions)
{
LoadMetadata = true,
AllowUnsolicitedAuthnResponse = true, // At first /Saml2/Acs page throwed an exception that response was unsolicited so I set it to true
MetadataLocation = "...",
SingleSignOnServiceUrl = new Uri("...") // I need to set it explicitly because my IdP returns different url in the metadata
};
options.IdentityProviders.Add(idp);
});
In AccountContoller.cs (I tried to follow a somewhat similar situation described at how to implement google login in .net core without an entityframework provider):
[Route("[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly ILog _log;
public AccountController(ILog log)
{
_log = log;
}
[HttpGet("Login")]
[AllowAnonymous]
public IActionResult Login(string returnUrl)
{
return new ChallengeResult(
Saml2Defaults.Scheme,
new AuthenticationProperties
{
// It looks like this parameter is ignored, so I set ReturnUrl in Startup.cs
RedirectUri = Url.Action(nameof(LoginCallback), new { returnUrl })
});
}
[HttpGet("Callback")]
[AllowAnonymous]
public async Task<IActionResult> LoginCallback(string returnUrl)
{
var authenticateResult = await HttpContext.AuthenticateAsync(Constants.Auth.Schema.External);
_log.Information("Authenticate result: {#authenticateResult}", authenticateResult);
// I get false here and no information on claims etc.
if (!authenticateResult.Succeeded)
{
return Unauthorized();
}
// HttpContext.User does not contain any data either
// code below is not executed
var claimsIdentity = new ClaimsIdentity(Constants.Auth.Schema.Application);
claimsIdentity.AddClaim(authenticateResult.Principal.FindFirst(ClaimTypes.NameIdentifier));
_log.Information("Logged in user with following claims: {#Claims}", authenticateResult.Principal.Claims);
await HttpContext.SignInAsync(Constants.Auth.Schema.Application, new ClaimsPrincipal(claimsIdentity));
return LocalRedirect(returnUrl);
}
TLDR: Configuration for SAML in my ASP.NET Core WebApi project looks fine, and I get success response with proper claims which I checked in the logs. I do not know how to extract this data (either return url is wrong or my callback method should work differently). Also, it is puzzling why successfuly redirect from SSO Sign-In page is treated as "unsolicited", maybe this is the problem?
Thanks for any assistance
For anyone who still needs assistance on this issue, I pushed a full working example to github which uses a .Net Core WebAPI for backend and an Angular client using the WebAPI. you can find the example from here:
https://github.com/hmacat/Saml2WebAPIAndAngularSpaExample
As it turned out, the various errors I've been getting were due to my solution being hosted inside docker container. This caused a little malfunction in internal aspnet keychain. More details can be found here (docker is mentioned almost at the end of the article):
https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x&view=aspnetcore-2.2
Long story short, for the code to be working I had to add only these lines:
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/some/volume/outside/docker")); // it needs to be outside container, even better if it's in redis or other common resource
It fixed everything, which includes:
Sign-in action to external cookie
Unsolicited SSO calls
Exceptions with data protection key chain
So it was very difficult to find, since exceptions thrown by the code didn't point out what's going on (and the unsolicited SSO calls made me think that the SSO provider was wrongly configured). It was only when I disassembled the Saml2 package and tried various code pieces one by one I finally encoutered proper exception (about the key chain) which in turned led me to an article about aspnet data protection.
I provide this answer so that maybe it will help someone, and I added docker tag for proper audience.

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

To retrieve access token

I have created a MVC application to escalate work to other person inside my organization. I have added all the members in my organization to AAD,
and registered an application there, created app service and linked that app service to registered app with SSO enabled.
Now every time someone visits the app, they can login successfully using their respective credential.
What I want to do know is to retrieve all the members in my AAD and display them inside dropdown list so that anyone can escalate to others by just looking in the dropdown list.
I have tried with sample graph SDK to get the name of users in my organization
with this code
private string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private string appId = ConfigurationManager.AppSettings["ida:AppId"];
private string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private string scopes = ConfigurationManager.AppSettings["ida:GraphScopes"];
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
HttpContextWrapper httpContext = new HttpContextWrapper(HttpContext.Current);
TokenCache userTokenCache = new SessionTokenCache(signedInUserID, httpContext).GetMsalCacheInstance();
//var cachedItems = tokenCache.ReadItems(appId); // see what's in the cache
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
userTokenCache,
null);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }), cca.Users.First());
return result.AccessToken;
}
// Unable to retrieve the access token silently.
catch (Exception)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new ServiceException(
new Error
{
Code = GraphErrorCode.AuthenticationFailure.ToString(),
Message = Resource.Error_AuthChallengeNeeded,
});
}
}
with some change in scope.
<add key="ida:AppId" value="xxxxx-xxxxxxx-xxxxxx-xxxxxxx"/>
<add key="ida:AppSecret" value="xxxxxxxxxxx"/>
<add key="ida:RedirectUri" value="http://localhost:55065/"/>
<add key="ida:GraphScopes" value="User.ReadBasic.All User.Read Mail.Send Files.ReadWrite"/>
This enables me to get basic details of all user in my organization.
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Thanks
Subham, NATHCORP, INDIA
But how I can achieve this in my app where authentication related stuffs are done in azure only, and there is no code for authentication and authorization in entire solution.
Based on my understanding, you are using the build-in feature App Service Authentication / Authorization. You could follow here to configure your web app to use AAD login. And you need to configure the required permissions for your AD app as follows:
Note: For Azure AD graph, you need to set the relevant permissions for the Windows Azure Active Directory API. For Microsoft Graph, you need to configure the Microsoft Graph API.
Then, you need to configure additional settings for your web app. You could access https://resources.azure.com/, choose your web app and update App Service Auth Configuration as follows:
Note: For using Microsoft Graph API, you need to set the resource to https://graph.microsoft.com. Details, you could follow here.
For retrieving the access token in your application, you could get it from the request header X-MS-TOKEN-AAD-ACCESS-TOKEN. Details, you could follow Working with user identities in your application.
Moreover, you could use Microsoft.Azure.ActiveDirectory.GraphClient package for Microsoft Azure Active Directory Graph API, Microsoft.Graph package for Microsoft Graph API using the related access token.

MSAL and Azure AD: What scopes should I pass when I just want to get the user ID?

I'm using MSAL to get an ID Token which is then used to access an Web API app. I've got a couple of questions and I was wondering if someone could help me understand what's going on.
Let me start with the authentication process in the client side. In this case, I'm building a Windows Forms app that is using the following code in order to authenticate the current user (ie, in order to get an ID Token which will be used to validate the user when he tries to access a Web API app):
//constructor code
_clientApp = new PublicClientApplication(ClientId,
Authority, //which url here?
TokenCacheHelper.GetUserCache());
_scopes = new []{ "user.read" }; //what to put here?
//inside a helper method
try {
return await _clientApp.AcquireTokenSilentAsync(_scopes, _clientApp.Users.FirstOrDefault());
}
catch (MsalUiRequiredException ex) {
try {
return await _clientApp.AcquireTokenAsync(_scopes);
}
catch (MsalException ex) {
return null;
}
}
The first thing I'd like to clear is the value that should be used for the authority parameter. In this case, I'm using an URL on the form:
https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/token
However, I'm under the impression that I could also get away with something like this:
https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration
It seems like one endpoint is specific to my Azure AD while the other looks like a general (catch all) URL...Where can I find more information about these endpoints and on what's the purpose of each...
Another thing that I couldn't quite grasp is the scope. I'm not interested in querying MS Graph (or any other Azure related service for that matter). In previous versions of the MSAL library, it was possible to reference one of the default scopes. However, it seems like that is no longer possible (at least, I tried and got an exception saying that I shouldn't pass the default scopes...).
Passing an empty collection (ex.: new List<string>()) or null will also result in an error. So, in this case, I've ended passing the user.read scope (which, if I'm not mistaken, is used by MS Graph API. This is clearly not necessary, but was the only way I've managed to get the authentication process working. Any clues on how to perform the call when you just need to get an ID Token? Should I be calling a different method?
Moving to the server side, I've got a Web API app whose access is limited to calls that pass an ID token in the authentication header (bearer). According to this sample, I should use something like this:
private void ConfigureAuth(IAppBuilder app) {
var authority = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration";
app.UseOAuthBearerAuthentication(
new OAuthBearerAuthenticationOptions {
AccessTokenFormat = new JwtFormat(GetTokenValidationParameters(),
new OpenIdConnectCachingSecurityTokenProvider(authority)),
Provider = new OAuthBearerAuthenticationProvider {
OnValidateIdentity = ValidateIdentity
}
});
}
Now, this does work and it will return 401 for all requests which don't have a valid ID Token. There is one question though: is there a way to specify the claim from the Ticket's Identity that should be used for identifying the username (User.Identity.Name of the controller)? In this case, I've ended handling the OnValidateIdentity in order to do that with code that looks like this:
private Task ValidateIdentity(OAuthValidateIdentityContext arg) {
//username not getting correctly filled
//so, i'm handling this event in order to set it up
//from the preferred_username claim
if (!arg.HasError && arg.IsValidated) {
var identity = arg.Ticket.Identity;
var username = identity.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value ?? "";
if (!string.IsNullOrEmpty(username)) {
identity.AddClaim(new Claim(ClaimTypes.Name, username));
}
}
return Task.CompletedTask;
}
As you can see, I'm searching for the preferred_username claim from the ID Token (which was obtained by the client) and using its value to setup the Name claim. Is there any option that would let me do this automatically? Am I missing something in the configuration of the OAuthBearerAuthenticationMiddleware?
Regarding your First Query -
Where can I find more information about these endpoints and on what's the purpose of each...
Answer -
https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
The {tenant} can take one of four values:
common -
Users with both a personal Microsoft account and a work or school account from Azure Active Directory (Azure AD) can sign in to the application.
organizations -
Only users with work or school accounts from Azure AD can sign in to the application.
consumers -
Only users with a personal Microsoft account can sign in to the application.
8eaef023-2b34-4da1-9baa-8bc8c9d6a490 or contoso.onmicrosoft.com -
Only users with a work or school account from a specific Azure AD tenant can sign in to the application. Either the friendly domain name of the Azure AD tenant or the tenant's GUID identifier can be used.
Regarding your Second Query on Scope -
Answer - Refer to this document - OpenID Connect scopes
Regarding your Third Query on Claim -
Answer - Refer to this GIT Hub sample - active-directory-dotnet-webapp-roleclaims

How to add parameters to redirect_uri in WebApi Oauth Owin authentication process?

I'm creating a webapi project with oauth bearer token authenthication and external login providers (google, twitter, facebook etc.). I started with the basic VS 2013 template and got everything to work fine!
However, after a user successfully logs is, the owin infrastructure creates a redirect with the folllowing structure:
http://some.url/#access_token=<the access token>&token_type=bearer&expires_in=1209600
In my server code I want to add an additional parameter to this redirect because in the registration process of my app, a new user needs to first confirm and accept the usage license before he/she is registered as a user. Therefore I want to add the parameter "requiresConfirmation=true" to the redirect. However, I've no clue about how to do this. I tried setting AuthenticationResponseChallenge.Properties.RedirectUri of the AuthenticationManager but this doesn't seem to have any affect.
Any suggestions would be greatly appreciated!
It should be relatively easy with the AuthorizationEndpointResponse notification:
In your custom OAuthAuthorizationServerProvider implementation, simply override AuthorizationEndpointResponse to extract your extra parameter from the ambient response grant, which is created when you call IOwinContext.Authentication.SignIn(properties, identity).
You can then add a custom requiresConfirmation parameter to AdditionalResponseParameters: it will be automatically added to the callback URL (i.e in the fragment when using the implicit flow):
public override Task AuthorizationEndpointResponse(OAuthAuthorizationEndpointResponseContext context) {
var requiresConfirmation = bool.Parse(context.OwinContext.Authentication.AuthenticationResponseGrant.Properties.Dictionary["requiresConfirmation"]);
if (requiresConfirmation) {
context.AdditionalResponseParameters.Add("requiresConfirmation", true);
}
return Task.FromResult<object>(null);
}
In your code calling SignIn, determine whether the user is registered or not and add requiresConfirmation to the AuthenticationProperties container:
var properties = new AuthenticationProperties();
properties.Dictionary.Add("requiresConfirmation", "true"/"false");
context.Authentication.SignIn(properties, identity);
Feel free to ping me if you need more details.

Resources