Get Token from Asp.net Core to pass to backend as verification

Hello!

I’m trying to integrate Okta authentication into a suite of apps that are built in-house. Initially, I had an idea for passing the user’s “token” once authenticated between the backend servers to be used as a “login” to verify the user before processing the request. Basically a flow like this:

  1. Users attempts to access .net core front end
  2. Redirected to Okta login page
  3. Successfully logs in and gets access to front end (everything working great up to this point)
  4. Get “token” of user
  5. Frontend wants to get information from different backend server
  6. Pass token to backend server
  7. Backend server validates token with Okta
  8. Backend sees user is validated and processes the request

I’ve searched and searched and can’t seem to find anything about this. So my question then: is this even possible (and if so what am I missing) or am I completely wrong and should do it a different way?

Thanks, and let me know if you’d like code samples, though it’s mostly the same as the .net core sample.

This sounds very similar to what I am trying to accomplish as well. I am able to authenticate the user and access the id token from the cookie in hopes to pass it in an authorization header when requesting data from my API’s. Here is my post, maybe your post will shed some light on what needs to be done or vice versa.

https://devforum.okta.com/t/cookie-and-jwt-auth/5923

Indeed it does sound similar! I actually was able to make some progress on this, but still a little stuck (maybe you can help). Basically, without even having to change any of my auth setup, I am able to get the token by using:
var idToken = await HttpContext.GetTokenAsync(“id_token”);

Then when I pass that to the backend, I run the following code to verify the jwt token:

//Enable TLS1.2 for okta
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
 var issuer = "https://{oktasubdomain}.okta.com";

var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
    issuer + "/.well-known/openid-configuration",
    new OpenIdConnectConfigurationRetriever(),
    new HttpDocumentRetriever());

try
{
    var validatedToken = OktaValidation.ValidateToken(token, issuer, configurationManager).Result;

    return validatedToken;
}
catch (Exception)
{
    return null;
}

Here’s the OktaValidation class:

public static class OktaValidation
{
    public static async Task<JwtSecurityToken> ValidateToken(
    string token,
    string issuer,
    IConfigurationManager<OpenIdConnectConfiguration> configurationManager,
    CancellationToken ct = default(CancellationToken))
    {
        if (string.IsNullOrEmpty(token)) throw new ArgumentNullException(nameof(token));
        if (string.IsNullOrEmpty(issuer)) throw new ArgumentNullException(nameof(issuer));

        var discoveryDocument = await configurationManager.GetConfigurationAsync(ct);
        var signingKeys = discoveryDocument.SigningKeys;

        var validationParameters = new TokenValidationParameters
        {
            RequireExpirationTime = true,
            RequireSignedTokens = true,
            ValidateIssuer = true,
            ValidIssuer = issuer,
            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = signingKeys,
            //If I set this to false then it will verify...but that doesn't seem like the right thing to do
            ValidateLifetime = true, 
            ValidateAudience = false,
            // Allow for some drift in server time
            // (a lower value is better; we recommend two minutes or less)
            ClockSkew = TimeSpan.FromMinutes(2),
            // See additional validation for aud below
        };

        try
        {
            var principal = new JwtSecurityTokenHandler()
                .ValidateToken(token, validationParameters, out var rawValidatedToken);

            return (JwtSecurityToken)rawValidatedToken;
        }
        catch (SecurityTokenValidationException)
        {
            // Logging, etc.

            return null;
        }
    }
}

The problem right now is that the token is failing due to lifetime. We do have a high session timeout for our okta org, so maybe that’s why? The token lifetime doesn’t match the okta session lifetime, so the token is invalidating before okta requires a re-signin? If it’s a fresh signin then it validates fine…

You know the concept you have explained, sign in once and access many apps is know as Single Sign On or SSO for short, and OKTA already supports this. Why not try to implement your solution along this school of thought?

Hey @cjackson, that is what I am attempting to accomplish. However the backend server isn’t directly accessed by the user, so normal SSO authentication wouldn’t work. That’s what I’m trying to solve here with the token validation. I believe I have it functioning correctly, and after some further testing will post the results.

Ok, for anyone that finds this post down the line, here’s how I was able to get everything to work.

Assuming the frontend is a .net core app, add in the Okta.AspNetCore, IdentityMOdel, and Microsoft.IdentityModel.Tokens nuget packages (maybe more, apologies if I missed any)

Most of these steps are in the Asp.Net core getting started guide on Okta, but will try to include everything that’s necessary to get things rolling.

Update the startup.cs file and add the following under the ConfigureServices function:

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;//JwtBearerDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OktaDefaults.MvcAuthenticationScheme;
})
.AddCookie()
.AddOktaMvc(new OktaMvcOptions
{
    // Replace these values with your Okta configuration
    OktaDomain = Configuration.GetValue<string>("Okta:OktaDomain"),
    AuthorizationServerId = string.Empty, //Important for proper authorization using your okta org
    ClientId = Configuration.GetValue<string>("Okta:ClientId"),
    ClientSecret = Configuration.GetValue<string>("Okta:ClientSecret"),
    GetClaimsFromUserInfoEndpoint = true,
    Scope = new List<string> { "openid", "profile", "email", "offline_access" }
});

