Skip to content

SecurityProfileRequirements Class

The bundle of controls a ClientSecurityProfile forces on a client, expressed as individual flags the request-pipeline validators consult. This is the single place the profile-to-controls mapping lives, so a validator never needs to know what "FAPI 2.0" means - it only reads the one flag it owns - and adding a future profile touches only Resolve(ClientSecurityProfile).

C#
public sealed record SecurityProfileRequirements : System.IEquatable<Abblix.Oidc.Server.Features.ClientInformation.SecurityProfileRequirements>

Inheritance System.Object → SecurityProfileRequirements

Implements System.IEquatable<SecurityProfileRequirements>

Remarks

A flag normally requires a control and never relaxes one, so a profile tightens a client and cannot weaken it. That is what lets a granular toggle (for example PkceRequired set to false) coexist with a profile without silently downgrading it.

One flag goes the other way, and the exception is deliberate rather than an escape hatch. ForbidRefreshTokenRotation removes a control, because the specification it comes from replaces that control with two others instead of dropping protection: rotation earns nothing once the client is confidential and its tokens are bound to their sender, and it costs a user their session whenever a client fails to store the token it was handed. A relaxing flag is therefore admissible only when the same profile carries the controls that stand in for what it removes, which FindUnreplacedRelaxations() checks for every profile at startup rather than leaving to review.

Every flag below names the validator that enforces it. That coupling is documented here on purpose: the enforcement is distributed across the request pipeline, so a new flag added to a profile without a matching consumer would ship silently unenforced. When adding a flag, wire a validator that reads it and a test that proves the control fires.

Properties

SecurityProfileRequirements.DefaultClockSkew Property

The tolerance in force where a deployment names none of its own - an answer this profile supplies rather than imposes: a deployment setting a value of its own wins over it, and is held only by MaxClockSkew.

C#
public Abblix.Jwt.ClockSkew DefaultClockSkew { get; init; }

Property Value

ClockSkew

Remarks

Selecting no profile is a posture rather than the absence of one, and its answer is Abblix.Oidc.Server.Features.ClientInformation.SecurityProfileRequirements.UnprofiledClockSkew: an assertion arrives from an issuer whose clock this server does not run, and RFC 7523 Section 3 allows for that offset without naming a bound.

Where a profile bounds freshness the two halves part company: an expiry the client itself chose is a deadline this server has no reason to extend, because the grace exists for a clock that disagrees rather than for a token that is simply late.

SecurityProfileRequirements.ForbidRefreshTokenRotation Property

The profile forbids refresh token rotation, which is the one flag that removes a control rather than requiring one. See the remarks on this type for why that is admissible here and what stands in its place. Enforced by Features.Tokens.RefreshTokenService.

