1
votes

I have an interesting problem that I haven't encountered before.

My front end and my api projects are running on different origins (so need CORS). My application will send username and password to the OWIN middleware for validation and returning a token. It DOES return a token after validating.

The weird part is that all my requests from there to any WebAPI endpoints with [Authorize] attribute return a 401 error.

For the Angular requests, I have an authorization interceptor that adds the "Bearer " Authorization header to every request. I've checked the token in that header against the token returned during log in and they match.

I've been banging my head against the wall for a few days now and think I just need to put this out there in the hopes that someone can point out something obvious to me.

2
You're working with identityServer3? - AranS
Did you put $httpProvider.defaults.withCredentials = true; in your app.js config? - Xnake
@Xnake Yeah, I did. I think I figured out the issue and answered myself below. I may put up a more detailed answer because I've seen questions like this asked so many times. - JakeHova

2 Answers

2
votes

I've seen a lot of these types of issues in other SO questions, so, in addition to solving my question (at the end of this post), I added how I set up a CORS Api project that leveraged: angular front end/.net web api using JWT bearer authentication and .NET Identity authorization. This is by no means an end all super secure way of doing things, but it will get you started:

1) On the Web API side, I setup my OWIN startup class by decorating the class with:

[assembly: OwinStartup(typeof(OwinTestApp.Api.App_Start.Startup))]

2) I then configured the server by ConfigureOAuthTokenGeneration, ConfigureOAuthTokenConsumption, ConfigureWebApi, and then told app use WebAPI using the configuration for WebApi.

 public void Configuration(IAppBuilder app)
    {

        HttpConfiguration httpConfig = new HttpConfiguration();

        ConfigureOAuthTokenGeneration(app);

        ConfigureOAuthTokenConsumption(app);

        ConfigureWebApi(httpConfig);

        app.UseWebApi(httpConfig);

    }

    private void ConfigureOAuthTokenConsumption(IAppBuilder app)
    {

        var issuer = "OwinTestApp";

//WebAPI server is serving as Resource and Authorization Server at the same time, so we are fixing the Audience Id and Audience Secret (Resource Server) in the web.config. If you separated your resource and authorization servers, then you would need to handle this differently


        string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
        byte[] audienceSecret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["as:AudienceSecret"]);

        app.UseJwtBearerAuthentication(
            new JwtBearerAuthenticationOptions
            {
                AuthenticationMode = AuthenticationMode.Active,
                AllowedAudiences = new[] { audienceId },
                IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
                {
                    new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
                }
            });
    }

    private void ConfigureOAuthTokenGeneration(IAppBuilder app)
    {
        // Configure the db context and user manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            //For Dev enviroment only (on production should be AllowInsecureHttp = false)
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/oauth/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(30),
            Provider = new AuthorizationServerProvider(),
            AccessTokenFormat = new CustomJwtFormat("OwinTestApp")
        };

        // OAuth 2.0 Bearer Access Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);
    }


    private void ConfigureWebApi(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();

        var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
        jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();            
    }

3) One of the keys here is configuring the provider in the OAuthServerOptions. Most of the SO questions I've seen on this topic and most tutorials I've seen on this have talked about using Allowing all origins (app.UseCors(CorsOptions.AllowAll)). Don't do this. It's fine for development but you more than likely want to lock down what origins your requests are coming from. And you do that in the when you set the Provider in the OAuthServerOptions.

  /// <summary>
