Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

User login not being authenticated using OWIN bearer tokens

I'm working with an MVC app. The login is handle by Web API.

The issue I'm facing is when I click on Login button in the MVC app the API is generating the token along with the user info. However I can't display the user information in the view.

The following code is in the AccountController of MVC App

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model)
{
   string token = Token.GetToken(Token.GRANT_TYPE, model.Email, model.Password, Token.CLIENT_ID, Token.CLIENT_SECRET);

  //token has the appropriate data if I debug it

   if (!string.IsNullOrEmpty(token))
       return RedirectToAction("Index", "Home");

   return View(model);
}

Then in the View I have

@if (Request.IsAuthenticated)
{
  <p>@User.Identity.GetUserName()</p> //this line is always blank
}

The API looks like this

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{

        var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");

        if (allowedOrigin == null) allowedOrigin = "*";

        context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });

        using (AuthRepository _repo = new AuthRepository())
        {
            ApplicationUser user = await _repo.FindUser(context.UserName, context.Password);

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

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);

        identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
        identity.AddClaim(new Claim("sub", context.UserName));

        var props = new AuthenticationProperties(new Dictionary<string, string>
            {
                {
                    "as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
                },
                {
                    "userName", context.UserName
                },
                {
                    "email", context.UserName
                },
            });

        var ticket = new AuthenticationTicket(identity, props);
        context.Validated(ticket);
}

Can someone tell me why the request is not being Authenticated? What am I missing

like image 823
Izzy Avatar asked Jul 19 '16 10:07

Izzy


2 Answers

Some things that seem wrong to me from your example


Part I: Your Login Method

Convention is to use the GrantResourceOwnerCredentials as an endpoint (/api/token) which receives credentials and returns Access Token.

By convention, this endpoint replaces the Login(LoginViewModel) Method. Clearly you should not be using the Login() method, but I suspect you are.

  • If you are using the Login method, it only returns your original, unaltered View Model with Credentials. Your LoginViewModel class may have a token property in it, but you are not populating it. So, the Login Method authenticates your user, but doesn't authorize them because the user will never get their Access Token.
  • Your GrantResourceOwnerCredentials Method does return a Token and populates Claims. But do you have any client logic to store the access token and send it in each request?
  • Unless your Token is in a format like JWT (Json Web Tokens), I suspect it would be a major challenge to un-encrypt the token to access the claims (e.g. userName / email). Meaning, it wouldn't be easy for your client to extract the claims from the Access Token unless it was JWT.

Part II: Can someone tell me why the request is not being Authenticated?

You ARE being authenticated. But this isn't Forms Authentication. There is no Cookie. Razor Calls to Identity will not work when an API returns Access Tokens.

In your typical Forms Auth model:

credentials are sent and validated (Authenticated). If Validation is successful, the user is Authorized (given access) with a client side cookie that has encrypted access information and claims. The OWIN Middle-ware reads the Cookie on each request and populates the httpContext (Identity is a property on the httpContext object.)

Access Token work flow:

is very similar, except, since there is no Cookie, and no connection between the API and the client View other than the data returned in View Models. The httpContext will not be populated except on the server side (Web API), each time the OWIN Middle-ware reads the access Token.

So, a Razor call to

@User.Identity.GetUserName()

Will come up empty.


Solution: Getting and Storing User Info on the client

If you want user information from the Server, you should cache the username (and any other info you need) on the client.

Ways to store user info on client browser:

  • Session storage
  • Local Storage
  • JavaScript singleton class (like an Angular Service)

Ways to get username

  • Store the username you get when user enters credentials (clear it if Authentication fails)

  • Have a dedicated end point (API Method) to return the user info you want

  • Return claims in JWT's.

If you change your Token Type (in Global.asax or Startup.cs -- depending on how you have configured) to use Jason Web Tokens, the client will be able to un-encode your token and read the claims as JSON Array.


  • A last option: You can use both Access Tokens AND Cookies. You can return an Access Token and create a cookie. Suppress cookie auth for Web API where ever you configure Web API.

  • A final thought: Access Tokens are used for an extra level of security (cookies are more vulnerable). But what is your purpose in using Access Tokens? Such security is more important if your are creating a SPA (Single Page App) like with AngularJS. Otherwise, Cookie Auth is a much simpler work flow, and, unless you feel you need the extra security, cookies are preferable.

like image 129
Dave Alperovich Avatar answered Sep 19 '22 23:09

Dave Alperovich


The token was obtained but the principal wasn't loaded. You'll have to load the principal onto the thread some time before the controller instantiates. If you try to set it IN the controller, the new data wont be reflected, since controller data and the view are rendered at the same time, and that includes the principal.

Since there's a redirect happening, you could use that as an opportunity to load it. In order for that to happen, you'll have to save the token either to a cookie or to the session so that it'll be available on the next request (the redirect) keeping in mind that a response has to make it back to the browser in order for it to actually store the cookie, then the followup redirect will include the cookie that was just saved. Then just plug into the pipeline somewhere before controller resolution (like in a custom http module), pull it back out, use it to build the principal and set it on the thread and it'll be present for the duration of the request.

I'd also suggest switching over to using JWT tokens. Once you learn how to make them, they're much more usable and contain more useful info... well anything you place in the claims and they deserialize directly into a ClaimsIdentity.

Alternatively, you might be able to simply save the token to an auth ticket cookie and configure OWIN to pick that up on the MVC side and build the principal for you... basically do everything I just suggested for you. I'm not that experienced with it, so I'm not sure if that would work.

Here is what the basic flow might look like. The red part is the part I think you're missing. The orange part is also missing but you can either do it by hand via custom HttpModule or configure OWIN to do it for you (i think).

enter image description here

like image 42
Sinaesthetic Avatar answered Sep 21 '22 23:09

Sinaesthetic