ASP.NET Core - Multiple Microsoft Authentication Providers - c#

I'm trying to add multiple Microsoft Authentication Providers to my ASP.NET Core Application and dynamically show one of them on the login site based on the url parameter (tenant).
I have this loop:
var authBuilder = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
foreach (var microsoftExternalProvider in securityOptions.ExternalAuthentication.Microsoft)
{
authBuilder.AddMicrosoftAccount(microsoftExternalProvider.Name, microsoftOptions => {
microsoftOptions.ClientId = microsoftExternalProvider.ClientId;
microsoftOptions.ClientSecret = microsoftExternalProvider.ClientSecret;
if (microsoftExternalProvider.IsSingleTenant)
{
microsoftOptions.AuthorizationEndpoint = $"https://login.microsoftonline.com/{microsoftExternalProvider.TenantId}/oauth2/v2.0/authorize";
microsoftOptions.TokenEndpoint = $"https://login.microsoftonline.com/{microsoftExternalProvider.TenantId}/oauth2/v2.0/token";
}
});
}
I'm taking ids and secrets from array in appsettings.json.
The problem is that only the first provider set up in appsettings works. The other ones after trying to log in throw an exception:
System.Exception: An error was encountered while handling the remote login.
---> System.Exception: The oauth state was missing or invalid.
--- End of inner exception stack trace ---
at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
What should I do to make all the providers work? Couldn't find any information about adding "AddMicrosoftAccount()" more then once. That's really important for me to be able to log in from different tenants in different organizations. It can't be just one multi-tenant provider - business requirement.

The problem is that each provider has the same default callback path - "/signin-microsoft".
The solution is to configure it in the loop like this for example:
microsoftOptions.CallbackPath = new PathString($"/signin-microsoft-{microsoftExternalProvider.Name}");
and set azure active directory app authentication web redirect url to proper url.

Related

Losing USER information in ELMAH when using ADFS

I have an MVC web application using ELMAH to log unhandled exception. I'm now moving from windows authentication to ADFS authentication. The application has been adapted and is working fine. However now when I check the error, the user information is not there anymore. Which makes sense as ELMAH is using the context identity and not the claims to retrieve this info. Does anyone of you have an idea how I could do to get this information logged again?
You can enrich ELMAH errors using error filtering hook if you can accept a small hack. In short, you need to implement the ErrorLog_Filtering method in the Global.asax.cs file:
void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs args)
{
var httpContext = args.Context as HttpContext;
if (httpContext != null)
{
var error = new Error(args.Exception, httpContext);
error.User = GetUserFromDatabase();
ErrorLog.GetDefault(httpContext).Log(error);
args.Dismiss();
}
}
In the example, I update the User property with the value of a dummy method. How you want to implement this depends on how you would get the currently logged in user from ADFS. Finally, I log the error again and dismiss the initial (user-less) error.

.Net Core Identity 2 Provider login Cancel leads to unhandled exception

