Am I using CustomCredentialsAuthProvider and ServiceRunner the wrong way

I am currently using:

 public class CustomCredentialsAuthProvider : CredentialsAuthProvider
 {...
        public override async Task<object> AuthenticateAsync(IServiceBase authService, IAuthSession session, Authenticate request, CancellationToken token = default)
        {
          ...
            return await AuthenticateAsync(authService, session, request.UserName, request.Password, returnUrl, token).ConfigAwait();
        }
}

And I also implement(it is registered as it should be)

public class DefaultServiceRunner<T> : ServiceRunner<T>
{...
    public override async Task<object> HandleExceptionAsync(IRequest request, T requestDto, Exception ex, object service)
    {
         ...
    }
}

My initial expectation is that the HandleExceptionAsync is called every time the error happens.

Now my implementation works as it should as long as someone does not call it with header

Accept = 'text/html'

and one of specific:

  • headers
  • query strings
  • cookie values
    (one of those values is "ReturnUrl").

In this case the function HandleExceptionAsync is not called.

I would like to handle all exceptions on my own because I wrap all the responses in my own response object with Error property, translations, error codes, metadata…

Additionally, I log audits for specific exceptions inside this method. So because it is not called I am missing those logs.

So, I’m looking for suggestions on the best approach to ensure all exceptions go through my custom logic. Did I choose the right strategy, or is there a better way to achieve this?

There are a lot more than Service (API) Requests in ServiceStack and any handler in the Request Pipeline can short circuit requests, ending further processing.

Overriding void OnExceptionTypeFilter(Exception ex, ResponseStatus responseStatus) on your AppHost would cover the largest surface area, basically anything returning an Error ResponseStatus. You can mutate the structured ResponseStatus here, i.e. you can’t replace it, also note that all ServiceStack clients rely on structured error information in the ResponseStatus, you’ll want to be careful returning a different wire format.

We would like to have tighter control over the error responses returned to the user, primarily for security reasons.

Currently, we’ve observed that when a user enters an incorrect username/password, and when a user account becomes locked (after too many failed attempts), the system returns different response.

From a security perspective, this behavior is problematic, because an attacker who understands that an account is locked after a certain number of failed attempts can extract extra information from the system:

  1. User existence enumeration
    By observing when the error changes from “wrong username/password” to “account locked,” an attacker can infer whether a user with a given username actually exists.
  2. Targeted password guessing on valid accounts
    Once the attacker knows a username is valid (because it eventually gets locked), they can focus password-guessing attempts on real accounts.

Individually, the account lockout itself might seem protective (since the account becomes locked), but in practice this can still create a vulnerability. Many users reuse the same or similar passwords across different systems. Learning that a given username is valid on our system, and that the password was repeatedly guessed, may help an attacker in compromising the same user on other services.

For these reasons, we want to centralize and control the error information returned to the user, in particular:

  • Avoid exposing whether a specific username exists or is locked.
  • Ensure error responses are uniform and do not leak additional security-relevant details.

Our main concern is to prevent error messages from revealing subtle differences that attackers can use to infer account state or user existence. However the functionality of locking a user is very important to us(prevents the brute force attack for known account).

The only info returned in Error responses is within the populated Response Status so OnExceptionTypeFilter() could do what you need since you can change everything in it.

But more likely you should use a local copy of CredentialsAuthProvider.cs so you can control it’s behavior, i.e. not throw a different Exception when the user is locked.

Ok I tried suggested solution, but unfortunately I could not caught inside OnExceptionTypeFilter()

So I look a bit deeper into it and just for example I use a CredentialsAuthProvider.cs for authentication. Again I make a call as described previously.

Now when I debug the flow I see that after a

await AuthenticateAsync(authService, session, request.UserName, request.Password, authService.Request.GetReturnUrl(), token)
                .ConfigAwait()

is called it throws

HttpError.Unauthorized(ErrorMessages.InvalidUsernameOrPassword)

The first class that catches this exception seems to be AuthenticateService inside of function:

AuthenticateService.PostAsync(Authenticate request)

At that point there you have a variable isHttp and if it is set to true with expresion:

var isHtml = base.Request.ResponseContentType.MatchesContentType(MimeTypes.Html);

you might handle the exception by just redirecton:
return HttpResult.Redirect(errorReferrerUrl);

from my perspective I believe this is the reason why I do not catch this exception inside OnExceptionTypeFilter or any other exception handler function you provide.

So my question is what would be the best way to catch all Unauthorized accesses(possibly with all the other exceptions)?

It’s still as I said, OnExceptionTypeFilter() has the widest coverage.

You’re referring to built-in behavior of failed authentication requests from HTML Forms redirecting back to the originating web page.

Returning a JSON Response would break the Auth web flow. Either way as it’s only returning a HTTP Redirect, there’s no error response to scrub.

Also note your Credential Auth Provider can control what Responses are returned by overriding it’s AuthenticateAsync(IServiceBase authService, IAuthSession session, Authenticate request, CancellationToken token=default). You can override and intercept the response or Exception from the base method to return something different.