How do I get log output from JwtSecurityTokenHandler? - asp.net

I have an ASP.NET Core 2.1 Web Application project that uses JWT tokens for authenticating the Web API that's built-in to the project. It works fine when I run it locally on my machine, but when I deploy it to Azure (with identical environment and app-settings) it simply returns empty HTTP 401 responses to requests from my authenticated clients and I need to find out why so I can fix it.
I enabled logging of every detail in ASP.NET Core, however I never received any useful output.
First, I added Serilog.AspNetCore and the Console sink to the project through NuGet, then configured logging at Verbose level in Program.cs:
public class Program
{
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Verbose)
.MinimumLevel.Override("System", LogEventLevel.Verbose)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Verbose)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", theme: AnsiConsoleTheme.Literate)
.CreateLogger();
CreateWebHostBuilder( args ).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(String[] args)
{
return WebHost.CreateDefaultBuilder( args )
.ConfigureLogging( (ctx, cfg ) =>
{
cfg.ClearProviders();
} )
.UseStartup<Startup>()
.UseSerilog();
}
}
But when I run my web-application on Azure (with console stdout logging to file) I got this output:
[04:13:10 Verbose] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization Filter: Before executing OnAuthorizationAsync on filter
Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter.
[04:13:10 Verbose]
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
HandleAuthenticateAsync called
[04:13:10 Debug]
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
AuthenticationScheme: Bearer was not authenticated.
[04:13:10 Information]
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService
Authorization failed.
[04:13:10 Verbose] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization Filter: After executing OnAuthorizationAsync on filter
Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter.
[04:13:10 Information]
Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
[04:13:10 Verbose] Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker
Before executing action result Microsoft.AspNetCore.Mvc.ChallengeResult.
[04:13:10 Information] Microsoft.AspNetCore.Mvc.ChallengeResult
Executing ChallengeResult with authentication schemes (["Bearer"]).
[04:13:10 Verbose]
IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler
Forwarding challenge to scheme: BearerIdentityServerAuthenticationJwt
Note how despite verbose logging, the error messages (repeated below) don't give me any explanation:
AuthenticationScheme: Bearer was not authenticated.
Authorization failed.
I dug around the ASP.NET Core Security source-code to see that JwtBearerHandler.HandleAuthenticateAsync doesn't do much logging of its own, but it does call into the not-open-sourced System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler which does normally do a lot of logging, including detailed reasons (e.g. (with IDX10209-type error codes in strings), but I don't know why it isn't outputting anything I can capture.
How do I log messages from JwtSecurityTokenHandler?

