Search code examples
.netactive-directoryldapadfs

How to authenticate users via ADFS while being able to relate users to database records?


Currently I am developing an application that should authenticate users who are registered in an on-premise Active Directory to support Single Sign-on (SSO). This can be achieved using the Active Directory Federation Service (ADFS), but this service is only for authentication. An alternative is using the Lightweight Directory Access Protocol (LDAP). This protocol provides functionality like reading all users from the directory.

I am running into the problem that I want to both authenticate users via SSO and associate users with entities in my database. Let me use a simplified example: a user has one or more books. When creating a book, I want to select a user as the author who is registered in the Active Directory.

Please keep in mind that I am new to ADFS, LDAP and SSO. I do have experience with Active Directory in general.

Architecture for context

The application consists of two compontents: a front-end (NextJS) and a back-end .NET 7 Web API. The front-end should talk to the back-end to get all data.

Beside those applications, there is a Domain Controller (DC) and an ADFS server connected to this DC. The users who should be able to log in and who should be related to other database entities are registered here.

Problem

The problem I am running into and can't wrap my mind around is: how can I both authenticate users via SSO and be able to fetch all users to relate them to entities in the database?

There are four ways of authenticating users I can think of:

  1. Let NextJS talk to ADFS directly to authenticate users and to obtain an access token for API authentication on the back-end (using OIDC);

  2. Let NextJS talk to the Web API that talks to ADFS to authenticate users, obtain an access token and return it to the front-end to use for API authentication;

  3. Let NextJS talk to the Web API that talks to the Active Directory using LDAP, creates a JWT token and returns it to the front-end to use for API authentication;

  4. Forget on-premise ADFS and use Azure AD (customer does not prefer this).

Option 1 means there is no way to fetch and store users as ADFS does not allow to fetch any data, but this feels like the best and easy way to make SSO possible. However, I don't see any option to relate users to database records.

Option 2 and 3 means that I should make custom authentication routes in my API and talk to the Active Directory using LDAP. This also means that I should probably make my own table with users which gives the option to relate this table with other tables. But this adds issues like synchronisation with Active Directory and concerns like: what if a user is removed from the Active Directory?

I can think of background processes to do this, but it feels a bit wrong. However, managing an own user table seems the only option if I want to relate them to other tables.

Option 4 is the modern version of ADFS and still does not provide the option to relate users to database records as far as I am aware.

This is a bit of dilemma for me, because I want the best of both worlds (SSO and user relations) and this is definitily not a new 'problem'.

I've read many articles and documentation from Microsoft about AD FS authentication, Single Sign-on, and Identity management, but I just can't connect the dots here.

Any advice would be welcome! What would be a best practice?


Solution

  • It has been a while since I posted my question. Since I have found a solution, I thought I'd share it here (thanks @Anton for poking me 😉). I won't go into configuring (applications in) ADFS , as this is beyond the scope of my initial question.

    TL;DR: I was thinking way too difficult. I simply had to setup SSO authentication between the front-end application and the ADFS server using Open ID Connect / oAuth 2.0. The back-end API validates each request using an Access Token (JWT) provided by ADFS through the front-end. The last part was relating user records to other database records. For this, I have written a background task in the API that synchronises users between Active Directory and the local database using the LDAP protocol. The sole purpose of the users table in the local database is to relate users to other records. I am not sure if this is the perfect solution, but for me it works. Just keep in mind to think about scenarios like user removal, disabling or updates in Active Directory.


    The authentication process (front-end)

    The front-end application has a direct connection with the Active Directory Federation Service (ADFS) server, meaning that on authentication, NextJS will redirect the user to the Single Sign-on page and vice versa. The back-end API is out of sight during authentication.

    Using the next-auth package I was able to implement a custom oAuth Authentication Provider, including support for refresh tokens and (multiple) user roles (defined by AD Security Groups). Even though I use OpenID Connect (OIDC) on ADFS, oAuth still applies since OIDC is built on top of the oAuth 2.0 protocol.

    This concludes the user sign-in part.

    The authentication validation (back-end)

    An important requirement for me was to validate each request to the back-end API. This was where my confusion started, as I thought that the .NET API had to check with ADFS on each request to validate the authentication. But the solution was way simpler! I am using oAuth of course! I simply had to validate the JSON Web Token. The token signature is validated using the Public Key from ADFS.

    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.Audience = $"microsoft:identityserver:{settings.ClientId}";
        options.Authority = settings.Authority;
        options.MetadataAddress = settings.MetadataAddress;
                
        // Ignore HTTPS in development only!
        options.RequireHttpsMetadata = false;
                
        // Use the public key of ADFS to validate the JSON Web Token.
        var publicCert = new X509Certificate2(settings.PublicKey);
                
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer  = settings.ValidIssuer,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
                    
            RequireExpirationTime = true,
            RequireSignedTokens = true,
            
            IssuerSigningKey = new X509SecurityKey(publicCert),
                    
            // Expired tokens should have no slack in expiration time
            ClockSkew = TimeSpan.Zero
         };
                
         // Ignore the self-signed certificate (only in development!)
         options.BackchannelHttpHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = delegate { return true; } };
    });
    

    Relating AD users to local database records

    The last question I was struggling with is: how can I relate Active Directory users to database records in a local database? For this, I have created a background task in the back-end. This task periodically fetches all Active Directory users from a certain Organisational Unit using the Lightweight Directory Access Protocol (LDAP). Each user is inserted into a Users table in the local database, which I then can relate to other records in the same database.

    The synchronisation process keeps track of updated, deleted or deactivated users. Lets say a user has been deleted from Active Directory, what do you do with all records in the database related to this user? Well, thats up to you to decide, but you have to pay attention to those scenarios.