MVC POST requests losing Authorization header - how to use API Bearer Token once retrieved
Asked Answered
M

1

8

I have spent the last week creating an API for an existing MVC application, and am now attempting to secure the API along with reworking the MVC side security as needed.

Currently, the MVC application is set up to use an application cookie via OWIN/OAuth/Identity. I have attempted to incorporate the Bearer token that the Web API is set up to generate whenever making calls to restricted API methods, but have had little success so far - GET requests work just fine, but POST requests are losing the Authorization header when received by the API.

I have created an SDK Client that is being used by the MVC app to make the calls to the API, and have tried a total of three methods of setting the Authorization header for any given call to the API, all of which seem to work just fine for GET requests, but fail completely for any POST requests I need to make...

I can set the Request header in the MVC controller:

HttpContext.Request.Headers.Add("Authorization", "Bearer " + response.AccessToken);

(where response.AccessToken is the token previously retrieved from the API)
I can set the Request header via an extension method on the SDK Client:

_apiclient.SetBearerAuthentication(token.AccessToken)

or I can set the Request header manually on the SDK Client:

_apiClient.Authentication = new AuthenticationHeaderValue("Bearer, accessToken);

(Where accessToken is the token retrieved previously, passed to the Client method being called).

I have very little to go on from this point as to what is causing the issue. The only thing I have been able to glean so far is that ASP.NET causes all POST requests to first send in a request with an Expect header for an HTTP 100-Continue response, after which it will finish the actual POST request. However, it seems that when it does this second request, the Authorization header is no longer present and so the API's Authorize attribute will cause a 401-Unauthorized response instead of actually running the API method.

So, how do I take the Bearer token that I am able to retrieve from the API, and use it on subsequent requests, including the various POST requests that I will need to make?

Beyond that, what is the best way of storing this token on the MVC application itself? I would rather like to avoid having to pass around the string to every method in the application that could need it, but I also have been reading that storing it in a cookie is a very bad idea for security reasons.

A few further points that will be of interest immediately after I get passed this issue:

Does using OAuth Bearer Tokens mean that I can no longer use ApplicationCookies for the MVC application? And/or will it render the following code useless throughout the application?

User.Identity.GetUserId()

Currently I am forced into commenting out my API [Authorize] attributes in order to continue with my work, which obviously isn't ideal but it does allow me to get on with things temporarily.

Startup files:

MVC:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);
    }

    private void ConfigureAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ADUIdentityDbContext.Create);
        app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);
        
        app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
                                 {
                                     AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                                     //This should be set to FALSE before we move to production.
                                     AllowInsecureHttp =  true,
                                     ApplicationCanDisplayErrors = true,
                                     TokenEndpointPath = new PathString("/api/token"),

                                 });

        app.UseCookieAuthentication(new CookieAuthenticationOptions
                                    {
                                        AuthenticationType = DefaultAuthenticationTypes.ExternalBearer,
                                        CookieName = "ADU",
                                        ExpireTimeSpan = TimeSpan.FromHours(2),
                                        LoginPath = new PathString("/Account/Login"),
                                        SlidingExpiration = true,
                                        
                                    });
    }
}

API

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        HttpConfiguration config = new HttpConfiguration();

        config.DependencyResolver = new NinjectResolver(new Ninject.Web.Common.Bootstrapper().Kernel);

        WebApiConfig.Register(config);

        ConfigureOAuth(app);
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);

        app.UseWebApi(config);
    }

    public void ConfigureOAuth(IAppBuilder app)
    {
        app.CreatePerOwinContext(ADUIdentityDbContext.Create);
        app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);

        OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/api/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new SimpleAuthorizationServerProvider(),
        };

        //token generation
        app.UseOAuthAuthorizationServer(oAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
    }
}
public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    private IUserBusinessLogic _userBusinessLogic;
    
    /// <summary>
    /// Creates the objects necessary to initialize the user business logic field and initializes it, as this cannot be done by dependency injection in this case.
    /// </summary>
    public void CreateBusinessLogic()
    {
        IUserRepository userRepo = new UserRepository();
        IGeneratedExamRepository examRepo = new GeneratedExamRepository();
        IGeneratedExamBusinessLogic examBLL = new GeneratedExamBusinessLogic(examRepo);
        _userBusinessLogic = new UserBusinessLogic(userRepo, examBLL);
    }

    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { context.Validated(); }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

        //create a claim for the user
        ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType);
        identity.AddClaim(new Claim("sub", user.Id));
        context.Validated(identity);
    }
}
Marianomaribel answered 25/7, 2016 at 20:17 Comment(0)
M
2

After a good deal of time working on other aspects of the project, implementing other features has actually made solving this far easier - there is now a Response Wrapper Handler as part of the API, and part of that Handler saves all Headers from the incoming Requests and adds them to the outgoing Responses. I believe this is allowing the ASP.NET MVC side of the application to send the Authorization header again after the 200-OK request is initially sent.

I have modified my authentication in order to take advantage of Roles, but I will attempt to exclude that code as it should not be relevant here:

MVC Startup.cs:

    public class Startup
    {
        public void Configuration(IAppBuilder app) { ConfigureAuth(app); }

        /// <summary>
        ///     Configures authentication settings for OAuth.
        /// </summary>
        /// <param name="app"></param>
        private void ConfigureAuth(IAppBuilder app)
        {
            app.CreatePerOwinContext(ADUIdentityDbContext.Create);
            app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);

            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

            app.UseCookieAuthentication(new CookieAuthenticationOptions
                                        {
                                            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                                            CookieName = "ADU",
                                            ExpireTimeSpan = TimeSpan.FromHours(2),
                                            LoginPath = new PathString("/Account/Login"),
                                            SlidingExpiration = true
                                        });
        }
    }

