Aurelius
Aurelius

Reputation: 112

Adding Authentication Error Handling for expired Token

I am using graphQl Hot Chocolate V11 with .net Core 3.1 I have no problem with the identifying of token expire its just the relaying of that problem back to the requester is the problem.

i am trying to add some Authentication to my Requests but i am having an issue with responding when the authorization token is no longer valid due to the time expiring or even any other potential reason for a token to not be valid for that matter. enter image description here

enter image description here but when i throw an exception to try tell the requester that their token has expired it is not returning through the Hot Chocolate IErrorFilter style it comes through as like a server error. enter image description here if there is any better built in way to check these things and respond to the requester propely could anybody please help me out? i would morse think the error should be displayed like in the format of the last screenshot i guess as a Hot Chocolate IErrorFilter response (the error in that screenshot is if i dont properly handle when a user is not authenticated seen as i dont have a currentUser to add to the context that the query is expecting) enter image description here

Upvotes: 0

Views: 1597

Answers (3)

When a JWT token expires, the default auth error object does not provide a clear or specific error. To address this, I implemented a solution that captures authentication failures during the JWT validation process and saves the error in the HTTP context. Later, a custom error filter inspects GraphQL errors and checks for these saved authentication failures to provide a more specific error message. Here’s how I implemented the solution:

  1. Configure JWT Bearer Authentication: Add custom behavior to detect token expiration during authentication, then save it to the HTTP context's data.
  2. Implement Error Filtering: Intercept GraphQL AUTH_NOT_AUTHENTICATED errors, then check if there was a prior authentication error via HttpContextAccessor. This allows us to provide a specific error message, especially when the token is expired.

Program.cs

...
builder.Services.AddHttpContextAccessor();
builder.Services.ConfigureOptions<ConfigureJwtBearerOptions>();
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer();

builder.Services.AddGraphQLServer()
    .AddErrorFilter<ErrorFilter>();
...

ConfigureJwtBearerOptions.cs

public class ConfigureJwtBearerOptions : IConfigureNamedOptions<JwtBearerOptions>
{
    private readonly JwtAuthOptions _options;

    public ConfigureJwtBearerOptions(IOptions<JwtAuthOptions> options)
    {
        _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
    }

    public void Configure(JwtBearerOptions options) => configureJwtBearerOptions(options);

    public void Configure(string? name, JwtBearerOptions options) => configureJwtBearerOptions(options);

    private void configureJwtBearerOptions(JwtBearerOptions options)
    {
        options.RequireHttpsMetadata = false;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = _options.Issuer,
            ValidAudience = _options.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Secret!)),
            ClockSkew = TimeSpan.Zero
        };
        
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                // Intercepts JWT auth failure. Save to HTTP context
                context.HttpContext.Items["JwtAuthenticationFailure"] = context.Exception;
                return Task.CompletedTask;
            }
        };
    }
}

ErrorFilter.cs

public class ErrorFilter : IErrorFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ErrorFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public IError OnError(IError error)
    {
        // Now we can conditionally show token expired error
        if (error.Code == "AUTH_NOT_AUTHENTICATED" && 
            _httpContextAccessor.HttpContext?.Items["JwtAuthenticationFailure"] is SecurityTokenExpiredException)
        {
            return ErrorBuilder.New()
                .SetCode("AUTH_TOKEN_EXPIRED")
                .SetMessage("Access token already expired")
                .Build();
        }
        return error;
    }
}

Related github issue: https://github.com/ChilliCream/graphql-platform/issues/2960

Upvotes: 0

keithl8041
keithl8041

Reputation: 2403

I don't know whether this will fix your issue but it might be an acceptable workaround and have a different outcome. Use this to enforce valid is-logged-in security policy:

            // Add policy in Startup.cs
            services.AddAuthorization(options =>
            {
                options.AddPolicy("LoggedInPolicy", Policies.LoggedInPolicy);
            });

then add a Policies class

    public class Policies
    {
        /// <summary>
        /// Requires the user to be logged in
        /// </summary>
        /// <param name="policy"></param>
        public static void MultiFactorAuthenticationPolicy(AuthorizationPolicyBuilder policy)
        {
            policy.RequireAuthenticatedUser();
        }
    }

And then decorate the appropriate endpoints with your authorization attribute to apply the policy

        /// <summary>
        /// Test 'hello world' endpoint
        /// </summary>
        /// <returns>The current date/time on the server</returns>
        [Authorize(Policy = "LoggedInPolicy")]
        public string Hello()
        {
            return DateTime.Now.ToString("O");
        }

You may also have to add this to the GraphQL configuration

SchemaBuilder.New()
  ...
  .AddAuthorizeDirectiveType()
  ...
  .Create();

(from https://chillicream.com/docs/hotchocolate/v10/security/, don't know how much of this applies to v11)

That might connect with some different error handlers.

Upvotes: 0

Aurelius
Aurelius

Reputation: 112

The only thing that semi worked was creating my exception like this it allowed me to add a proper error code but still deoesnt return as an answer to the query

throw new GraphQLRequestException(ErrorBuilder.New()
                                .SetMessage(ExpiredTokenString)
                                .SetCode(ExpiredTokenCode)
                                .Build());

Upvotes: 0

Related Questions