C#
public bool ForbidRefreshTokenRotation { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.MaxClockSkew Property

The furthest either clock window may reach under this profile, or null where the profile puts no bound on them. Read by every place that builds JWT validation parameters.

C#
public System.Nullable<System.TimeSpan> MaxClockSkew { get; init; }

Property Value

System.Nullable<System.TimeSpan>

Remarks

Unlike the flags around it this carries a VALUE, because the requirement names one and a boolean would leave each reader to remember it - which is how two readers come to bound the same thing differently. Null rather than a large number, so that a profile putting no bound on the future is the ABSENCE of one and cannot be mistaken for a generous bound somebody chose.

SecurityProfileRequirements.RequireCodeResponseTypeOnly Property

The profile permits only the authorization-code response type, rejecting any implicit or hybrid response type that returns a token or id_token from the authorization endpoint. Enforced by Endpoints.Authorization.Validation.FlowTypeValidator at request time and by SecurityProfileConsistency as a fail-loud registration/startup check.

C#
public bool RequireCodeResponseTypeOnly { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequireConfidentialClient Property

The profile admits only confidential clients as defined by RFC 6749, so a client that authenticates with nothing at the token endpoint cannot be held to it. Enforced by SecurityProfileConsistency at registration and at startup.

C#
public bool RequireConfidentialClient { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequireIssuerAudienceInClientAssertion Property

The profile accepts only the server's issuer identifier, and only as a string, in the audience of a client authentication assertion, narrowing what the underlying specification otherwise permits. Enforced by Features.ClientAuthentication.JwtAssertionAuthenticatorBase.

C#
public bool RequireIssuerAudienceInClientAssertion { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequireKeyBasedClientAuthentication Property

The profile admits only client authentication that proves possession of a key: mutual TLS (RFC 8705 section 2) or a private key JWT assertion (OpenID Connect Core section 9). Every method keyed on a shared secret is refused. Enforced by SecurityProfileConsistency at registration and at startup.

C#
public bool RequireKeyBasedClientAuthentication { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequirePkce Property

The profile mandates PKCE on every authorization request, even when the client's own PkceRequired is false. Enforced by Endpoints.Authorization.Validation.PkceValidator.

C#
public bool RequirePkce { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequirePushedAuthorizationRequests Property

The profile requires the client to start every authorization flow through a Pushed Authorization Request, independent of the server-wide RequirePushedAuthorizationRequests flag. Enforced by Endpoints.Authorization.RequestFetching.PushedRequestFetcher.

C#
public bool RequirePushedAuthorizationRequests { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequireS256CodeChallenge Property

The profile restricts the PKCE code challenge method to exactly S256, rejecting both plain and the non-standard S512 extension. FAPI 2.0 names S256, and the IANA "PKCE Code Challenge Methods" registry defines only plain and S256, so a conformance suite never presents S512. Enforced by Endpoints.Authorization.Validation.PkceValidator.

C#
public bool RequireS256CodeChallenge { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequireSenderConstrainedTokens Property

The profile requires a sender-constrained access token, satisfied by either a DPoP proof (RFC 9449) or a certificate-bound token over mutual TLS (RFC 8705 section 3). Enforced by Endpoints.Token.Validation.DPoPTokenEndpointValidator.

C#
public bool RequireSenderConstrainedTokens { get; init; }

Property Value

System.Boolean

SecurityProfileRequirements.RequireStrictRequestObjectProcessing Property

The profile requires strict RFC 9101 section 6.3 request-object processing: only the parameters inside the request object are used and any parameter passed outside it is ignored, instead of the OpenID Connect Core section 6.1 merge behaviour. FAPI 2.0 mandates JWT-Secured Authorization Requests with this exclusivity. Enforced by Features.RequestObject.RequestObjectFetcher.

C#
public bool RequireStrictRequestObjectProcessing { get; init; }

Property Value

System.Boolean

Methods

SecurityProfileRequirements.ClockSkewOrDefault(Nullable<ClockSkew>) Method

The tolerance that actually applies: what the caller configured where it configured anything, otherwise DefaultClockSkew, in either case held to MaxClockSkew.

C#
public Abblix.Jwt.ClockSkew ClockSkewOrDefault(System.Nullable<Abblix.Jwt.ClockSkew> configured=null);

Parameters

configured System.Nullable<ClockSkew>

A tolerance the caller set, or null to take this profile's own.

Returns

ClockSkew

Remarks

The bound is applied HERE rather than carried onward beside the value, so that no reader can take one without the other. A ceiling travelling as a second field is a ceiling somebody forgets to pass, and the omission reads as a deployment allowed to be looser rather than as the mistake it is.

SecurityProfileRequirements.FindUnreplacedRelaxations() Method

Names every profile that removes a control without carrying the controls that stand in for it. An empty list means each relaxation in this file is paid for.

C#
public static System.Collections.Generic.IReadOnlyList<string> FindUnreplacedRelaxations();

Returns

System.Collections.Generic.IReadOnlyList<System.String>

Remarks

This exists because a relaxing flag is one edit away from becoming an ordinary permission. Someone adding a profile, or loosening an existing one, sees a set of booleans with no direction to them, and nothing in the type distinguishes the flag that removes protection from the nine that add it. So the condition that makes the removal sound is stated as code and run at startup, where it can fail, rather than as a paragraph that can be skipped.

Refusing refresh token rotation is sound only alongside a confidential client and a sender-constrained token, because those two are what make rotation redundant. A profile carrying the relaxation without them would hand out long-lived multi-use refresh tokens to a client that may be public and whose tokens anyone may replay.

SecurityProfileRequirements.For(ClientInfo, ClientSecurityProfile) Method

The control bundle a client is actually held to: what the deployment demands of everyone, tightened by whatever the client names for itself.

C#
public static Abblix.Oidc.Server.Features.ClientInformation.SecurityProfileRequirements For(Abblix.Oidc.Server.Features.ClientInformation.ClientInfo client, Abblix.Oidc.Server.Common.Constants.ClientSecurityProfile defaultProfile);

Parameters

client ClientInfo

The client whose effective profile is being resolved.

defaultProfile ClientSecurityProfile

The profile the deployment holds every client to.

Returns

SecurityProfileRequirements

Remarks

A deployment-wide profile is a FLOOR, not a default. Turning one on is a statement about every client the server serves, so a client cannot step out from under it - it can only ask for more. The alternative reading, where a client naming a profile replaces the deployment's, makes the server-wide setting a suggestion: one registration would quietly leave a FAPI 2.0 deployment serving a client under none of its controls, and nothing about that registration would look like a decision to weaken the server.

The combination is per CONTROL rather than a choice between two bundles, because "stricter" is not a property a bundle has - one profile can demand sender-constrained tokens while another names a tighter clock window, and picking either bundle whole would drop the other's demand.

SecurityProfileRequirements.Resolve(ClientSecurityProfile) Method

Returns the control bundle a given profile mandates.

C#
public static Abblix.Oidc.Server.Features.ClientInformation.SecurityProfileRequirements Resolve(Abblix.Oidc.Server.Common.Constants.ClientSecurityProfile profile);

Parameters

profile ClientSecurityProfile

Returns

SecurityProfileRequirements

Remarks

The default arm throws rather than answering Abblix.Oidc.Server.Features.ClientInformation.SecurityProfileRequirements.NoneRequirements. A profile added to the enum without a bundle here would otherwise resolve to no requirements at all, which is silently the weakest answer available and reads at every call site as a deliberate one.

A value the enum does not define is a different population and gets a different answer. It arrives from outside - a configuration binder takes a number outside the range as it stands, and a client store the host writes can hold anything - so it is data rather than a mistake in this file, and the readers meeting it are handling a live request. Throwing there turns a host's bad value into a 500 from the authorization and token endpoints, which is the reader's failure rather than the writer's. It resolves to Abblix.Oidc.Server.Features.ClientInformation.SecurityProfileRequirements.StrictestRequirements instead: nothing here can say what the value meant, and of the answers available only the strictest cannot quietly serve a client the deployment believed was constrained.