I've added LinkedIn as a provider. I have implemented the login and register with LinkedIn without any issue. In the use case where the user CANCELS from within the provider Pages (either linkedIn login or cancels the authorization of the app) the identity middleware seems to throw an unhandled exception:
An unhandled exception occurred while processing the request.
Exception: user_cancelled_login;Description=The user cancelled LinkedIn login
Unknown location
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
System.Exception: user_cancelled_login;Description=The user cancelled LinkedIn login
Exception: An error was encountered while handling the remote login.
The provider setup in startup defines the callback:
services.AddAuthentication().AddOAuth("LinkedIn", "LinkedIn", c =>
{
c.ClientId = Configuration["Authentication:LinkedIn:ClientId"];
c.ClientSecret = Configuration["Authentication:LinkedIn:ClientSecret"];
c.Scope.Add("r_basicprofile");
c.Scope.Add("r_emailaddress");
c.CallbackPath = "/signin-linkedin";
....
And As I have said the middleware seems to handled ALL other cases except where the user cancels within the LinkedIn pages. The return URL from LinkedIn looks correct:
https://localhost:44372/signin-linkedin?error=user_cancelled_login&error_description=The+user+cancelled+LinkedIn+login&state=CfDJ8MA7iQUuXmhBuZKmyWK9xeAgBBkQvnhf1akLhCIn9bsajCPUf7Wg22oeZBH9jZOIY3YrchMSWZ4dH7NQ1UngLKEuqgU-IHfBglbgJDtS-wc4Z-6DnW66uR0G1ubvNVqatFJHYv17pgqZT98suVkvKgihcJdbNEw7b1ThkuFbn9-5EcYhQ5ln6ImoTgthT8io1DOcCfc_-nBVfOa93a6CpUJTsZc9w93i70jn5dKKXSLntZe0VyRSA0r0PKc5spu5En-0R1rxiLjsjo4dy89PV3A
But never gets to my ExternalCallback controller method where the other cases like successful login/authorization are handled??
I'm wondering if this is working for anyone else with 3rd part providers?
There's a Github issue that explains what's happening here in more detail, with a bit of information as to why it's happening and even an indication that this won't be "fixed":
Handling the RemoteFailure event is the right thing to do. We should update our docs/samples to show how to handle that event and at least show a more appropriate message to the user. The error page sample could include a link to enabled the user to try logging in again.
Unfortunately it's difficult to implement this event in a very generic way that's also super useful because each remote auth provider has its own behavior for different types of failures.
The workaround for this (as quoted above) is to handle the RemoteFailure event:
services.AddAuthentication().AddOAuth("LinkedIn", "LinkedIn", c => {
// ...
c.Events.OnRemoteFailure = ctx =>
{
// React to the error here. See the notes below.
return Task.CompletedTask;
}
// ...
});
ctx is an instance of RemoteFailureContext, which includes an Exception property describing what went wrong. ctx also contains a HttpContext property, allowing you to perform redirects, etc, in response to such exceptions.
I've found the following to work well for me, based on this and similar to Kirk Larkin's answer. The part that took a little figuring out was where to redirect to, without causing problems for subsequent login attempts.
services.AddAuthentication().AddOAuth("LinkedIn", "LinkedIn", c =>
{
...
c.Events = new OAuthEvents()
{
OnRemoteFailure = (context) =>
{
context.Response.Redirect(context.Properties.GetString("returnUrl"));
context.HandleResponse();
return Task.CompletedTask;
}
};
};

Active Directory: MSAL (UWP) PublicClientApplication.AcquireTokenAsync(...) returns exception

I am trying to implement a sign in / login function using Active Directory. I am basing myself in on this b2c-xamarin sample.
Below is the relevant code that I am having issues with. I have made modifications here to simplify readability. I have inserted comments for anything noteworty, particularly AcquireTokenAsync:
string ClientID = "<application_id_of_b2c_application>"
string Authority = "https://login.microsoftonline.com/tfp/<b2c_tenant_name>/<signin_policy_name>/oauth2/v2.0/authorize"
PublicClientApplication PCA = new PublicClientApplication(ClientID, Authority);
// The application says to override this which I do not as I am not sure if its required for actual sign in
PCA.RedirectUri = $"msal{ClientID}://auth";
// UWP SIGN IN CODE
string Scopes = { "User.Read" };
string PolicySignUpSignIn = "<signin_policy_name>";
// Arguments #2 and #3 both return null. This happens also with the unmodified sample that works.
// I do not know what to put in for argument #1 (scopes) - I have tried numerous combinations to no avail. currently I have { "User.Read" }
AuthenticationResult ar = await PCA.AcquireTokenAsync(Scopes, GetUserByPolicy(PCA.Users, PolicySignUpSignIn), PCA.UiParent);
The excpetion I get when calling AcquireTokenAsync is the following (truncated for readability purposes - I inserted the beginning and the end)
{Microsoft.Identity.Client.MsalException: WAB authentication failed
---> System.IO.FileNotFoundException: The specified protocol is unknown. (Exception from HRESULT: 0x800C000D) at
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task
task) ...(TRUNCATION)...
--- End of stack trace from previous location where exception was thrown --- at
UserDetailsClient.MainPage.d__2.MoveNext()
ErrorCode: authentication_ui_failed}
The way I see it the problem could be any of the following:
PublicClientApplication (PCA) was initialized with incorrect parameters (client id / authority)
I am using the wrong Scopes argument for AcquireTokenAsync - currently its: { "User.Read" }
I need to specify the proper redirect URI and assign it to PCA before calling AcquireTokenAsync in UWP
I am missing something on the Azure end
I have tried many combinations of arguments based on the values I have in Azure AD to no avail. I could really use some help.

MSAL Error message AADSTS65005 when trying to get token for accessing custom api

I downloaded the example below to get an access token from MS Graph and it worked fine. Now I changed the code to get a token from a custom web API. On apps.dev.microsoft.com I registered a client application and an the API.
Client and server registration in AD
private static async Task<AuthenticationResult> GetToken()
{
const string clientId = "185adc28-7e72-4f07-a052-651755513825";
var clientApp = new PublicClientApplication(clientId);
AuthenticationResult result = null;
string[] scopes = new string[] { "api://f69953b0-2d7f-4523-a8df-01f216b55200/Test" };
try
{
result = await clientApp.AcquireTokenAsync(scopes, "", UIBehavior.SelectAccount, string.Empty);
}
catch (Exception x)
{
if (x.Message == "User canceled authentication")
{
}
return null;
}
return result;
}
When I run the code I login to AD via the dialog en get the following exception in the debugger:
Error: Invalid client Message = "AADSTS65005: The application
'CoreWebAPIAzureADClient' asked for scope 'offline_access' that
doesn't exist on the resource. Contact the app vendor.\r\nTrace ID:
56a4b5ad-8ca1-4c41-b961-c74d84911300\r\nCorrelation ID:
a4350378-b802-4364-8464-c6fdf105cbf1\r...
Error message
Help appreciated trying for days...
For anyone still striking this problem, please read this:
https://www.andrew-best.com/posts/please-sir-can-i-have-some-auth/
You'll feel better after this guy reflects all of your frustrations, except that he works it out...
If using adal.js, for your scope you need to use
const tokenRequest = {
scopes: ["https://management.azure.com/user_impersonation"]
};
I spent a week using
const tokenRequest = {
scopes: ["user_impersonation"]
};
.. since that is the format that the graph API scopes took
As of today, the V2 Endpoint does not support API access other than the Microsoft Graph. See the limitations of the V2 app model here.
Standalone Web APIs
You can use the v2.0 endpoint to build a Web API that is secured with
OAuth 2.0. However, that Web API can receive tokens only from an
application that has the same Application ID. You cannot access a Web
API from a client that has a different Application ID. The client
won't be able to request or obtain permissions to your Web API.
For the specific scenario that you are trying to accomplish, you need to use the V1 App Model (register apps on https://portal.azure.com).
In the very near future, V2 apps will be enabled to call other APIs other than Microsoft Graph, so your scenario will be supported, but that is just not the case today. You should keep an eye out on our documentation for this update.
In your (server) application registration in AAD, you need to specify your scopes in the oauth2Permissions element.
You may already have a user_impersonation scope set. Copy that as a baseline, give it a unique GUID and value, and then AAD will let your client request an access token with your new scope.

Facebook C# SDK OAuth Exception "ClientID required"

This question is, I think, similar to my previous one.
Using the latest C# Facebook SDK on .NET 4 I get an Exception with the message "ClientID required" with the following code on the last line:
var app = new DefaultFacebookApplication();
app.AppId = "appId";
app.AppSecret = "secret";
var fb = new FacebookWebContext(app);
fb.IsAuthenticated();
App ID and secret are properly set. The stack trace of the exception is the following:
System.Exception occurred
Message=ClientID required. Source=Facebook StackTrace:
at Facebook.FacebookOAuthClient.BuildExchangeCodeForAccessTokenParameters(IDictionary`2 parameters, String& name, String& path)
at Facebook.FacebookOAuthClient.ExchangeCodeForAccessToken(String code, IDictionary`2 parameters)
at Facebook.FacebookSession.get_AccessToken()
at Facebook.FacebookSession.get_Expires()
at Facebook.Web.FacebookWebContext.IsAuthenticated()
at Piedone.FacebookTest.Authorize() InnerException:
On the client side I'm using the JS SDK, initialized as following:
FB.init({
appId: appId,
status: true, // check login status
cookie: true, // enable cookies to allow the server to access the session
xfbml: true, // parse XFBML
oauth: true // enable OAuth 2.0
});
The users gets properly logged in with the JS login() method, as the alert in the following piece of code runs:
FB.login(function (response) {
if (response.authResponse) {
alert("logged in");
} else {
alert('User cancelled login or did not fully authorize.');
}
}, { scope: scope });
In the app settings on Facebook both the "Forces use of login secret for OAuth call and for auth.login" and "Encrypted Access Token" are turned on. As far as I know all this should enable the use of the OAuth 2 authentication.
Anybody has an idea what am I doing wrong? There really can't be any error in these few lines of code...
Thanks in advance for any help!
Edit:
The AccessToken property of FacebookWebContext throws the same error and HttpContext.CurrentNotification does:
CurrentNotification '(_facebookWebContextCache.HttpContext).CurrentNotification' threw an exception of type 'System.PlatformNotSupportedException' System.Web.RequestNotification {System.PlatformNotSupportedException}
This operation requires IIS integrated pipeline mode.
Since I must run the program from Visual Studio with its Development Server (as I'm currently developing the application) there is no way anything can be done about the latter exception, I suppose. Actually I also tried with Webmatrix's IIS express, but the problem persists.
It's also interesting, that in the FacebookWebContext the settings (app id, secret) are correctly set as well, the user Id and the signed request is also there...
Edit 2:
I also get the same error when using the SDK source. It looks that AccessToken and in the Session the Expires property throw the exception. I don't know if this is connected to the httpcontext issue above.
One more solution is add facebook settings to you web or app congfig
<facebookSettings appId="appid" appSecret="secret" />
after that create Auth class
var oauth = new FacebookOAuthClient(FacebookApplication.Current);
And it wil work as well
Finally I managed to solve the problem, but most likely this is a bug in the SDK.
The cause
The problem is that the FacebookApplication.Current is empty, as it does not get populated with data set in the FacebookWebContext ctor. This leads to the problem of the access token: in FacebookSession.AccessToken on line 119 FacebookOAuthClient is instantiated with FacebookApplication.Current, that of course is practically empty. So FacebookOAuthClient is throwing the exception as it doesn't get the application settings.
The solution
The workaround is to simply explicitly set the current FacebookApplication together with the instantiation of FacebookWebContext:
var app = new DefaultFacebookApplication();
app.AppId = "appId";
app.AppSecret = "secret";
var fb = new FacebookWebContext(app);
FacebookApplication.SetApplication(app); // Note this is the new line

Categories