Azure AD Authentication in ASP.NET Core 2.2 - c#

Im currently struggling to connect a ASP.NET Core 2.2 Web API to an existing Azure AD. I based my configuration upon this sample code by the ASP.NET Core team. Cookies were replaced with JWTs.
Unable to retrieve document from metadata adress
Now I face the following error message:
IOException: IDX10804: Unable to retrieve document from: {MetadataAdress}.
- Microsoft.IdentityModel.Protocols.HttpDocumentRetriever+<GetDocumentAsync>d__8.MoveNext()
- System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
- System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
- Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever
+<GetAsync>d__3.MoveNext()
- System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
- System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
- Microsoft.IdentityModel.Protocols.ConfigurationManager+<GetConfigurationAsync>d__24.MoveNext()
When I call the URL directly, I receive an instant response with the configuration file. However, the code does not seem to be able to do it. Im not sure what the reason could be.
Azure AD Configuration Syntax
The most likely cause of this issue is a configuration mistake. Maybe I have mistaken a field's syntax or am missing an important value.
Connection Info Fields
The connection info fields are provided like this:
TenantId: {Tenant-GUID}
Authority: https://login.microsoftonline.com/{TenantId}
Resource: https://{resource-endpoint}.{resource-domain}
ClientId: {Client-GUID}
ClientSecret: {ClientSecret}
Service Configuration
The authentication service configuration in the Startup.cs looks like this:
services
.AddAuthentication(options => {
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddJwtBearer()
.AddOpenIdConnect(options => {
options.ClientId = this.ClientId;
options.ClientSecret = this.ClientSecret;
options.Authority = this.Authority;
options.Resource = this.Resource;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.SignedOutRedirectUri = "/signed-out";
options.Events = new OpenIdConnectEvents()
{
OnAuthorizationCodeReceived = async context =>
{
var request = context.HttpContext.Request;
var currentUri = UriHelper.BuildAbsolute(
request.Scheme, request.Host, request.PathBase, request.Path
);
var credentials = new ClientCredential(this.ClientId, this.ClientSecret);
var authContext = new AuthenticationContext(
this.Authority,
AuthPropertiesTokenCache.ForCodeRedemption(context.Properties)
);
var result = await authContext.AcquireTokenByAuthorizationCodeAsync(
context.ProtocolMessage.Code,
new System.Uri(currentUri),
credentials,
this.Resource
);
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
// Custom
options.MetadataAddress = $"{this.Authority}/federationmetadata/2007-06/federationmetadata.xml";
options.RequireHttpsMetadata = false; // Dev env only
}
Existing APIs
There is a bunch of existing Web APIs that connect to this Azure AD. Sadly, they are all using the full .NET Framework. They use the UseWindowsAzureActiveDirectoryBearerAuthentication method from the Microsoft.Owin.Security.ActiveDirectory namespace's WindowsAzureActiveDirectoryBearerAuthenticationExtensions.
Another thing they use is the HostAuthenticationFilter with an authentication type of Bearer.
Questions
What is the problem?
How can I resolve this issue?
How can I use these components together?
ASP.NET Core 2.2
JWT Bearer Authentication
Azure AD (token validation + claim extraction only - creation is handled by other service)

You are using OpenIDConnect libraries and point them to WS-Federation metadata (/federationmetadata/2007-06/federationmetadata.xml). This is not going to work.
The correct metadata endpoint for OpenIDConnect is /.well-known/openid-configuration. This is described here. Change that first, and then return cookies.
UPDATE
What I oversaw, was that you are protecting WebAPI. You say the middleware to use JwtBearer as default authentication cheme, but you also include a challenge scheme to be OIDC. That doesn't really make sense for me. Why do you want an OIDC challenge scheme for an WebAPI?
Here you can find the ASP.NET Core samples about JwtBearer. Here the Azure AD samples demoing WebApp calling WebApi (also bearer for the WebAPI, OIDC for the App FrontEnd.
There are no samples for JWT Bearer Auth using OIDC challenge. Why do you want to implement that? What is the case? You might be looking at implementing multiple Authentication schemes, which is possible. But not having one scheme for Authentication and another for challenge...
If by updating/removing the wrong metata changes the error message, include that in your original question. As it is now - the pure error message is that OIDC Middleware cannot parse WS-Federation metadata. Which is expected.

Source of the problem
After some testing I managed to identify the problem: Apparently the main cause of this issue was network related. When I switched from our company's to an unrestricted network the authentication was a success.
The fix
I had to configure a proxy and provide it to the JwtBearer and OpenIdConnect middleware. This looks like this:
var proxy = new HttpClientHandler
{
Proxy = new WebProxy("{ProxyUrl}:{ProxyPort}") { UseDefaultCredentials = true; },
UseDefaultCredentials = true
};
services
.AddJwtBearer(options => {
// ... other configuration steps ...
options.BackchannelHttpHandler = proxy;
})
.AddOpenIdConnect(options => {
// ... other configuration steps ...
options.BackchannelHttpHandler = proxy;
})
Metadata adress
#astaykov was right that the metadata adress is indeed incorrect. I had this feeling as well but kept it as previous APIs were running successfully with it. During problem testing I removed it, too, but it would not make a difference due to the network issues.
After the network issues were resolved, using the default metadata adress worked. The custom one failed - as expected when using a different authentication schema.

Related

JWT-based authorization in microservice architecture with dedicated identity service

I have two services implemented as Web APIs in ASP.NET Core, dockerized and orchestrated (docker-compose and Kubernetes). One service provides authentication and authorization (authNZ, IdentityService), and the other provides resources to authNZ'd users (ResourceService).
Any authenticated user (OIDC-based authentication against Google) has a JWT token, which they can use to add as a bearer token to their API calls to the ResourceService.
Q1: Should ResourceService validate every token calling IdentityService?
Supposing the answer is yes, the authorization middleware of the ResourceService fails with the following error when validating authNZ to an API endpoint.
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Exception occurred while processing message.
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'https://identity/.well-known/openid-configuration'.
Q2: Do I need to implement the .well-known endpoints in the IdentityService to validate the JWT tokens?
I am aware of the complexities and challenges associated with implementing an Identity service without leveraging dedicated libraries (e.g., IdentityServer4). However, given some licensing issues, I cannot leverage such libraries.
I am sharing some setups I find most relevant, but happy to share other parts of the code if needed.
AuthNZ configuration of the ResourceService:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://identity";
options.Audience = "https://resource";
});
AuthNZ configuration of the IdentityService:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://identity";
options.Audience = "https://resource";
})
.AddCookie(options =>
{
options.LoginPath = "/api/v1/authnz/signin";
})
.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
{
options.ClientId = "...";
options.ClientSecret = "...";
options.CallbackPath = "/authnz/google/callback";
});
Clients and APIs needs to be able to download the configuration document at https://identity/.well-known/openid-configuration and it should be a public document that is not protected in anyway.
When you get this error:
Exception occurred while processing message. System.InvalidOperationException: IDX20803: Unable to obtain configuration from
It is typically because the service can't reach the IdentityProvider. Typically it s a HTTPS or networking issue in your backend.
Every service that provides tokens should preferably expose a configuration document. If you are using IdentityServer, this is built-in.
The alternative is that you provide the public signing key manually to each service.
To fix Q1, the it all depends on the deployment and network setup of your services. It is typically either due to HTTPS certificate issues or if one service can't talk to other services behind the firewall. Sometimes you might need to use a different "service name" when you need to send HTTP(s) requests behind the firewall.

Antiforgery throws bad request in asp.net core web api angular

I am trying to add anti-forgery to my asp.net core 3.1 web API by adding a filter in the startup file.
options => options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute())
This web API is consumed by my angular app which is hosted in a different port. I have done all the configuration which is specified in Microsoft docs as below.
app.Use(next => context =>
{
string path = context.Request.Path.Value;
if (path != null)
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN",
tokens.RequestToken, new CookieOptions
{
HttpOnly = false,
Path = "/",
}
);
}
return next(context);
});
In services
services.AddAntiforgery(options =>
{
options.HeaderName = "X-XSRF-TOKEN";
});
It generates two tokens one is.Asp.NetCore.AntiForgery and XSRF-TOKEN. I am getting this token in my client app and sending it to API as request header as x-xsrf-token but it fails every time. I have set up my cors to allow any origin. I am getting token as below in my angular app.
let xsrfToken = this.xsrfTokenExtractor.getToken() as string;
if(xsrfToken){
request = request.clone({headers: request.headers.set("X-XSRF-TOKEN", xsrfToken)});
}
Let me explain to you my flow. I have an identity server, web API, and angular app all of which are hosted in different ports. The angular app redirects to the identity server for authentication once it's done it will be redirected back to my client app. I have set up this csrf in web API. so basically, the authentication happens using a bearer token. I know that we don't need csrf protection because we already use a bearer token as my authentication mechanism. But I need it to work for csrf as well. Is there any way to achieve this?
I have a similar flow and I encountered a similar problem. In my case, I did not use the Configure method but instead I created a separate Endpoint that allows me to request the cookies.
var aft = _antiForgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append("XSRF-TOKEN", aft.RequestToken, new CookieOptions
{
HttpOnly = false,
Secure = true,
Domain = config.Domain
});
Initially I was doing the logging in part the cookie part in the same request and that lead to every request being rejected with a 400 Bad Request token but once I separated the login part and the cookies part in two separated endpoints and made two requests, everything worked as expected.