Now, set up the token validation during page loads. Add the following to the Configure function in startup.cs

app.Use(async (context, next) =>
{
    DateTime expires;
    var idToken = await context.GetTokenAsync("id_token");
    var expiresToken = await context.GetTokenAsync("expires_at");
    var accessToken = await context.GetTokenAsync("access_token");
    var refreshToken = await context.GetTokenAsync("refresh_token");

    if (refreshToken != null && (DateTime.TryParse(expiresToken, out expires)))
    {
        if (expires < DateTime.Now) //Token is expired, let's refresh
        {
            var client = new HttpClient();
            var tokenResult = client.RequestRefreshTokenAsync(new RefreshTokenRequest
            {
                Address = "https://yourOrg.okta.com/oauth2/v1/token",
                ClientId = "---yourclientid---",
                ClientSecret = "---yourclientsecret---",
                RefreshToken = refreshToken
            }).Result;


            if (!tokenResult.IsError)
            {
                var oldIdToken = idToken;
                var newAccessToken = tokenResult.AccessToken;
                var newRefreshToken = tokenResult.RefreshToken;
                idToken = tokenResult.IdentityToken;

                var tokens = new List<AuthenticationToken>
                {
                    new AuthenticationToken {Name = OpenIdConnectParameterNames.IdToken, Value = tokenResult.IdentityToken},
                    new AuthenticationToken
                    {
                        Name = OpenIdConnectParameterNames.AccessToken,
                        Value = newAccessToken
                    },
                    new AuthenticationToken
                    {
                        Name = OpenIdConnectParameterNames.RefreshToken,
                        Value = newRefreshToken
                    }
                };

                var expiresAt = DateTime.Now + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
                tokens.Add(new AuthenticationToken
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                });

                var result = await context.AuthenticateAsync();
                result.Properties.StoreTokens(tokens);

                await context.SignInAsync(result.Principal, result.Properties);
            }
        }
    }

    await next.Invoke();
});

app.UseAuthentication();

Now for the fun part. You can get the token from the controllers by using the following:

var idToken = await HttpContext.GetTokenAsync("id_token");

Then just pass that idToken to the backend client. The backend client can validate the token with the following code:

public bool VerifyToken(string token)
{
    var issuer = "https://yourOrg.okta.com";

    var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
        issuer + "/.well-known/openid-configuration",
        new OpenIdConnectConfigurationRetriever(),
        new HttpDocumentRetriever());
    try
    {
        var validatedToken = OktaValidation.ValidateToken(token, issuer, configurationManager).Result;

        return (validatedToken != null);
    }
    catch (Exception ex)
    {
        //Do some logging
        return false;
    }
}

And the OktaValidation class (need to add at least Microsoft.IdentityModel.Protocols.OpenIdConnect nuget):

public static class OktaValidation
{
    public static async Task<JwtSecurityToken> ValidateToken(
    string token,
    string issuer,
    IConfigurationManager<OpenIdConnectConfiguration> configurationManager,
    CancellationToken ct = default(CancellationToken))
    {

        try
        {
            if (string.IsNullOrEmpty(token)) throw new ArgumentNullException(nameof(token));
            if (string.IsNullOrEmpty(issuer)) throw new ArgumentNullException(nameof(issuer));

            var discoveryDocument = await configurationManager.GetConfigurationAsync(ct);
            var signingKeys = discoveryDocument.SigningKeys;

            var validationParameters = new TokenValidationParameters
            {
                RequireExpirationTime = true,
                RequireSignedTokens = true,
                ValidateIssuer = true,
                ValidIssuer = issuer,
                ValidateIssuerSigningKey = true,
                IssuerSigningKeys = signingKeys,
                ValidateLifetime = true,
                LifetimeValidator = new LifetimeValidator((notBefore, expires, secToken, tokenParams) =>
                {
                    if (!expires.HasValue)
                        return false;

                    //Add 3 hours to bump the expiration to where our session refreshes
                    return (expires.Value > DateTime.Now.ToUniversalTime());
                }),
                ValidateAudience = false,
                // Allow for some drift in server time
                // (a lower value is better; we recommend two minutes or less)
                ClockSkew = TimeSpan.FromMinutes(2),
            };
            var principal = new JwtSecurityTokenHandler()
                .ValidateToken(token, validationParameters, out var rawValidatedToken);

            return (JwtSecurityToken)rawValidatedToken;
        }
        catch (Exception ex)
        {
            //Error handling/loggin
            return null;
        }
    }
}

So far this has been working pretty well. I had gone through a few iterations, but hopefully I captured the ending code here. Hopefully this helps someone in the future!