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!