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

17,603

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.

Share:
17,603
Amanvir Mundra
Author by

Amanvir Mundra

Updated on July 01, 2022

Comments

  • Amanvir Mundra
    Amanvir Mundra almost 2 years

    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.)?
  • Amanvir Mundra
    Amanvir Mundra over 8 years
    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 :)
  • Amanvir Mundra
    Amanvir Mundra over 8 years
    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.
  • Francis Ducharme
    Francis Ducharme over 8 years
    @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.
  • Amanvir Mundra
    Amanvir Mundra over 8 years
    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.
  • Suraj Rao
    Suraj Rao over 3 years
    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.