I found the problem:
My HttpClient (that was sending the HTTP Authorization header Bearer token) was unintentionally sending it to a http:// URI that immediately received a 301 redirect to a https:// URI. The redirect was performed by IIS without the ASP.NET Core pipeline getting involved.
The HttpClient class does not re-send the Authorization header following a redirect (this is by-design).
I never noticed this because my HttpClient's received HttpResponseMessage had a reference to the original request which had the Authorization header, not the post-redirect request that lacked the header. I had to use Fiddler with the HTTPS proxy to see the second request was lacking the Authorization header.
When IdentityServerAuthenticationHandler or ASP.NET Core's own JwtBearerHandler receives a request with no Authorization header it does not call into JwtSecurityTokenHandler at all. To see this, open the JwtBearerHandler.cs file in the ASP.NET Core Security Git repo and look at HandleAuthenticateAsync: It has this logic:
if (string.IsNullOrEmpty(token))
{
string authorization = Request.Headers["Authorization"];
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
So in my case, it never actually called JwtSecurityTokenHandler at all, hence the lack of output messages about JWT validation.
However the output messages I did receive did not help. They're both misleading:
"AuthenticationScheme: Bearer was not authenticated." should have been something like "AuthenticationScheme: No Bearer token was present in the request." instead.
And "Authorization failed." should have been "Authorization skipped because no token was present in the request."
So in the end, the fix was to change the original request URI's scheme from http:// to https://.

Related

When an API is doing HTTP Token Authentication using Introspection, what should it return if the Introspection server is down or returns a 500?

When an API is doing HTTP Token Authentication using Introspection, what should it return if the Introspection server is down or returns a 500?
I am writing an API and want to adhere to Http standards. If the API fails to validate the token (due to the custom Introspection server being down or returning a 500) should the API return a 401, 422, or a 500?
Also, if this token introspection endpoint is not an Oauth endpoint should it return the same Http code?
I have done some research and found:
https://tools.ietf.org/id/draft-ietf-oauth-v2-bearer-11.xml#authn-header
This says: "If the protected resource request included an access token and failed authentication, the resource server SHOULD include the error attribute to provide the client with the reason why the access request was declined. The parameter value is described in Section 3.1. In addition, the resource server MAY include the error_description attribute to provide developers a human-readable explanation that is not meant to be displayed to end users."
https://tools.ietf.org/id/draft-ietf-oauth-v2-bearer-11.xml#resource-error-codes
This says: "The access token provided is expired, revoked, malformed, or invalid for other reasons. The resource SHOULD respond with the HTTP 401 (Unauthorized) status code."
Now the above make me think that for sure when using Oauth Introspection a 401 is the correct approach in the given case and not a 500. The reason being since the authentication was not a success = failure. Does this sound correct? Also, should this be the same for non-Oauth Introspection?
I reviewed Microsoft's code for their .net core API Introspection package. It shows they return a 401 in this case. The code is at: https://github.com/aspnet-contrib/AspNet.Security.OAuth.Extensions/blob/dev/src/AspNet.Security.OAuth.Introspection/OAuthIntrospectionHandler.cs
The relevant part is:
// Return a failed authentication result if the introspection
// request failed or if the "active" claim was false.
var payload = await GetIntrospectionPayloadAsync(token);
if (payload == null || !payload.Value<bool>(OAuthIntrospectionConstants.Claims.Active))
{
Context.Features.Set(new OAuthIntrospectionFeature
{
Error = new OAuthIntrospectionError
{
Error = OAuthIntrospectionConstants.Errors.InvalidToken,
ErrorDescription = "The access token is not valid."
}
});
return AuthenticateResult.Fail("Authentication failed because the authorization " +
"server rejected the access token.");
}
Finally I came accross this:
https://www.rfc-editor.org/rfc/rfc6750#section-2.1
Which says: "If the protected resource request does not include authentication
credentials or does not contain an access token that enables access
to the protected resource, the resource server MUST include the HTTP
"WWW-Authenticate" response header field; it MAY include it in
response to other conditions as well."
You definitely don't want a 4xx error, because it suggests that it might be solvable by the client or at least a client-side error. This is clearly an outage so your error code should at least start with 5.
If you are accessing a external service, that service is down or malfunctioning and the service is critical (e.g.: there's nothing you can do in terms of a fallback) the most appropriate status code might be 503 Service Unavailable.

Azure AD reply url failing on html handler

Via ASP.NET I have created a startup file that will use Azure AD to log in a user
e.g.
public void Configuration(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions()
{
ClientId = "42067b8d-b972-44e9-af86-ef60bc6d6fdb",
Authority = "https://login.windows.net/...com",
RedirectUri = "http://localhost:50560/content/story_html5.html",
PostLogoutRedirectUri = "http://localhost:50560/content/story_html5.html",
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken
});
}
And as you can see my RedirectUri in hitting a static file html file.
On my app registration in Azure portal my manifest for the replyUrls states
"replyUrls": [
"http://localhost:50560/content/story_html5.html"
],
So everything is working and connecting correctly.
(if I use a aspx for example the redirection would work)
However using the .html file I'm getting the error
HTTP Error 405.0 - Method Not Allowed
The page you are looking for cannot be displayed because an invalid
method (HTTP verb) is being used.
All I believe I need to do is add the html handler to Azure AD, does anyone know how to do this?
Thanks
This has nothing to do with Azure AD, but your configuration. Your end. Your Project. Your IIS config. Because sign-in response is a HTTP POST for security reasons. And static files handler in IIS does not accept anything beside GET for obvious reasons.
More information you will find here and there.
First, why would you want to redirect to a static page?! With the redirection after OIDC login, the IdP (Identity Provider, understand Azure AD in that case) sends valuable information which is needed by the OIDC middleware (understand the .UseOpenIdConnectAuthentication method) to be able to verify the token and initialize user session. By sending the sign-in response back to a static page you accomplish couple of things:
You cut out the OIDC middleware from the authentication - it is no longer able to process the response. Because it will not listen on static file requests. Static files are processed outside your OWIN authentication middleware.
Thus not able to verify authenticity of the user.
Thus not able to create secure cookie.
Thus not able to sign-in the user into your application.
Conclusion
Do not change the reply URL for your ASP.NET middleware, unless you explicitly and knowingly want to override the complete handling of sign-in responses.

Using Identity Server 3, ClaimsPrinciple null even after successful bearer token authentication

I have a test console app which I'm pointing at a local instance of Identity Server 3 to request an access token. The following code does this and returns my token fine (passing a single scope "scope.test.client").
static TokenResponse GetClientToken(string clientId, string clientSecret, string[] scopes)
{
var uri = new Uri(string.Concat(ID_BASE_URI, ID_URL_TOKEN));
var client = new TokenClient(
uri.AbsoluteUri,
clientId,
clientSecret);
return client.RequestClientCredentialsAsync(string.Join(" ", scopes)).Result;
I then use this token to call an API also running locally. This takes the TokenResponse obtained above and passed it to this method:
static void CallApi(string url, TokenResponse response)
{
try
{
using (var client = new HttpClient())
{
client.SetBearerToken(response.AccessToken);
Console.WriteLine(client.GetStringAsync(url).Result);
}
}
catch (Exception x)
{
Console.WriteLine(string.Format("Exception: {0}", x.Message));
}
}
The API (an ASP.NET WebApi project) uses an Owin Startup class to enforce bearer token authentication for all requests:
appBuilder.Map(baseApiUrl, inner =>
{
inner.UseWebApi(GlobalConfiguration.Configuration);
// Enforce bearer token authentication for all API requests
inner.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://identityserver/core",
ValidationMode = ValidationMode.ValidationEndpoint,
RequiredScopes = new[] { "scope.test.client" }
});
});
It also ensures all API requests are handled by a custom authorize attribute:
GlobalConfiguration.Configuration.Filters.Add(new DefaultApiAuthorizeAttribute());
Debugging this API, the first line in my overridden OnAuthorize method (in DefaultApiAuthorizeAttribute) is this:
var caller = actionContext.RequestContext.Principal as System.Security.Claims.ClaimsPrincipal;
If I break on this line I can see that actionContext.RequestContext.Principal is always null. However, I can see that ((System.Web.Http.Owin.OwinHttpRequestContext)actionContext.RequestContext).Request.Headers contains an Authorization header with the bearer token passed from my console app.
So it would seem that the API project is not authenticating the bearer token. Certainly the Identity Server logs suggest it isn't being hit at all after issuing the initial access token. So I'd appreciate your expert advice about why this might not be happening, or at least some pointers about where to look.
I suspect it might have something to do with SSL. Both sites are hosted locally under self-signed SSL certs, although Identity Server is configured to not require SSL and uses the idsrv3test.pfx development certificate for signing. I do have another test MVC web app which delegates authentication to the same IS3 instance which works fine locally, so I believe my IS3 instance is configured correctly.
You need to call UseIdentityServerBearerTokenAuthentication before you call UseWebApi. When you set up an OWIN Middleware Pipeline, the order is important.
In your case, Web API will be handling your requests before they get sent onto Identity Server (if they get sent on at all).
I imagine a range of possible issues could have the impact I described, but in my case I was able to find the cause by adding a diagnostics log to my consuming API. This led me to discover that the problem was an assembly conflict. The Owin middleware was looking for a Newtonsoft.JSON assembly with version 8.0.0.0 but my consuming API (actually running on top of a CMS intance) was using 7.0.0.0.
For anyone else who wants to find the answer fast, rather than spend hours tweaking configurations, here's the documentation that describes how to add this logging: https://identityserver.github.io/Documentation/docsv2/consuming/diagnostics.html

