Authentication APIs can be found in the IAdvancedContentRepository
in the Users().Authentication()
method chain.
The AuthenticationSample project in the Cofoundry.Samples.UserAreas sample contains an example of authentication features.
Signing In
Single-step
Authentication and sign in can be completed in a single operation using SignInWithCredentialsAsync(SignInUserWithCredentialsCommand)
. When signing in using this method, any validation errors that occur will be thrown as ValidationErrorException
. This makes it more suitable when you do not need to take any action based on the type of error, or in a web API where the errors are sent directly to the client application.
await _advancedContentRepository
.Users()
.Authentication()
.SignInWithCredentialsAsync(new()
{
UserAreaCode = MemberUserArea.Code,
Username = "[email protected]",
Password = "ExamplePassword",
RememberUser = true
});
Multi-step
Often it will be more suitable to use a multi-step sign in process where we might:
- Authenticate the user with a query
- Identify if there's any special actions we need to take e.g. a password change requirement
- Sign in the user if the attempt is valid
This flow is more flexible and allows you to inject any custom requirements you have into the process.
// First authenticate the user without signing them in
var authResult = await _advancedContentRepository
.Users()
.Authentication()
.AuthenticateCredentials(new()
{
UserAreaCode = MemberUserArea.Code,
Username = "[email protected]",
Password = "ExamplePassword"
})
.ExecuteAsync();
// If credentials are not valid, throw
authResult.ThrowIfNotSuccess();
// If a special action is required
if (authResult.User.RequirePasswordChange)
{
// ... redirect etc
}
else if (!authResult.User.IsAccountVerified)
{
// ... redirect etc
}
// ... or additional custom validation
// If no action required: sign the user in
await _advancedContentRepository
.Users()
.Authentication()
.SignInAuthenticatedUserAsync(new()
{
UserId = authResult.User.UserId,
RememberUser = true
});
Validation Errors
The errors returned from the AuthenticateCredentials(AuthenticateUserCredentialsQuery)
are the same that are thrown from SignInWithCredentialsAsync(SignInUserWithCredentialsCommand)
. Each validation error has a unique code, and some use specific exception types that make them easier to catch if you prefer to use a try
/catch
approach:
- cf-user-auth-invalid-credentials: Either the username or password is invalid. Thrown as an
InvalidCredentialsAuthenticationException
- cf-user-auth-max-attempts-exceeded: Too many failed authentication attempts have occurred either for the username or IP address.
- cf-user-auth-not-specified: The error was not specified. This can be used when an error is picked up outside of the core authentication operation e.g. in MVC if the ModelState is invalid and the result is returned before authentication is attempted.
These errors will also be thrown when signing in a user that does not pass these additional checks:
- cf-user-auth-password-change-required: The credentials are valid but a password change is required before sign in is permitted. This error isn't expected to be shown to the user but is instead expected to be intercepted and handled in the UI. Thrown as a
PasswordChangeRequiredException
. - cf-user-auth-account-not-verified: The credentials are valid but the account has not been verified, and the user area is configured to not allow sign ins for unverified users. Thrown as an
AccountNotVerifiedException
.
Rate limiting
Credential authentication is rate limited to mitigate abuse. If the rate limit is exceed a ValidationErrorException
is thrown with the code "cf-users-auth-rate-limit-exceeded". Rate limiting can be configured via IUserAreaDefinition.ConfigureOptions(options)
:
options.Authentication.IPAddressRateLimit.Quantity
: The maximum number of failed authentication attempts allowed per IP address during the rate limit time window. The default value is 50 attempts.options.Authentication.IPAddressRateLimit.Window
: The time window to measure authentication attempts when rate limiting by IP address, specified as aTimeSpan
. The default value is 60 minutes.options.Authentication.UsernameRateLimit.Quantity
: The maximum number of failed authentication attempts allowed per username during the rate limiting time window. The default value is 20 attempts.options.Authentication.UsernameRateLimit.Window
: The time window to measure authentication attempts when rate limiting by username, specified as aTimeSpan
. The default value is 60 minutes. The default value is 60 minutes.
Execution duration
Credential authentication applies a "random duration" technique to mitigate timing-based enumeration attacks to discover valid usernames. This can be configured via IUserAreaDefinition.ConfigureOptions(options)
:
options.Authentication.ExecutionDuration.Enabled
: Controls whether the randomized execution duration feature is enabled. Defaults totrue
.options.Authentication.ExecutionDuration.MinInMilliseconds
: The inclusive lower bound of the randomized credential authorization execution duration, measured in milliseconds (1000ms = 1s). Defaults to 1 second.options.Authentication.ExecutionDuration.MaxInMilliseconds
: The inclusive upper bound of the randomized credential authorization execution duration, measured in milliseconds (2000ms = 2s). Defaults to 1.5 seconds.
Note that the minimum duration should exceed the expected duration of the authentication query, which will depend on the response times of your database and the hashing algorithm used.
Signing Out
To sign out of the current (ambient) user area:
await _advancedContentRepository
.Users()
.Authentication()
.SignOutAsync();
To sign out of all user areas (including the Cofoundry admin panel user area):
await _advancedContentRepository
.Users()
.Authentication()
.SignOutAllUserAreasAsync();
Forcing a password change
Users can be forced to change their password at sign in by setting the RequirePasswordChange
flag to true
when adding or updating a user. When this setting is true
then any attempt to sign into the account will throw a PasswordChangeRequiredException
validation exception. If you want to support forcing a password change in your sign in flow, the best approach is to use the multi-step sign in approach documented above and redirect these users to a password change form. Alternatively, if you are authenticating over a web API boundary you can handle the "cf-user-auth-password-change-required" error code in your front-end application.
Updating the password
To update the password, execute UpdateUserPasswordByCredentialsCommand
via the IAdvancedContentRepository
:
await _advancedContentRepository
.Users()
.UpdatePasswordByCredentialsAsync(new()
{
UserAreaCode = MemberUserArea.Code,
Username = "Example Username",
OldPassword = "ExampleOldPassword",
NewPassword = "ExampleNewPassword"
});
Because the user is prevented from signing in when a password change is required, they will need to provide sign in credentials again to make the change.