How to Set Up CORS enabled domain authentication cookie .Net Core?

I am trying to authenticate CORS origin requests and set Claims principle with the user of internal company single sign on utility. I have the current setting so far, the cookie will never get created on the domain set at the authentication setup.
I have an Angular client application and .Net Core 3.0 Webapi, the requirement is for the client to be able to set authentication for future api calls.
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.Name = "access_token";
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.Domain = "localhost:xxxx";
});
//CORS
services.AddCors(options =>
{
options.AddPolicy(
"AllowOrigin",
builder => builder.WithOrigins("localhost:xxxx")
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod());
});
//Sign In
HttpContext.SignInAsync(
scheme: CookieAuthenticationDefaults.AuthenticationScheme,
principal: new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme)),
properties: new AuthenticationProperties { ExpiresUtc = DateTime.Now.Add(120) });
I am testing this all on local so both URLS are localhost with different ports
Angular is hosted: http://localhost:xxxx
WebAPi is hosted :http://localhost:xxx2
http request from Angular to webapi is http://localhost:xxx2/api/auth which has the SignInAsync call, the company single sign does a username but the cookie never gets created. If I remove the options.Cookie.Domain = "localhost:xxxx"; the cookie does get created on the webapi domain http://localhost:xxx2. I must be missing something here.
After reading up some other posts on stackoverflow , it tuned out that AllowAllOrigins will only fix this problem but poses a threat.
So I ended up fixing this issue with JWT - setting authorization token for every request sent from client interface. This issue was caused due to fact that the client and WebApi are hosted on different domains.

