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 ValidationModeClientSecret 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

See also in IdentityServer4

comments powered by Disqus