How to store bearer tokens when MVC and Web API are in different projects
Asked Answered
P

2

13

Situation: I have a Web API 2 project which acts as an Authorization server (/token endpoint) and a resource server. I am using the template that comes out of box with ASP.Net Web API minus any MVC reference. The Start.Auth is configured as below:

public void ConfigureAuth(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);

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            app.UseCookieAuthentication(new CookieAuthenticationOptions());
            app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

            // Configure the application for OAuth based flow
            PublicClientId = "self";
            OAuthOptions = new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/Token"),
                Provider = new ApplicationOAuthProvider(PublicClientId),
                AuthorizeEndpointPath = new PathString("/Account/ExternalLogin"),
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
                // In production mode set AllowInsecureHttp = false
                AllowInsecureHttp = true
            };

            // Enable the application to use bearer tokens to authenticate users
            app.UseOAuthBearerTokens(OAuthOptions);

            var facebookAuthenticationOptions = new FacebookAuthenticationOptions()
            {
                AppId = ConfigurationManager.AppSettings["Test_Facebook_AppId"],
                AppSecret = ConfigurationManager.AppSettings["Test_Facebook_AppSecret"],
                //SendAppSecretProof = true,
                Provider = new FacebookAuthenticationProvider
                {
                    OnAuthenticated = (context) =>
                    {
                        context.Identity.AddClaim(new System.Security.Claims.Claim("FacebookAccessToken", context.AccessToken));
                        return Task.FromResult(0);
                    }
                }
            };

            facebookAuthenticationOptions.Scope.Add("email user_about_me user_location");
            app.UseFacebookAuthentication(facebookAuthenticationOptions);

        }

The MVC 5 Client (different Project) uses the Web API app for authorization and data. Below is the code to retrieve the Bearer token in case of Username/Password store:

[HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            model.ExternalProviders = await GetExternalLogins(returnUrl);
            return View(model);
        }

        var client = Client.GetClient();

        var response = await client.PostAsync("Token", 
            new StringContent(string.Format("grant_type=password&username={0}&password={1}", model.Email, model.Password), Encoding.UTF8));

        if (response.IsSuccessStatusCode)
        {
            return RedirectToLocal(returnUrl);
        }
        return View();
    }

Problem

I could retrieve the Bearer token and then add it to the Authorization Header for subsequent calls. I think that would be ok in case of an Angular App or a SPA. But I think there should be something in MVC that handles it for me, like automatically store it in a cookie and send the cookie on subsequent requests. I have searched around quite a lot and there are posts which hint towards this (Registering Web API 2 external logins from multiple API clients with OWIN Identity) but I haven't been able to figure out what to do after I get a token.

Do I need to add something in the MVC app Startup.Auth?

Ideally, I need the functionality which the AccountController in ASP.Net Template (MVC + Web API) gives out of box (Logins, Register, External logins, forget password etc etc...) but with the MVC and Web API in different projects.

Is there a template or a git repo which has this boiler plate code?

Thanks in advance!

Update Incorporating @FrancisDucharme suggestions, below is the code for GrantResourceOwnerCredentials().

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;
            }

            ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager,
               OAuthDefaults.AuthenticationType);
            ClaimsIdentity cookiesIdentity = await user.GenerateUserIdentityAsync(userManager,
                CookieAuthenticationDefaults.AuthenticationType);

            AuthenticationProperties properties = CreateProperties(user.UserName);
            AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);

            //Add a response cookie...
            context.Response.Cookies.Append("Token", context.Options.AccessTokenFormat.Protect(ticket));


            context.Validated(ticket);
            context.Request.Context.Authentication.SignIn(cookiesIdentity);
        }

But I can't still seem to get that Cookie or figure out what to do next.

Restating Questions:

  1. What would be the correct way to authenticate, authorize and call Web API methods (Auth and Resource server) from an MVC client?
  2. Is there boilerplate code or template for AccountController which does the basic plumbing (Login, register - internal/external, forgot password etc.)?
