Search code examples
authentication.net-coreoauth-2.0openid-connectadfs

ADFS OpenId integration in .NET API results in Stackoverflow


Background

I am working on a web application that consists of a separate API (.NET 7) and Front-end (NextJS / React). In short: the front-end calls the API to fetch information that should be displayed.

The authentication and authorization should go through Single Sign-on (SSO). The client has a on-premise ADFS server (Windows Server 2019). So far I have managed to setup ADFS to work with the front-end application to actually log in and obtain a JSON Web Token.

For this I have create a Application Group:

The application group I created

The Front-end uses NextAuth to authenticate users via the oAuth 2.0 / OpenID Connect protocol by automatically redirect to the ADFS authentication page where the user logs in with their AD-account. This is working fine.

What I am trying

Now that the front-end is secured, I want to secure the API as well. As far as my understanding of ADFS goes, the front-end should use JWT as a Bearer token when calling the API which then should validate the token using ADFS. So I am trying to setup the connection between the .NET API and ADFS.

The problem

When accessing any route (also anonymous!), the console of the API is massively spamming exceptions. In fact: its a Stack Overflow exception:

info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
Stack overflow.
   at System.Collections.Generic.Dictionary`2[[Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCacheKey, Microsoft.Extensions.DependencyInjection, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].FindValue(Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCacheKey)
   at System.Collections.Generic.Dictionary`2[[Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCacheKey, Microsoft.Extensions.DependencyInjection, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].TryGetValue(Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCacheKey, System.__Canon ByRef)
   at DynamicClass.ResolveService(ILEmitResolverBuilderRuntimeContext, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(System.Type, Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]](System.IServiceProvider)
   at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.GetAuthenticationService(Microsoft.AspNetCore.Http.HttpContext)
   at Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions.AuthenticateAsync(Microsoft.AspNetCore.Http.HttpContext, System.String)
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1+<HandleAuthenticateAsync>d__13[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
   at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[[Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1+<HandleAuthenticateAsync>d__13[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], Microsoft.AspNetCore.Authentication, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]](<HandleAuthenticateAsync>d__13<System.__Canon> ByRef)
   at System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].Start[[Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1+<HandleAuthenticateAsync>d__13[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], Microsoft.AspNetCore.Authentication, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]](<HandleAuthenticateAsync>d__13<System.__Canon> ByRef)
   at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].HandleAuthenticateAsync()
   at Microsoft.AspNetCore.Authentication.AuthenticationHandler`1+<AuthenticateAsync>d__48[[System.__Canon, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()

This continues until the app finally crashes.

What I have done so far

OpenID Connect / oAuth 2.0 and definitely ADFS are new concepts for me. I do understand the basics, but I find it very difficult to find useful examples with ADFS within a .NET API. They are usually Azure AD or .NET Framework.

By scraping all kinds of sources, I was able to produce the following code:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = "Bearer";
    options.DefaultChallengeScheme = "Bearer";
})
.AddOpenIdConnect("Bearer", options =>
{
    options.ClientId = configuration["ClientId"];
    options.Authority = configuration["Authority"];
    options.MetadataAddress = configuration["MetadataAddress"];
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidAudiences = new[] { options.ClientId }
    };
    options.RequireHttpsMetadata = false; // Set this to true in production environments
});
  • The ClientID matches the ID of the Server application inside the Application Group as this is the only application with a Client ID.
  • The Authority is in this format: https://adfs.<company>.local/adfs
  • The MetadataAddress is in this format: https://adfs.<company>.local/federationmetadata/2007-06/federationmetadata.xml. I do know that OpenID uses the .well-known/openid-configuration format, but examples using this code use the XML file.

An important note I think is that the ADFS-server is using a self-signed certificate as this is a local testserver.

My question

I have no clue where the issue is located. It could be a wrong ADFS configuration due to my limited understanding, it could be a code issue. The only theory I can think of is that it has something to do with the self-signed certificate, but I don't know how to 'trust' or bypass it apart from the RequireHttpsMetadata option.

If anyone has an idea of what could be wrong, please let me know! This topic seems very limited to the enterprise world, as I can only find a very limited amount of online examples with similar situations.


Solution

  • @Tore's answer and comments have pointed me into the right direction, thanks! I have to treat the Front and Backend differently. Since my solution is not exactly the same as Tore suggested, I will answer my question myself.

    The Front-end has not changed and is still using OpenID to let the sign in using the ADFS Sign In page. This was and is stil working.

    The Back-end however should not be using OpenID, but use the JWT Bearer token that was generated by the Front-end instead. I ended up with the following solution:

    
    services.AddAuthentication(options =>
    {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Audience = "microsoft:identityserver:<SERVER_APP_CLIENT_ID>";
        options.Authority = "https://adfs.<company>.local/adfs";
        options.MetadataAddress = "https://adfs.<company>.local/adfs/.well-known/openid-configuration";
                    
        // Ignore HTTPS in development only!
        options.RequireHttpsMetadata = false;
                    
        // Use the public key of ADFS to validate the JSON Web Token.
        var publicCert = new X509Certificate2("adfs_pkey.cer");
                    
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer  = "http://adfs.<company>.local/adfs/services/trust",
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
                        
            RequireExpirationTime = true,
            RequireSignedTokens = true,
                
            IssuerSigningKey = new X509SecurityKey(publicCert),
        };
                    
        // Ignore the self-signed certificate (only in development!)
        options.BackchannelHttpHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = delegate { return true; } };
    });
    

    Hardcoded values will be moved to a configuration file ;-)

    As you can see: the only thing I do is simply validate the Bearer token on issuer, audience, signature and expiration.

    Another important step - for me atleast - is to validate the Bearer token signature using the public key from ADFS (RSA256). I do know that it is best practice to store those keys in key vaults.

    Thats is. This way the API is able to correctly validate the Bearer token generated by the Front-end using SSO (or directly via Postman) to secure the Back-end as well!