Why isn't OAuth2 client refreshing expired access_token?

I've got a client app configured with #EnableOAuth2Sso and #EnableZuulProxy, and a resource server (separate app) configured with #EnableOAuth2Resource. I can see that the client correctly authenticates to the resource server with Authorization: Bearer {access_token here}, but when once the access token expires, the proxied resource server request fails permanently.
[Edited]
I've modified my resource server by providing a custom RemoteTokenServices bean that uses OpenAM's /tokeninfo endpoint to decide whether an access_token remains valid. (The Spring-provided RemoteTokenServices bean attempts to POST, which gets a 405 from OpenAM). When I detect the access_token is invalid, I throw InvalidTokenException from my.spring.oauth2.OpenAMRemoteTokenServices#loadAuthentication. Now, my resource server is (I think correctly) sending HTTP 401 on the response to the client, in the case where the access_token has expired.
Still, the client is not attempting to refresh the token.
Maybe my mental model is wrong. I expect the client, in the case of expired access_token, to automatically use the refresh_token to obtain a new one. I don't know whether I think it should proactively refresh an access_token (within some epsilon before expiry time), or wait for a downstream request to fail and try refreshing then. But my client appears to be doing neither, and I can't tell why not.
As stated in this git issue: https://github.com/spring-guides/tut-spring-security-and-angular-js/issues/140, the problem might be related to the fact that with versions 1.4 and above of spring boot the Zuul filter that handles the downstream of access tokens to services (org.springframework.cloud.security.oauth2.proxy.OAuth2TokenRelayFilter) is missing a bean of type OAuth2RestTemplate, which is used by the filter itself to automatically handle the refresh_token grant when access tokens expire.
I had the same issue and I solved it by adding in a configuration class the following bean:
#Configuration
public class ZuulConfiguration {
#Bean
protected OAuth2RestTemplate oauth2RestTemplate(OAuth2ProtectedResourceDetails resource,
OAuth2ClientContext context) {
return new OAuth2RestTemplate(resource, context);
}
}