Placard answered 8/1, 2016 at 10:26 Comment(6)
If you have your Web API return the token hash in the response cookies, the client will send back this cookie for all subsequent requests, assuming the client browser has cookies enabled.Covey
@FrancisDucharme could you elaborate this process please. I am using the standard token endpoint and config that comes out of web api template.Placard
Your main issue is that you want the MVC client to always add the Authorization: Bearer <hash> header automatically, right ?Covey
Yep, and I think the MVC way is to store it in a cookie which is sent on subsequent requests (I could be very wrong). I am sure I am missing something in the Startup.Auth for the MVC client. Right now, I don't have an auth configured in the Client.Placard
Do I need to do something extra once I get the token in response (in the MVC Login method - posted above)?Placard
This method makes you vulnerable to session hijacking and CSRF attacks unless you have some method of token verification meant to protect against this. This is especially dangerous if you use this same code in a non-browser context such as Cordova where cross domain requests are not restricted.Amygdaloid
C
1

You could have your Startup class return a response cookie that the client will then return on all subsequent requests, here's an example. I would do it in GrantResourceOwnerCredentials.

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{

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

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {                          

        //your authentication logic here, if it fails, do this...
        //context.SetError("invalid_grant", "The user name or password is incorrect.");
        //return;

         var identity = new ClaimsIdentity(context.Options.AuthenticationType);
         identity.AddClaim(new Claim("sub", context.UserName));
         identity.AddClaim(new Claim("role", "user"));

         AuthenticationTicket ticket = new AuthenticationTicket(identity);

        //Add a response cookie...
        context.Response.Cookies.Append("Token", context.Options.AccessTokenFormat.Protect(ticket));

        context.Validated(ticket);

}

The Startup class:

public partial class Startup
{

    public static OAuthBearerAuthenticationOptions OAuthBearerOptions { get; private set; }

    public Startup()
    {
        OAuthBearerOptions = new OAuthBearerAuthenticationOptions();
    }

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

        ConfigureOAuth(app);
        //I use CORS in my projects....
        app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
        app.UseWebApi(config);

        WebApiConfig.Register(config);

    }

    public void ConfigureOAuth(IAppBuilder app)
    {
        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true, //I have this here for testing purpose, production should always only accept HTTPS encrypted traffic.
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
            Provider = new AuthorizationServerProvider()
        };

        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        app.UseOAuthBearerAuthentication(OAuthBearerOptions);

    }
}

That assumes the client has cookies enabled, of course.

Then, modify your MVC headers to add the Authorization header to all requests as such.

In the ActionFilterAttribute, fetch your cookie value (Token) and add the header.

Covey answered 8/1, 2016 at 16:8 Comment(4)
Thanks @FrancisDucharme for the detailed explanation. I am sort of new to oAuth. Let me assimilate all this info and I'll get back once I get this to work on my setup :)Placard
I have updated the question with your suggestions. I am not able to get that cookie in the browser, maybe because I am using HttpClient (please excuse my limited understanding of the concept). Do you think this is the right way to go? I have updated the questions to get an understanding about the correct way to implement the whole handshake.Placard
@AmanvirSinghMundra I'm sorry, I don't have much experience with ASP MVC client side. What's Client exactly ? Check in Chrome network if you get the cookie back in the response headers when POSTing to /token.Covey
ASP.Net MVC 5 is the client app. Checked in chrome network tab, and the cookie doesn't show up! But thanks for the help though. I'll dig more.Placard
B
0

Instead of storing in session, I have added it to the the DefaultRequestHeaders as shown below so I don't need to add this in every call I make to Web API.

    public async Task AuthenticateUser(string username, string password)
    {
        var data = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("grant_type", "password"),
            new KeyValuePair<string, string>("username", username),
            new KeyValuePair<string, string>("password", password)
        });

        using (HttpResponseMessage response = await APIClient.PostAsync("/Token", data))
        {
            if (response.IsSuccessStatusCode)
            {
                var result = await response.Content.ReadAsAsync<AuthenticatedUser>();
                APIClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.Access_Token);
            }
            else
            {
                throw new Exception(response.ReasonPhrase);
            }
        }
    }
Bey answered 19/9, 2020 at 7:14 Comment(1)
Please add code and data as text (using code formatting), not images. Images: A) don't allow us to copy-&-paste the code/errors/data for testing; B) don't permit searching based on the code/error/data contents; and many more reasons. Images should only be used, in addition to text in code format, if having the image adds something significant that is not conveyed by just the text code/error/data.Inextricable

© 2022 - 2024 — McMap. All rights reserved.