I'm not familiar with Microsoft's Identity Server (the "Identity Service" I refer to in the code below is a custom implementation), but you could consider writing an authentication handler to intercept the token in HTTP headers, examine a token prefix, then decide whether to process normally or allow an extended lifetime.
In my case, I intercept the token prior to JWT processing it. (I had to do this to get around a SharePoint workflow limitation. Oh, SharePoint.) Here's the AuthenticationHandler class:
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace CompanyName.Core2.Application.Middleware
{
[UsedImplicitly]
public class AuthenticationHandler : AuthenticationHandler<AuthenticationOptions>
{
public const string AuthenticationScheme = "CompanyName Token";
[UsedImplicitly] public const string HttpHeaderName = "Authorization";
[UsedImplicitly] public const string TokenPrefix = "CompanyName ";
public AuthenticationHandler(IOptionsMonitor<AuthenticationOptions> Options, ILoggerFactory Logger, UrlEncoder Encoder, ISystemClock Clock)
: base(Options, Logger, Encoder, Clock)
{
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.TryGetValue(HttpHeaderName, out StringValues authorizationValues))
{
// Indicate failure.
return await Task.FromResult(AuthenticateResult.Fail($"{HttpHeaderName} header not found."));
}
string token = authorizationValues.ToString();
foreach (AuthenticationIdentity authenticationIdentity in Options.Identities)
{
if (token == $"{TokenPrefix}{authenticationIdentity.Token}")
{
// Authorization token is valid.
// Create claims identity, add roles, and add claims.
ClaimsIdentity claimsIdentity = new ClaimsIdentity(AuthenticationScheme);
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, authenticationIdentity.Username));
foreach (string role in authenticationIdentity.Roles)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));
}
foreach (string claimType in authenticationIdentity.Claims.Keys)
{
string claimValue = authenticationIdentity.Claims[claimType];
claimsIdentity.AddClaim(new Claim(claimType, claimValue));
}
// Create authentication ticket and indicate success.
AuthenticationTicket authenticationTicket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), Scheme.Name);
return await Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
// Indicate failure.
return await Task.FromResult(AuthenticateResult.Fail($"Invalid {HttpHeaderName} header."));
}
}
}
Then in the Startup class of your service, add code to decide which authentication handler to use. The key feature here is the ForwardDefaultSelector:
public void ConfigureServices(IServiceCollection Services)
{
// Require authentication token.
// Enable CompanyName token for SharePoint workflow client, which cannot pass HTTP headers > 255 characters (JWT tokens are > 255 characters).
// Enable JWT token for all other clients. The JWT token specifies the security algorithm used when it was signed (by Identity service).
Services.AddAuthentication(AuthenticationHandler.AuthenticationScheme).AddCompanyNameAuthentication(Options =>
{
Options.Identities = Program.AppSettings.AuthenticationIdentities;
Options.ForwardDefaultSelector = HttpContext =>
{
// Forward to JWT authentication if CompanyName token is not present.
string token = string.Empty;
if (HttpContext.Request.Headers.TryGetValue(AuthenticationHandler.HttpHeaderName, out StringValues authorizationValues))
{
token = authorizationValues.ToString();
}
return token.StartsWith(AuthenticationHandler.TokenPrefix)
? AuthenticationHandler.AuthenticationScheme
: JwtBearerDefaults.AuthenticationScheme;
};
})
.AddJwtBearer(Options =>
{
Options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Program.AppSettings.ServiceOptions.TokenSecret)),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(_clockSkewMinutes)
};
});
Add an extension method to the AuthenticationBuilder class:
public static AuthenticationBuilder AddCompanyNameAuthentication(this AuthenticationBuilder AuthenticationBuilder, Action<AuthenticationOptions> ConfigureOptions = null)
{
return AuthenticationBuilder.AddScheme<AuthenticationOptions, AuthenticationHandler>(AuthenticationHandler.AuthenticationScheme, ConfigureOptions);
}
And authentication options if you need them.
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authentication;
namespace CompanyName.Core2.Application.Middleware
{
public class AuthenticationOptions : AuthenticationSchemeOptions
{
[UsedImplicitly]
public AuthenticationIdentities Identities { get; [UsedImplicitly] set; }
public AuthenticationOptions()
{
Identities = new AuthenticationIdentities();
}
}
}
AuthenticationIdentities is just a class I define to associate a token with a username, roles, and claims (the token for the SharePoint workflow engine). It's populated from appsettings.json. Your options class most likely would contain a list of users who are authorized for an extended lifetime.
using System.Collections.Generic;
using JetBrains.Annotations;
namespace CompanyName.Core2.Application.Middleware
{
public class AuthenticationIdentity
{
public string Token { get; [UsedImplicitly] set; }
public string Username { get; [UsedImplicitly] set; }
[UsedImplicitly] public List<string> Roles { get; [UsedImplicitly] set; }
[UsedImplicitly] public Dictionary<string, string> Claims { get; [UsedImplicitly] set; }
public AuthenticationIdentity()
{
Roles = new List<string>();
Claims = new Dictionary<string, string>();
}
}
}