/// To configure the Authorization server, you need to inherit OAuthAuthorizationServerProvider and override
/// certain methods of it to handle request authorizatio
/// </summary>
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    /// <summary>
    /// I'm validating the context here because I'm actually checking the user's credentials in the 
    /// GrantResourceOwnerCredentials method.  You can do a basic authentication check in this method
    /// if you're using a TryGetBasicCredentials with  client_id/client_secret properties
    /// </summary>
    /// <param name="context">The context of the event carries information in and results out.</param>
    /// <returns></returns>
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        context.Validated();
        return Task.FromResult<object>(null);
    }

    /// <summary>
    /// I'm using ASP.NET identity to authenticate my user so I am doing the actual grant in this method
    /// </summary>
    /// <param name="context">The context of the event carries information in and results out.</param>
    /// <returns></returns>
    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {  
        var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();

        ApplicationUser user = await userManager.FindAsync(context.UserName, context.Password);

        if (user == null)
        {
            context.SetError("invalid_grant", "The user name or password is incorrect.");
            return;
        }

        if (!user.EmailConfirmed)
        {
            context.SetError("invalid_grant", "User did not confirm email.");
            return;
        }

        ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, "JWT");

        //add static claims for the user
        oAuthIdentity.AddClaims(ExtendedClaimsProvider.GetClaims(user));
        //add any extra claims that may be added after the static claims have been added
        oAuthIdentity.AddClaims(RolesFromClaims.CreateRolesBasedOnClaims(oAuthIdentity));

        var ticket = new AuthenticationTicket(oAuthIdentity, null);

        context.Validated(ticket);

    }

    /// <summary>
    /// This is a key point to CORS.  CORS requests means that there is a pre-flight check (using an OPTIONS action)
    /// to see if the server allows access to this endpoint.
    /// </summary>
    /// <param name="context">Details of the request context</param>
    /// <returns></returns>
    public override Task MatchEndpoint(OAuthMatchEndpointContext context)
    {
        SetAllowedOrigins(context.OwinContext);
        //if this is a pre-flight request then indicate that the request completed and then
        //  return anything to indicate that the origin has access to this resource
        if (context.Request.Method == "OPTIONS")
        {
            context.RequestCompleted();
            return Task.FromResult(0);
        }

        //if its not a pre-flight request, then perform regular actions to match the endpoint 
        //  and authorization
        return base.MatchEndpoint(context);
    }


    /// <summary>
    /// add the allow-origin header only if the origin domain is found on the     
    /// allowedOrigin list
    /// </summary>
    /// <param name="context"></param>
    private void SetAllowedOrigins(IOwinContext context)
    {

        using (var db = new Data.Authorization.AuthDataContext()) {
            //origin gets the Origin of the request
            string origin = context.Request.Headers.Get("Origin");
            //check to see if the origin of the request is in your approved list of origins
            var allowedOrigin = db.OriginList.SingleOrDefault(a => a.Allowed && a.Active && a.Origin == origin);
            //if it is then add the Access-Control-Allow-Origin to your Response Header
            if (allowedOrigin != null) {
                context.Response.Headers.Add("Access-Control-Allow-Origin", new string[] { origin });
            }
        }
        //if this is an OPTIONS action request then add the "Access-Control-Allow-Headers" && "Access-Control-Allow-Methods"
        //      these are necessary headers to receive on the pre-flight request to validate access to the resource and also what 
        //      actions the user can make.  This occurs PRE execution of any method.
        if (context.Request.Method == "OPTIONS")
        {
            context.Response.Headers.Add("Access-Control-Allow-Headers", new string[] { "Authorization", "Content-Type", "Cache-Control" });
            context.Response.Headers.Add("Access-Control-Allow-Methods", new string[] { "OPTIONS", "GET", "POST" });

        }
        //add this to allow the user to send credentials (and log in)
        context.Response.Headers.Add("Access-Control-Allow-Credentials", new string[] { "true" });
    }
}

Lastly, to setup your client side, you need to do the following: 1) Set the $http provider to send credentials with the request:

  //need this for login to work.  token receipt wont work without this on there.  
$httpProvider.defaults.withCredentials = true;

2) Create an http interceptor to add the JWT bearer token to every request:

//handle the request
    function _request(config) {
        //grab the current headers of the request
        config.headers = config.headers || {};

        //get the token that was sent by the authorization process
        var token = //<Get token from wherever you saved it>;

        //if the token is not null then add an Authorization header 
        //  and set it's value to the token and suffix it with Bearer
        if (token) {
            config.headers.Authorization = 'Bearer ' + token;
        }

        return config;
    }

3) Lastly, add the http interceptor you created to the $http provider's interceptors list:

 //used to intercept calls and inject token after authorization has taken place
$httpProvider.interceptors.push('authInterceptorService');

I learned a LOT about this initially from bitoftech site. He uses allow all for CORS in the tutorial which you should not do in any cases unless you really want your API to be allowed access from all origins.

The issue I was having, was actually solved in my ConfigureWebAPI method. I forgot to Map my HTTP attibute routes, so I wasn't mapping my webapi methods to the [Authorize] decorations I was putting on them. (the line: config.MapHttpAttributeRoutes(); in that method).

Hopefully this helps other people out. There's a lot of other things you can do to secure and configure your OWIN server, but this should get you started with a relatively secure CORS api solution to start your development off of.

1
votes

In our application we embedded authentication in a login.aspx page. In particular, we use this mechanism in login.aspx.cs by using cookie authentication

                IAuthenticationManager authenticationManager = HttpContext.Current.GetOwinContext().Authentication;
                ClaimsIdentity userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie);
                user.LastLoginDate = DateTime.Now;
                userManager.Update(user);

                authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, userIdentity);
                Response.Redirect("~/YourPage/");