4

I'm trying to implement an identity server based on OpenIddict. The use case we have is one large javascript application that needs to authenticate users to multiple back-end APIs. The javascript application gets a token from a dedicated OpenIddict server using the password flow. The token is then validated by the various APIs that are called by the front end.

I've implemented the server using ASP.NET Identity and EF, and can successfully retrieve a valid token. Our APIs are running in AWS Lambda, so we can't (or don't want to) use the standard .AddDataProtection methods. We use a self-signed certificate stored in S3 to generate the tokens and validate them.

Then problem is that when I send the access_token to the back-end APIs, they are not able to validate the token and provide access. I know the tokens are valid because I can manually decrypt them in Linqpad using a JwtSecurityTokenHandler and the self-signed certificate.

Here is my server configuration:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                options.ClaimsIdentity.UserNameClaimType = Claims.Name;
                options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
                options.ClaimsIdentity.RoleClaimType = Claims.Role;
            })
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

            services.AddOpenIddict()
            .AddCore(options =>
            {
                options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>();
            })
            .AddServer(options =>
            {
                options.SetTokenEndpointUris("/connect/token");
                options.AllowPasswordFlow();
                options.AcceptAnonymousClients();
                options.AddEncryptionCertificate(MyCustomCertificate);
                options.AddSigningCertificate(MyOtherCustomCertificate);
                options.UseAspNetCore().EnableTokenEndpointPassthrough();
            });

            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(connectionString);
                options.UseOpenIddict();
            });

        }

I've tried two different confiugrations in the API, and neither of them work. Option 1, preferred, using the built-in OpenIddict token validation:

        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();

            services.AddOpenIddict().AddValidation(
                options =>
                {
                    options.SetTokenValidationParameters(config =>
                    {
                        config.ValidateAudience = false; //just to make sure it's not a typo causing the problem
                        config.ValidateIssuer = false; //just to make sure it's not a typo causing the problem
                        config.TokenDecryptionKey = new X509SecurityKey(MyCustomCertificate);
                        config.IssuerSigningKey = new X509SecurityKey(MyOtherCustomCertificate);
                    });

                    options.UseAspNetCore();
                });

        }

This invariably results in {"error":"invalid_token","error_description":"The specified token is not valid."}, even though I can manually decrypt and validate the token. I've tried setting the default log level to Trace, but nothing else shows up in the log that might explain where the problem is.

Option 2, using built-in .NET token validation:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.Authority = "https://openiddictserver";
                    options.Audience = "clientid"; 
                    options.RequireHttpsMetadata = false;
                    options.IncludeErrorDetails = true; 
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        NameClaimType = OpenIdConnectConstants.Claims.Subject,
                        RoleClaimType = OpenIdConnectConstants.Claims.Role,
                        ValidateAudience = false, //just to make sure it's not a typo problem
                        ValidateIssuer = false, //just to make sure it's not a typo problem
                        TokenDecryptionKey = new X509SecurityKey(MyCustomCertificate),
                        IssuerSigningKey = new X509SecurityKey(MyOtherCustomCertificate)
                    };
                });

Option 2 just results in a 401 with no helpful info whatsoever.

Some more context:

  • I can validate the signed id_token just fine. It's only when trying to use the encrypted access_token that I run into trouble
  • I'm calling app.UseAuthentication() in the correct place and the [Authorize(AuthenticationScheme="something")] attributes are correct.
  • I'm using the OpenIddict.AspNetCore package version 3.0.0 from the dedicated openiddict nuget feed.

I'm fairly certain it just comes down to not being able to decrypt the access_token in the proper way in the API.

wjm03
  • 191
  • 2
  • 7
  • Consider changing the default log level in ASP.NET Core to Trace to allow OpenIddict to log the low-level exception thrown by IdentityModel when validating your JWT access tokens. – Kévin Chalet May 17 '20 at 15:30
  • Thanks @KévinChalet, your prompt and persistent support of this package is a big reason we chose to use it. Default log level is set to Trace, but the only relevant log entry is `dbug: OpenIddict.Validation.OpenIddictValidationProvider[0]=> RequestPath:/api/myendpoint RequestId:someguid, SpanId:guid, TraceId:guid, ParentId: The request was rejected in user code.` – wjm03 May 17 '20 at 20:18
  • I did manage to get it working using the standard token validation through `AddJwtBearer` which is good enough in the short term. But the same validation parameters passed to `.AddValidation` lead to the error referenced above. We'd love to get the built-in OpenIddict validation working over the long term so as to take advantage reference tokens and other features. – wjm03 May 17 '20 at 20:19
  • Thanks for your kind words. Could you please post a repro on GitHub so I can take a look? :) – Kévin Chalet May 17 '20 at 22:31
  • Repro posted at https://github.com/wjmoody03/OpenIddictProblemRepro.git Hopefully the Readme has enough info to make it easy. – wjm03 May 18 '20 at 06:24
  • Thanks for the repro. There were a few issues (e.g the non-working `config => config = tokenValidationParameters` syntax and `/WeatherForecastControllerWithOpenIddict` instead of `/WeatherForecast`) but it definitely helped pinpoint the issue. I posted more details below. – Kévin Chalet May 18 '20 at 13:26

1 Answers1

4

The behavior you're seeing is caused by a limitation in the alpha bits of the OpenIddict validation handler, that checks whether TokenValidationParameters.IssuerSigningKeys is null, but not TokenValidationParameters.IssuerSigningKey.

To work around it, you can use:

config.IssuerSigningKeys = new[] { new X509SecurityKey(MyOtherCustomCertificate) };

Alternatively, you can use discovery to allow it to download the signing keys from the authorization server:

services.AddOpenIddict()
    .AddValidation(options =>
    {
        options.SetIssuer(new Uri("https://localhost:44365/"));
        options.AddEncryptionCertificate(AuthenticationExtensionMethods.TokenEncryptionCertificate());

        options.UseAspNetCore();
        options.UseSystemNetHttp();
    });

It's worth noting that the options.SetTokenValidationParameters() method will be removed very soon (as part of the introspection support addition). The new syntax for registering a static OIDC configuration will be something like that:

services.AddOpenIddict()
    .AddValidation(options =>
    {
        options.SetConfiguration(new OpenIdConnectConfiguration
        {
            Issuer = "https://localhost:44365/",
            SigningKeys = { new X509SecurityKey(AuthenticationExtensionMethods.TokenSigningCertificate()) }
        });

        options.AddEncryptionCertificate(AuthenticationExtensionMethods.TokenEncryptionCertificate());
    });
Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
  • 1
    @wjm03 FYI, OpenIddict was updated earlier today to use the new syntax I mentioned. – Kévin Chalet May 26 '20 at 16:08
  • Hey Kevin, I am facing kind of the same issue : Microsoft.IdentityModel.Tokens.SecurityTokenInvalidIssuerException: IDX10205: Issuer validation failed. Issuer: 'System.String'. Did not match: validationParameters.ValidIssuer: 'System.String' or validationParameters.ValidIssuers: 'System.String'. – Manzur Alahi Mar 21 '21 at 14:18
  • My services work fine in my computer, When deployed The identity server can validate it's own token on it's own API, but rejects the token when sent through introspect end point – Manzur Alahi Mar 21 '21 at 14:19
  • But everything works fine in my local machine – Manzur Alahi Mar 21 '21 at 14:20
  • 1
    How do i turn off issuer validation using the new validation configuration?? – Manzur Alahi Mar 21 '21 at 20:20
  • 2
    Is AuthenticationExtensionMethods class yours or some nuget package? – Arthur Neto Nov 25 '21 at 19:14