Identity Server 4 Protect an API

I'm trying to use Identity Server 4 to protect my API. Now I have gone through all the documentation at http://docs.identityserver.io/en/release/quickstarts/1_client_credentials.html and I have set up a few successful demos. However, there is one thing that I am failing to understand.
For example, First, we need to define a client on the IS4 that looks like this:
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
Then on the API we protect it by adding the IdentityServer4.AccessTokenValidation package and adding configuration to startup.cs
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
And then finally we add app.UseAuthentication();
Now, this all works when ran, however, the part that I don't understand is where is the secret defined in the API. As you can see, the client clearly expects a secret and yet I don't define this secret anywhere on my API. I also don't define anywhere on the IS4 anything about my API to say that you are protecting the API from this URI or something along those lines.
So how does this actually works in terms of IS4 knowing about the API and authenticating its requests?
EDIT:
To clarify some confusion, yes there is a client that I opted out in code above and I see now that I shouldn't, and in there I provide a secret, but I'm still not understanding how does the IS4 knows to protect my specific API. What if the request came from www.somerandomapi.com? From what I"m reading it would work regardless. Based on what you wrote, it does make sense that the client is passing the secret, but nowhere in my code is the IS4 told which API to protect.

401 Unauthorized when using the generic OAuth Middleware with ASP.NET 5

I am trying to hook up a website that I am building to FitBit using ASP.NET 5 (rc1-final), Identity and the MS.AspNet.Authentication.OAuth middleware. I am intending to use the Authorization Grant Flow for OAuth 2.0. I have the app set up (details below) on FitBit, and my Startup.cs looks like:
app.UseIdentity();
app.UseOAuthAuthentication(options =>
{
options.AuthenticationScheme = "FitBit-AccessToken";
options.AuthorizationEndpoint = "https://www.fitbit.com/oauth2/authorize";
options.TokenEndpoint = "https://api.fitbit.com/oauth2/token";
options.SaveTokensAsClaims = true;
options.CallbackPath = new PathString("/signing-fitbit-token/");
options.ClientId = "[MY ID STRIPPED OUT]";
options.ClientSecret = "[MY SECRET STRIPPED OUT]";
options.DisplayName = "FitBit";
options.Scope.Add("activity");
options.Scope.Add("heartrate");
options.Scope.Add("location");
options.Scope.Add("nutrition");
options.Scope.Add("profile");
options.Scope.Add("settings");
options.Scope.Add("sleep");
options.Scope.Add("social");
options.Scope.Add("weight");
options.AutomaticAuthenticate = true;
});
When I click the login button, I am directed to the authorization page on FitBit, but when I click Authorize, I am greeted with the ASP.NET dev error page:
An unhandeled exception occurred while processing the request.
HttpRequestException: Response status code does not indicate success: 401 (Unauthorized)
System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
I did read here that with some OAuth endpoints (namely Yahoo) they don't like localhost. So, I tried it both with localhost, and modifying my hostfile to a different domain. I have ensured that the redirect url that I am passing in is what is registered for the app at FitBit.
This error is coming from my website, and is getting through to the point where its exchanging the code for the access token. I have fiddler open I'm a bit lost as to where to go from here. I am running on http (since this is local dev and I don't have an ssl cert yet), but I wasn't entirely sure if that mattered.
By default, the OAuth2 generic middleware sends the client credentials by flowing them in the request form (encoded using application/x-www-form-urlencoded).
Sadly, Fitbit only supports basic authentication: since the credentials are not flowed in the Authorization header, Fitbit treats your token request as unauthenticated and rejects it.
Luckily, this is something the dedicated Fitbit social provider (internally based on the OAuth2 generic middleware) will handle for you: https://www.nuget.org/packages/AspNet.Security.OAuth.Fitbit/1.0.0-alpha3
app.UseFitbitAuthentication(options => {
options.ClientId = "230H9";
options.ClientSecret = "ae7ff202cg5z42d85a3041fdc43c9c0b2";
});
Something is going wrong with the OAuth request to FitBit, you need to debug that request and see why you got a 401 back from FitBit.

Categories