Where it is used (AccountController):

    private async Task CreateLoginCookie(AuthorizationToken response, User result)
        {
            //Create the claims needed to log a user in 
            //(uses UserManager several layers down in the stack)
            ClaimsIdentity cookieIdent = await _clientSDK.CreateClaimsIdentityForUser(response.AccessToken, result, true).ConfigureAwait(false);

            if (cookieIdent == null) throw new NullReferenceException("Failed to create claims for cookie.");
            cookieIdent.AddClaim(new Claim("AuthToken", response.AccessToken));

            AuthenticationProperties authProperties = new AuthenticationProperties();
            authProperties.AllowRefresh = true;
            authProperties.IsPersistent = true;
            authProperties.IssuedUtc = DateTime.Now.ToUniversalTime();

            IOwinContext context = HttpContext.GetOwinContext();
            AuthenticateResult authContext = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);

            if (authContext != null)
                context.Authentication.AuthenticationResponseGrant = new AuthenticationResponseGrant(cookieIdent, authContext.Properties);

            //Wrapper methods for IOwinContext.Authentication.SignOut()/SignIn()
            SignOut();
            SignIn(authProperties, cookieIdent);
        }

In my SDK layer, I created a method that I call from the various other methods I use to reach my API in order to set the Authorization for each outgoing Request (I'd like to figure out how to make this into an Attribute, but I'll worry about that later):

    private void SetAuthentication()
        {
            ClaimsIdentity ident = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
            Claim claim;
            //Both of these methods (Thread.CurrentPrincipal, and ClaimsPrincipal.Current should work,
            //leaving both in for the sake of example.
            try
            {
                claim = ident.Claims.First(x => x.Type == "AuthToken");
            }
            catch (Exception)
            {
                claim = ClaimsPrincipal.Current.Claims.First(x => x.Type == "AuthToken");
            }
            
            _apiClient.SetBearerAuthentication(claim.Value);
        }

API Startup.cs

        /// <summary>
        ///     Configures the settings used by the framework on application start.  Dependency Resolver, OAuth, Routing, and CORS
        ///     are configured.
        /// </summary>
        /// <param name="app"></param>
        public void Configuration(IAppBuilder app)
        {
            HttpConfiguration config = new HttpConfiguration();

            config.DependencyResolver = new NinjectResolver(new Bootstrapper().Kernel);

            WebApiConfig.Register(config);

            ConfigureOAuth(app);
            app.UseCors(CorsOptions.AllowAll);

            app.UseWebApi(config);
        }

        /// <summary>
        ///     Configures authentication options for OAuth.
        /// </summary>
        /// <param name="app"></param>
        public void ConfigureOAuth(IAppBuilder app)
        {
            app.CreatePerOwinContext(ADUIdentityDbContext.Create);
            app.CreatePerOwinContext<ADUUserManager>(ADUUserManager.Create);

            OAuthAuthorizationServerOptions oAuthServerOptions = new OAuthAuthorizationServerOptions
                                                                 {
                                                                     AllowInsecureHttp = true,
                                                                     TokenEndpointPath = new PathString("/api/token"),
                                                                     AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                                                                     Provider = new SimpleAuthorizationServerProvider()
                                                                 };

            //token generation
            app.UseOAuthAuthorizationServer(oAuthServerOptions);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        }

SimpleAuthorizationServerProvider.cs:

    /// <summary>
        ///     Creates an access bearer token and applies custom login validation logic to prevent invalid login attempts.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });

            // Performs any login logic required, such as accessing Active Directory and password validation.
            User user = await CustomLoginLogic(context).ConfigureAwait(false);

            //If a use was not found, add an error if one has not been added yet
            if((user == null) && !context.HasError) SetInvalidGrantError(context);

            //Break if any errors have been set.
            if (context.HasError) return;

            //create a claim for the user
            ClaimsIdentity identity = new ClaimsIdentity(context.Options.AuthenticationType);

            //Add some basic information to the claim that will be used for the token.
            identity.AddClaim(new Claim("Id", user?.Id));
            identity.AddClaim(new Claim("TimeOf", DateTime.Now.ToShortDateString() + " " + DateTime.Now.ToLongTimeString()));

            //Roles auth
            SetRoleClaim(user, ref identity);

            context.Validated(identity);
        }

And finally, the apparent key that wraps everything up together:

    public class ResponseWrappingHandler : DelegatingHandler
    {
        /// <summary>
        /// Catches the request before processing is completed and wraps the resulting response in a consistent response wrapper depending on the response returned by the api.
        /// </summary>
        /// <param name="request">The request that is being processed.</param>
        /// <param name="cancellationToken">A cancellation token to cancel the processing of a request.</param>
        /// <returns></returns>
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

            //Calls Wrapping methods depending on conditions,
            //All of the Wrapping methods will make a call to PreserveHeaders()
        }

        /// <summary>
        /// Creates a response based on the provided request with the provided response's status code and request headers, and the provided response data.
        /// </summary>
        /// <param name="request">The original request.</param>
        /// <param name="response">The reqsponse that was generated.</param>
        /// <param name="responseData">The data to include in the wrapped response.</param>
        /// <returns></returns>
        private static HttpResponseMessage PreserveHeaders(HttpRequestMessage request, HttpResponseMessage response, object responseData)
        {
            HttpResponseMessage newResponse = request.CreateResponse(response.StatusCode, responseData);

            foreach (KeyValuePair<string, IEnumerable<string>> header in response.Headers)
                newResponse.Headers.Add(header.Key, header.Value);

            return newResponse;
        }

With all of that in place my project is now able to use authorization/authentication without needing client secrets and such (which was one of the goals of my employer).

Marianomaribel answered 18/8, 2016 at 16:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.