The OWIN OpenID Connect Middleware
- 1/22/2016
- OWIN and Katana
- OpenID Connect middleware
- TokenValidationParameters
- More on sessions
- Summary
TokenValidationParameters
You think we’ve gone deep enough to this point? Not quite, my dear reader. The rabbit hole has one extra level, which grants you even more control over your token-validation strategy.
OpenIdConnectAuthenticationOptions has a property named TokenValidationParameters, of type TokenValidationParameters.
The TokenValidationParameters type predates the RTM of Katana. It was introduced when the Azure AD team released the very first version of the JWT handler (a .NET class for processing the JWT format) as a general-purpose mechanism for storing information required for validating a token, regardless of the protocol used for requesting and delivering it and the development stack used for supporting such protocol. That was a clean break with the past: up to that moment, the same function was performed by special XML elements in the web.config file, which assumed the use of WIF and IIS. It was soon generalized to support the SAML token format, too.
The OpenID Connect middleware itself still uses the JWT handler when it comes to validating incoming tokens, and to do so it has to feed it a TokenValidationParameters instance with the desired validation settings. All the metadata inspection mechanisms you have been studying so far ultimately feed specific values—the issuer values to accept and the signing keys to use for validating incoming tokens’ signatures—in a TokenValidationParameters instance. If you did not provide any values in the TokenValidationParameters property (I know, it’s confusing) in the options, the values from the metadata will be the only ones used. However, if you do provide values directly in TokenValidationParameters, the actual values used will be a merger of the TokenValidationParameters and what is retrieved from the metadata (using all the options you learned about in the “Authority coordinates and validation” section).
The preceding mechanisms hold for the validation of the parameters defining the token issuer, but as you know by now, there are lots of other things to validate in a token, and even more things that are best performed during validation. If you don’t specify anything, as is the case the vast majority of the time, the middleware fills in the blanks with reasonable defaults. But if you choose to, you can control an insane number of details. Figure 7-16 shows the content of TokenValidationParameters in OpenID Connect middleware at the initialization time for our sample application. I am not going to unearth all the things that TokenValidationParameters allows you to control (that would take far too long), but I do want to make sure you are aware of the most commonly used knobs you can turn.
Figure 7-16 The TokenValidationParameters instance in OpenIdConnectAuthenticationOptions, as initialized by the sample application.
Valid values
As you’ve learned, the main values used to validate incoming tokens are the issuer, the audience, the key used for signing, and the validity interval. With the exception of the last of these (which does not require reference values because it is compared against the current clock values), TokenValidationParameters exposes a property for holding the corresponding value: ValidIssuer, ValidAudience, and IssuerSigningKey.
What is less known is that TokenValidationParameters also has an IEnumerable for each of these—ValidIssuers, ValidAudiences, and IssuerSigningKeys—which are meant to make it easy for you to manage scenarios in which you need to handle a small number of alternative values. For example, your app might accept tokens from two different issuers simultaneously. Or you might use a different audience for your development and staging deployments but have a single codebase that automatically works in both.
Validation flags
One large category of TokenValidationParameters properties allows you to turn on and off specific validation checks. These Boolean flags are self-explanatory: ValidateAudience turns on and off the comparison of the audience in the incoming claim with the declared audience (in the OpenID Connect case, the clientId value); ValidateIssuer controls whether your app cares about the identity of the issuer; ValidateIssuerSigningKey determines whether you need the key used to sign the incoming token to be part of a list of trusted keys; ValidateLifetime determines whether you will enforce the validity interval declared in the token or ignore it.
At first glance, each of these checks sounds like something you’d never want to turn off, but there are various occasions in which you’d want to. Think of the subscription sample I described for SecurityTokenValidated: in that case, the actual check is the one against the user and the subscription database, so the issuer check does not matter and can be turned off. There are more exotic cases: in the Netherlands last year, a gentleman asked me how his intranet app could accept expired tokens in case his client briefly lost connectivity with the Internet and was temporarily unable to contact Azure AD for getting new tokens.
There is another category of flags controlling constraints rather than validation flags. The first is RequireExpirationTime, which determines whether your app will accept tokens that do not declare an expiration time (the specification allows for this). The other, RequireSignedTokens, specifies whether your app will accept tokens without a signature. To me, a token without a signature is an oxymoron, but I did encounter situations (especially during development) where this flag came in handy for running some tests.
Validators
Validation flags allow you to turn on and off validation checks. Validator delegates allow you to substitute the default validation logic with your own custom code.
Say that you wrote a SaaS application that you plan to sell to organizations instead of to individuals. As opposed to the user-based validation you studied earlier, now you want to allow access to any user who comes from one of the organizations (one of the issuers) who bought a subscription to your app. You could use the ValidIssuers property to hold that list, but if you plan to have a substantial number of customers, doing that would be inconvenient for various reasons: a flat lookup on a list might not work too well if you are handling millions of entries, dynamically extending that list without recycling the app would be difficult, and so on. The solution is to take full control of the issuer validation operation. For example, consider the following code:
TokenValidationParameters = new TokenValidationParameters { IssuerValidator = (issuer,token,tvp) => { if(db.Issuers.FirstOrDefault(b => (b.Issuer == issuer)) == null) return issuer; else throw new SecurityTokenInvalidIssuerException("Invalid issuer"); } }
The delegate accepts as input the issuer value as extracted from the token, the token itself, and the validation parameters. In this case I do a flat lookup on a database to see whether the incoming issuer is valid, but of course you can imagine many other clever validation schemes. The validator returns the issuer value for a less-than-intuitive reason: that string will be used for populating the Issuer value of the claims that will ultimately end up in the user’s ClaimsPrincipal.
All the other main validators (AudienceValidator, LifetimeValidator) return Booleans, with the exception of IssuerSigningKeyValidator and CertificateValidator.
Miscellany
Of the plethora of remaining properties, I want to point your attention to two common ones.
SaveSignInToken is used to indicate whether you want to save in the ClaimsPrincipal (hence, the session cookie) the actual bits of the original token. There are topologies in which the actual token bits are required, signature and everything else intact: typically, the app trades that token (along with its credentials) for a new token, meant to allow the app to gain access to a web API acting on behalf of the user. This property defaults to false, as this is a sizable tax.
The TokenReplayCache property allows you to define a token replay cache, a store that can be used for saving tokens for the purpose of verifying that no token can be used more than once. This is a measure against a common attack, the aptly called token replay attack: an attacker intercepting the token sent at sign-in might try to send it to the app again (“replay” it) for establishing a new session. The presence of the nonce in OpenID Connect can limit but not fully eliminate the circumstances in which the attack can be successfully enacted. To protect your app, you can provide an implementation of ITokenReplayCache and assign an instance to TokenReplayCache. It’s a very simple interface:
public interface ITokenReplayCache { bool TryAdd(string securityToken, DateTime expiresOn); bool TryFind(string securityToken); }
In a nutshell, you provide the methods for saving new tokens (determining for how long they need to be kept around) and bringing a token up from whatever storage technology you decide to use. The cache will be automatically used at every validation—take that into account when you pit latency and storage requirements against the likelihood of your app being targeted by replay attacks.