Maxime Rossini
Maxime Rossini

Reputation: 3881

Set cookie from Web Api 2 IAuthenticationFilter AuthenticateAsync method

Using Web Api 2.2, I have a custom IAuthenticationFilter that I use for authenticating client requests with a custom scheme.

Basically, when a client is not authenticated and wants to access a protected resource, he sends an Authorization header: Authorization: MyCustomScheme XXXXXXX alongside the request. The filter then validates the credentials, authenticates the user and generates a stateless authentication token for further access (similar to a JWT).

I would like to store the resulting authentication token in a cookie. When present in incoming requests, the cookie is locally validated in a separate filter (which is not presented here).

My problem is that if I try to set the cookie like this:

Task IAuthenticationFilter.AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
    if (context.Request.Headers.Authorization != null &&
        string.Equals(context.Request.Headers.Authorization.Scheme, "MyCustomScheme", StringComparison.OrdinalIgnoreCase))
    {
        // This works
        CustomPrincipal principal = this.ValidateCredentials(context.Request.Headers.Authorization.Parameter);
        context.Principal = principal;

        // This doesn't work: context.ActionContext.Response is null
        var cookie = new CookieHeaderValue("MySessionCookie", principal.AuthenticationToken) { Path = "/", HttpOnly = true };
        context.ActionContext.Response.Headers.AddCookies(new CookieHeaderValue[] { cookie });
    }
    return Task.FromResult(0);
}

Then it fails because context.ActionContext.Response is null. How do I add a cookie to the response from within AuthenticateAsync?

See related: Setting Cookie values in HttpAuthenticationContext for IAuthenticationFilter (you can see in the comments that people experience the same issue).

Upvotes: 3

Views: 3011

Answers (3)

Hugh Jeffner
Hugh Jeffner

Reputation: 2946

My requirements were to add a header but it should be easy to adapt to add a cookie.

I took a different approach on this. I put the header I wanted to add into context.Request.Properties. Then in ChallengeAsync (it gets called for every request regardless) via a IHttpActionResult I check for the presence of the property and if it exists add it to the headers. Something like this:

protected class AddRenewOnAauthorizedResult : IHttpActionResult {

    public const string RenewalPropertyKey = "ETicket.RenewalKey";

    public AddRenewOnAauthorizedResult(HttpRequestMessage request, IHttpActionResult innerResult) {
        this.Request = request;
        this.InnerResult = innerResult;
    }

    public HttpRequestMessage Request { get; set; }
    public IHttpActionResult InnerResult { get; set; }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) {

        HttpResponseMessage response = await this.InnerResult.ExecuteAsync(cancellationToken);

        if (Request.Properties.ContainsKey(RenewalPropertyKey)) Request.response.Headers.Add("X-ETicket-Renew", Request.Properties(RenewalPropertyKey));

        Return response;

}

}

Then in ChallengeAsync:

public Threading.Tasks.Task ChallengeAsync(HttpAuthenticationChallengeContext context, Threading.CancellationToken cancellationToken)
{

    context.Result = new AddRenewOnAauthorizedResult(context.Request, context.Result);
    return Task.FromResult(0);

}

Upvotes: 2

Maxime Rossini
Maxime Rossini

Reputation: 3881

I got the filter to work by implementing IActionFilter in addition to IAuthenticationFilter. This method is valid because you get access to the request, the response and the user identity in the same place. This is my implementation:

async Task<HttpResponseMessage> IActionFilter.ExecuteActionFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
    // Process the request pipeline and get the response (this causes the action to be executed)
    HttpResponseMessage response = await continuation();

    // If the user is authenticated and the token is not present in the request cookies, then it needs to be set
    CustomPrincipal principal = actionContext.ControllerContext.RequestContext.Principal as CustomPrincipal;
    if (principal != null && !actionContext.Request.Headers.GetCookies("MySessionCookie").Any())
    {
        // Set the cookie in the response
        var cookie = new CookieHeaderValue("MySessionCookie", principal.AuthenticationToken) { Path = "/", HttpOnly = true };
        response.Headers.AddCookies(new CookieHeaderValue[] { cookie });
    }

    return response;
}

I find this method very unpractical (mixing interfaces), you should definitely have access to the response in IAuthenticationFilter.AuthenticateAsync (via an async continuation callback for exemple, or by being able to access the action result (IHttpActionResult) in the context, like in the ChallengeAsync method of the same interface).

Upvotes: 1

Mark Redman
Mark Redman

Reputation: 24515

You may need to implement IRequiresSessionState to have cookies persist?

See: http://www.strathweb.com/2012/11/adding-session-support-to-asp-net-web-api/

Upvotes: 0

Related Questions