SignalR cross-domain connections with self-hosting and authentication

I have a CORS problem when self-hosting SignalR with OWIN, which only happens when I try to enable authentication.
The error I get in my web browser is:
XMLHttpRequest cannot load http://.../signalr/negotiate?[snip] Origin ... is not allowed by Access-Control-Allow-Origin
This only happens if I enable authentication in my self-hosted server using the approach in this answer:
public void Configuration(IAppBuilder app)
{
var listener = (HttpListener)app.Properties[typeof(HttpListener).FullName];
listener.AuthenticationSchemes = AuthenticationSchemes.Ntlm;
app.MapHubs(new HubConfiguration { EnableCrossDomain = true });
}
If I comment out the AuthenticationSchemes line then CORS works (and I've checked everything in these instructions). I get the same problem if I use other authentication schemes than NTLM.
Using Fiddler to examine what's going on, without authentication enabled I see the necessary CORS headers coming back from the server:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: [my server]
However once I enable authentication I get a 401 response which is missing these headers. All the requests have the necessary Origin header.
Having examined the SignalR source code it looks like the headers are being set, but presumably with authentication enabled the HttpListener is sending the initial 401 response without hitting this code.
So I think my question is: How do I get the HttpListener to include an Access-Control-Allow-Origin header in its negotiation of authentication protocols?
I have gotten NTLM authentication to work with cross domain signalR self-hosted in OWIN by allowing the preflight requests anonymous access.
What one needs to do is create a delegate for choosing the authentication scheme which looks for the preflight request headers, and allows these through anonymously. All other requests will use NTLM.
public void Configuration(IAppBuilder appBuilder)
{
var listener = (HttpListener)appBuilder.Properties[typeof(HttpListener).FullName];
listener.AuthenticationSchemeSelectorDelegate += AuthenticationSchemeSelectorDelegate;
}
private AuthenticationSchemes AuthenticationSchemeSelectorDelegate(HttpListenerRequest httpRequest)
{
if (httpRequest.Headers.Get("Access-Control-Request-Method")!=null)
return AuthenticationSchemes.Anonymous;
else
return AuthenticationSchemes.Ntlm;
}
I presume you're using Chrome, which very unhelpfully tells you that these headers are missing and that this is the problem, when actually you have probably just forgot to set your XMLHttpRequest's withCredentials property to true.
If you're using jQuery you can do this for all requests with:
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
options.xhrFields = { withCredentials: true };
});
You also need to do the right thing with OPTIONS requests as in the other answer.

Resources