IdentityServer4 and ASP.NET Web API
Introduction
Recently I worked on a POC on IdentityServer4. The main project is to upgrade from IdentityServer1 to IdentityServer4. I wanted to verify if existing legacy ASP.NET Web API clients can work with IdentityServer4 as well as .NET Core clients. IdentityServer4 and .NET Core clients are built against .NET Core 3.1.0. The ASP.NET Web API client is .NET Framework 4.5.2.
I set up and run the IdentityServer and ASP.NET Core Web API very quickly following IdentityServer4 Quickstarts. I would not bore you explaining how I did it as it is very clear explained in the docs, feel free to explore my code. And then for ASP.NET Web API client part, let’s create API project using microsoft docs. Now its time to make it work with IdentityServer, where the fun starts! Here is how I made it work, trying to put it together modular to be easier to read:
Versions
This post is written based on IdentityServer4 3.1.0, .NET Framework 4.5.2, and .NET Core 3.1.
IdentityServer
Before starting let’s confirm how IdentityServer setup and config should looks like:
namespace IdentityServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users);
...
}
}
}
namespace IdentityServer
{
public static class Config
{
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
new ApiResource("api1", "My ASP.NET Core Web API"),
new ApiResource("api2", "My ASP.NET Web API")
};
}
}
Setting up ASP.NET Web API client
For this we need to have a working ASP.NET Web API project. Here we are dealing with .NET Framework 4.5.2. Suggested solution to connect to IdentityServer4 is to use Katana Access Token Validation Middleware called IdentitySever3.AccessTokenValidation
. here is the official docs. Let’s install its latest version from nuget and set it up. The code on Startup
class should look like:
namespace Net4Api
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
...
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",//URL of IdentityServer4
RequiredScopes = new[] { "api2" }"//The ID defined for ASP.NET Web API
});
...
}
}
}
For testing the access token authorization, we need an authorized endpoint on ASP.NET Web API:
namespace NetApi.Controllers
{
public class IdentityController : ApiController
{
[HttpGet]
[Route("identity")]
[Authorize]
public dynamic Get()
{
var principal = User as ClaimsPrincipal;
return from c in principal.Identities.First().Claims
select new
{
c.Type,
c.Value
};
}
}
}
Last but not least is to set up OWIN log on ASP.NET Web API project. Logs help to gather more information in case of error. IdentityServer3.AccessTokenValidation
uses OWIN log provider to log the errors. I used NLog
along with NLog.Owin.Logging
. As setting up logs can be tricky some times, I have left required code and config change here for you:
namespace Net4Api
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
...
app.UseNLog((eventType) => LogLevel.Debug);
...
}
}
}
<configuration>
...
<configSections>
<section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog" />
</configSections>
<nlog autoReload="true">
<variable name="logsPath" value="${basedir}/logs" />
<targets async="true">
<target name="logfile" type="File" fileName="${logsPath}/${date:format=yyyy-MM-dd}/log.${date:format=yyyy-MM-ddTHH-mm}.txt" />
</targets>
<rules>
<logger name="*" minlevel="Debug" writeTo="logfile" />
</rules>
<extensions>
<add assembly="NLog.Extended" />
</extensions>
</nlog>
...
</configuration>
Now its time to call identity
endpoint on ASP.NET Web API. I use postman for this purpose. identity
endpoint is protected by Authorize
attribute, and the API is setup to read Bearer
tokens. In our setup, this means we should send a Bearer
token on Authorization
header when calling this endpoint. Bearer
token is the access token issued by IdentityServer. If you are not sure how to set Bearer
token find details here.
Call the identity
endpoint on ASP.NET Web API, if it works as expected and returns user’s claims(200 status code in response), you can skip the rest of blog as you’ve got no issue :)
Jwt header type
In my case I got Unauthorized
401 response. Here is the error details I collected from ASP.NET Web API logs:
Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware|Authentication failed
System.ArgumentException: IDX10703: Unable to decode the 'header': 'my Access Token'. ---> System.IdentityModel.Tokens.SecurityTokenException: IDX10702: Jwt header type specified, must be 'JWT' or 'http://openid.net/specs/jwt/1.0'. Type received: 'at+jwt'.
at System.IdentityModel.Tokens.JwtSecurityToken.Decode(String jwtEncodedString) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\JwtSecurityToken.cs:line 392
--- End of inner exception stack trace ---
at System.IdentityModel.Tokens.JwtSecurityToken.Decode(String jwtEncodedString) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\JwtSecurityToken.cs:line 403
at System.IdentityModel.Tokens.JwtSecurityToken..ctor(String jwtEncodedString) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\JwtSecurityToken.cs:line 71
at System.IdentityModel.Tokens.JwtSecurityTokenHandler.ReadToken(String tokenString) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\JwtSecurityTokenHandler.cs:line 627
at Microsoft.Owin.Security.Jwt.JwtFormat.Unprotect(String protectedText)
at Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationHandler.<AuthenticateCoreAsync>d__0.MoveNext()
As you see in error log, the issue is the access token type. ASP.NET Web API (OWIN/Katana) requires access token with JWT
type. But on IdentityServer4, default value for access token type is at+jwt
. Let’s verify the access token on jwt.ms. The "typ"
value should be at+jwt
when catching this error.
To fix this, we should change the access token type to JWT
by simply modifying the IdentityServer options.
Code change is on Startup
class of IdentityServer project:
namespace IdentityServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer(
options =>
{
options.AccessTokenJwtType = "JWT";
})
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users);
}
}
}
After code change, redeploy the IdentityServer and regenerate the access token on IdentityServer. Let’s verify the access token on jwt.ms. The "typ"
should be JWT
now. Read more about this here.
Let’s call the identity
endpoint on ASP.NET Web API again. If it works as expected and returns user’s claims(200 status code in response), you can skip the rest of blog.
Audience validation
After trying to call identity
endpoint I got Unauthorized
401 response again. Here is the error details I collected from ASP.NET Web API logs:
2020-04-30 12:50:53.7012|DEBUG|Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware|Authentication failed
System.IdentityModel.Tokens.SecurityTokenInvalidAudienceException: IDX10214: Audience validation failed. Audiences: 'api1, api2'. Did not match: validationParameters.ValidAudience: 'http://localhost:5000/resources' or validationParameters.ValidAudiences: 'null'
at System.IdentityModel.Tokens.Validators.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\Validators.cs:line 92
at System.IdentityModel.Tokens.JwtSecurityTokenHandler.ValidateAudience(IEnumerable`1 audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\JwtSecurityTokenHandler.cs:line 1179
at System.IdentityModel.Tokens.JwtSecurityTokenHandler.ValidateToken(String securityToken, TokenValidationParameters validationParameters, SecurityToken& validatedToken) in c:\workspace\WilsonForDotNet45Release\src\System.IdentityModel.Tokens.Jwt\JwtSecurityTokenHandler.cs:line 702
at Microsoft.Owin.Security.Jwt.JwtFormat.Unprotect(String protectedText)
at Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationHandler.<AuthenticateCoreAsync>d__0.MoveNext()
The error details complains for missing audience of http://localhost:5000/resources
. This is a valid error, as default /resources
audience is removed on IdentityServer4. Let’s verify the access token on jwt.ms, the "aud"
value does not contains a URL like [IdentityServerUrl]/resources
. Good news is we can add it manually by modifying IdentityServer options on IdentityServer project.
Code change is on Startup
class of IdentityServer project and is to set EmitLegacyResourceAudienceClaim
to true
:
namespace IdentityServer
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer(
options =>
{
options.AccessTokenJwtType = "JWT";
options.EmitLegacyResourceAudienceClaim = true; //Default value is false
})
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddTestUsers(TestUsers.Users);
}
}
}
After code change, redeploy the IdentityServer and, regenerate the token on IdentityServer. Let’s verify the access token on jwt.ms. The "aud"
should contains a value like http://localhost:5000/resources
now. Read more about it here.
Let’s call the identity
endpoint on ASP.NET Web API again, if it works as expected, you are all set.
Token validation
There is a chance that you still get Unauthorized
401 response, and the error details looks something like:
Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationMiddleware|invalid bearer token received
If you get the error, it is most likely caused by token validation. The token validation mode can be either set to Local
(JWTs only), ValidationEndpoint
(JWTs and reference tokens using the validation endpoint) - and Both
for JWTs locally and reference tokens using the validation endpoint. The default mode is Both
. Read more here. The
access token validation endpoint is removed on IdentityServer4. This might cause random results in some cases, here is a good example of unexpected behaviours. The suggestion is to be specific, and set the validation mode explicitly to Local
or ValidationEndpoint
. This would help to avoid confusion and random or unexpected results.
ValidationMode = Local
The easiest option to do is to set the access token validation mode to Local
. Using Local
mode, there would be no access token validations against the IdentityServer.
Code change is on Startup
class of ASP.NET Web API, modify IdentityServerBearerTokenAuthenticationOptions
:
namespace Net4Api
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
ValidationMode = ValidationMode.Local,
RequiredScopes = new[] { "api2" }"
});
}
}
}
ValidationMode = ValidationEndpoint
This option is to set access token validation mode to ValidationEndpoint
. As mentioned above, access token validation endpoint is removed in IdentityServer4, however using validation endpoint mode will lead to calling IntrospectionEndpoint
on IdentityServer4.
We need to make two changes here. First, setting up the ASP.NET Web API for introspection. This can be achieved by adding ApiSecrets
. Second, to set the access token validation mode along with the API secret.
First part of code change is on Config
class of IdentityServer project to add secret for ASP.NET Web API client:
namespace IdentityServer
{
public static class Config
{
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
new ApiResource("api1", "My ASP.NET Core Web API"),
new ApiResource("api2", "My ASP.NET Web API")
{
ApiSecrets = new Secret[]
{
new Secret("secret3".Sha256())
}
}
};
}
}
Second part of code change is on Startup
class of ASP.NET Web API and to set ValidationMode
, ClientSecret
and ClientId
properties of IdentityServerBearerTokenAuthenticationOptions
:
namespace Net4Api
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "http://localhost:5000",
ValidationMode = ValidationMode.ValidationEndpoint,
RequiredScopes = new[] { "api2" },
ClientSecret = "secret3", // Value on ApiResource
ClientId = "api2" // Value on ApiResource
});
}
}
}
Let’s call the identity
endpoint on ASP.NET Web API for the last time, it should work like a charm, if not feel free to contact me to discuss it.
Sample Code
You can find the complete code here