Class DapperBackChannelAuthenticationRequestStore
- Namespace
- Fdyay.IdentityServer.Dapper.Stores
- Assembly
- Fdyay.IdentityServer.Dapper.dll
Dapper-backed store for pending backchannel authentication requests.
public sealed class DapperBackChannelAuthenticationRequestStore : IBackChannelAuthenticationRequestStore
- Inheritance
-
DapperBackChannelAuthenticationRequestStore
- Implements
- Inherited Members
Remarks
The store a multi-instance deployment needs, and CIBA needs it more than the device flow does. A device authorization is answered by the person holding the device, in a browser they opened for the purpose; a CIBA request arrives with nobody in front of it and is answered whenever that person next reaches an approval surface — on whichever instance the balancer gives them, minutes or hours later, while the client polls whichever instance it gets. With the in-memory default those are two different dictionaries, so a request approved on one instance is invisible to the poll on another and the client waits until its expiry for an answer that already exists.
Two single-record keys, and one question the device store is never asked. The client polls
by auth_req_id and the approval surface works in the internal id; a surface that shows somebody what
is waiting for them asks by subject, which is a query rather than a lookup, and the reason SubjectId and
IsComplete are columns instead of living only inside the serialised payload. Answering
FindPendingForSubjectAsync by deserialising every row in the table would work at the scale of a test
and not at the scale of a deployment.
Expiry is filtered on read as well as swept on a schedule, exactly as DapperDeviceFlowStore does it: a lapsed request stops being approvable at its expiry rather than whenever cleanup next runs.
Since 0.177.0 the auth_req_id is stored hashed and the request data-protected, as Duende's
DefaultBackChannelAuthenticationRequestStore stores its ciba grant (measured: the key a SHA-256 of the handle,
the data through IPersistentGrantSerializer, which protects it). The id is a bearer credential for the tokens, and
the data carries it, the client's notification token and the approving user's claims; until then all three were in the
table in the clear. A row written before is still read — by its unhashed key, and through the protector's plaintext
fallback — so nothing needs migrating, and such a row lives only for a request's lifetime.
Constructors
DapperBackChannelAuthenticationRequestStore(IDbConnectionFactory, TimeProvider)
Creates a backchannel authentication request store over the BackChannelAuthenticationRequests table.
public DapperBackChannelAuthenticationRequestStore(IDbConnectionFactory connectionFactory, TimeProvider time)
Parameters
connectionFactoryIDbConnectionFactoryOpens a connection for each operation and supplies the dialect's upsert.
timeTimeProviderThe clock reads compare a row's expiry against.
Remarks
Writes each request's data unprotected, as before 0.177.0; the container builds the overload taking a protector.
DapperBackChannelAuthenticationRequestStore(IDbConnectionFactory, TimeProvider, IGrantDataProtector)
Creates the store, protecting each request's data with protector.
public DapperBackChannelAuthenticationRequestStore(IDbConnectionFactory connectionFactory, TimeProvider time, IGrantDataProtector protector)
Parameters
connectionFactoryIDbConnectionFactoryOpens a connection for each operation and supplies the dialect's upsert.
timeTimeProviderThe clock reads compare a row's expiry against.
protectorIGrantDataProtectorProtects the
Datacolumn as it does a persisted grant's, underPersistentGrants.DataProtectData, and reads a row written unprotected as it was written.
Methods
FindByAuthenticationRequestIdAsync(string, CancellationToken)
Finds the request a client is polling for.
public Task<BackChannelAuthenticationRequest?> FindByAuthenticationRequestIdAsync(string authenticationRequestId, CancellationToken cancellationToken = default)
Parameters
authenticationRequestIdstringcancellationTokenCancellationToken
Returns
Remarks
Reads the BackChannelAuthenticationRequests row whose AuthenticationRequestId is the id's hash, or the
id itself for a row written before 0.177.0. A miss
returns null, and so does a row whose ExpiresAtUtc has passed, whether or not the sweep has removed it yet.
Exceptions
- ArgumentException
authenticationRequestIdis null or empty.
FindByInternalIdAsync(string, CancellationToken)
Finds the request the approval surface is acting on.
public Task<BackChannelAuthenticationRequest?> FindByInternalIdAsync(string internalId, CancellationToken cancellationToken = default)
Parameters
internalIdstringcancellationTokenCancellationToken
Returns
Remarks
Reads the BackChannelAuthenticationRequests row whose InternalId matches. A miss returns null, and
so does a row whose ExpiresAtUtc has passed, whether or not the sweep has removed it yet.
Exceptions
- ArgumentException
internalIdis null or empty.
FindPendingForSubjectAsync(string, CancellationToken)
The requests still waiting on an answer from this subject.
public Task<IReadOnlyCollection<BackChannelAuthenticationRequest>> FindPendingForSubjectAsync(string subjectId, CancellationToken cancellationToken = default)
Parameters
subjectIdstringcancellationTokenCancellationToken
Returns
Remarks
Both conditions are the query's, not the caller's: a request the subject has already answered is not waiting on them, and one past its expiry cannot be answered at all. Leaving either to the caller would put an approval surface in the position of showing somebody a decision they have already made.
RemoveByInternalIdAsync(string, CancellationToken)
Removes a request under both keys.
public Task RemoveByInternalIdAsync(string internalId, CancellationToken cancellationToken = default)
Parameters
internalIdstringcancellationTokenCancellationToken
Returns
Remarks
Deletes the BackChannelAuthenticationRequests row whose InternalId matches. A miss deletes nothing and is not an error.
Exceptions
- ArgumentException
internalIdis null or empty.
StoreAsync(string, BackChannelAuthenticationRequest, CancellationToken)
Stores a new request under both keys.
public Task StoreAsync(string authenticationRequestId, BackChannelAuthenticationRequest request, CancellationToken cancellationToken = default)
Parameters
authenticationRequestIdstringrequestBackChannelAuthenticationRequestcancellationTokenCancellationToken
Returns
Remarks
Upserts the BackChannelAuthenticationRequests row keyed by a hash of AuthenticationRequestId, with the
internal id, client id, subject id, completion flag and an ExpiresAtUtc of the request's creation time
plus its lifetime in seconds as columns and the request itself as JSON. An internal id already held by another
row violates the unique index on InternalId and the provider's exception propagates.
Exceptions
- ArgumentException
authenticationRequestIdis null or empty.- ArgumentNullException
requestis null.
UpdateByInternalIdAsync(string, BackChannelAuthenticationRequest, CancellationToken)
Replaces a request, by the key the approval surface holds.
public Task UpdateByInternalIdAsync(string internalId, BackChannelAuthenticationRequest request, CancellationToken cancellationToken = default)
Parameters
internalIdstringrequestBackChannelAuthenticationRequestcancellationTokenCancellationToken
Returns
Remarks
Looks up the AuthenticationRequestId of the BackChannelAuthenticationRequests row whose
InternalId matches and rewrites that whole row from request. When no row has that
internal id nothing is written and no error is raised. The lookup does not check expiry.
Exceptions
- ArgumentException
internalIdis null or empty.- ArgumentNullException
requestis null.