Understanding Security Features in the SPA Template for VS2013 RC

Understanding Security Features in the SPA Template for VS2013 RC

Rate This
  • Comments 35

The blog is based on Visual Studio 2013 RC release.

We have completely rewritten the SPA template from the previous version( MVC 4). Here are some of the changes we made:

  • The authentication story has completely changed. Instead of using cookie (which was used in the last release), we are using OAuth with a bearer token and OWIN, which is more correct in Web API world.
  • A real single page app. We converted all the app pages into one web page and control the visibility of them by knockout.
  • Account controller are now pure Web API controller
  • Removed the todo samples and made the template generate boilerplate code.
  • Updated to bootstrap to modernize and simplify the CSS story.
  • Updated to the new Identity API.
  • We use attribute routes as default route scenario.

Note that SPA VB template is still not available in this RC release. We will ship it with RTM release.

In this blog, I will focus on the security features in SPA template.

  • Bearer token authentication with Web API.
  • How the bearer token is created:

Bearer Token Authentication with Web API

This is one of the most asked for features in Web API. In the SPA template, we implemented this feature with OWIN security middleware. In order to use OWIN bearer token middleware, you need to make sure this package is installed:

  • Microsoft.Owin.Security.OAuth

Here is the code to enable Bearer Token middleware:

Code Snippet
  1. static Startup()
  2. {
  3.     OAuthOptions = new OAuthAuthorizationServerOptions
  4.     {
  5.         TokenEndpointPath = "/Token",
  6.         AuthorizeEndpointPath = "/api/Account/ExternalLogin",
  7.         Provider = new ApplicationOAuthProvider(PublicClientId, IdentityManagerFactory, CookieOptions)
  8.     };
  9. }
  10.  
  11. // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
  12. public void ConfigureAuth(IAppBuilder app)
  13. {
  14.     // Enable the application to use bearer tokens to authenticate users
  15.     app.UseOAuthBearerTokens(OAuthOptions, ExternalOAuthAuthenticationType);
  16. }

The UseOAuthBearerTokens extension method actually enables 3 OWIN middleware components:

  1. Authorization Server middleware.
  2. Application bearer token middleware.
  3. External bearer token middleware.

Here is the pseudo-code for the extension method:

Code Snippet
  1. public static void UseOAuthBearerTokens(this IAppBuilder app, OAuthAuthorizationServerOptions options, string externalAuthenticationType)
  2. {
  3.     app.UseOAuthAuthorizationServer(options);
  4.     app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
  5.     {
  6.         AccessTokenFormat = options.AccessTokenFormat,
  7.         AccessTokenProvider = options.AccessTokenProvider,
  8.         AuthenticationMode = options.AuthenticationMode,
  9.         AuthenticationType = options.AuthenticationType,
  10.         Description = options.Description,
  11.         Provider = new AppBuilderExtensions.ApplicationOAuthBearerProvider(),
  12.         SystemClock = options.SystemClock
  13.     });
  14.     app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
  15.     {
  16.         AccessTokenFormat = options.AccessTokenFormat,
  17.         AccessTokenProvider = options.AccessTokenProvider,
  18.         AuthenticationMode = AuthenticationMode.Passive,
  19.         AuthenticationType = externalAuthenticationType,
  20.         Description = options.Description,
  21.         Provider = new AppBuilderExtensions.ExternalOAuthBearerProvider(),
  22.         SystemClock = options.SystemClock
  23.     });
  24. }
   

Authorization server will be responsible to create the bearer token and external bearer token is only used with external logins (Such as Facebook, Google, etc). I will explain them later in the subsequent sections.

After enabling bearer token authentication in active mode, OWIN middleware will authenticate every request with the “Authorization: Bearer” header.  By default, the application bearer token middleware is active.

However, there is still a problem with Web API. What happens if your application enables other authentications?  For example, the SPA template enables application cookie middleware as active mode as well in order to enable other scenarios like MVC authentication. So Web API will still be authenticated if the request has session cookie but without a bearer token. That’s probably not what you want as you would be venerable to CSRF attacks for your APIs. Another negative impact is that if request is unauthorized, both middleware components will apply challenges to it. The cookie middleware will alter the 401 response to a 302 to redirect to the login page. That is also not what you want in a Web API request.

The SPA template demonstrates how to solve this problem in Web API layer. Here is the code snippet from WebApiConfig.Register method:

Code Snippet
  1. // Web API configuration and services
  2. // Configure Web API to use only bearer token authentication.
  3. config.SuppressDefaultHostAuthentication();
  4. config.Filters.Add(new HostAuthenticationFilter(Startup.OAuthOptions.AuthenticationType));

 

SuppressDefaultHostAuthentication will register a message handler and set current principal to anonymous, so no host principal will get passed to Web API. It will also suppress default challenges from OWIN middleware.

HostAuthenticationFilter behavior is the opposite. It will set the principal from specified OWIN authentication middleware. In this case, it is the bearer token middleware, it will also send a challenge to specified middleware when it sees a 401 response. Since this authentication filter is set as global filter, it will apply to all Web APIs. So the result is that Web API will only see the authentication principal from the bearer token middleware and any 401 response from Web API will add a bearer challenge.

Note: the authentication filter is a new filter type in Web API v2. It happens after the message handler but before the AuthorizationAttribute filter. It makes it possible to let you specify different authentication methods at the action level.

After configuring the OWIN bearer token middleware and Web API host authentication settings, you can easily protect your Web API resources with the AuthorizeAttribute.

Your next question is probably how the bearer token is created in the SPA template. In order to conform with OAuth 2.0 spec, we try to use the OWIN Authorization server to create and send bearer tokens to the client in every scenario.  We plan to have a blog post for OWIN Authorization server to explain how it supports all OAuth 2.0 flows. Here I will only explain the authentication flows that are used in SPA template: Resource Owner Password Credentials Grant and Implicit Grant.

Password Login Flow

This flow happens when the user logs in by typing her user name and password in login form ( This is the most common authentication scenario).

In the SPA template, we use OAuth 2.0’s Resource Owner Password Credentials Grant flow for this scenario and implement it in the OWIN Authorization server.

In this flow, the browser sends a POST request with grant type, user name and password and server returns back an access token. For example, the browser sends:

POST http://localhost:47948/Token HTTP/1.1
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 46
Host: localhost:47948

grant_type=password&username=a&password=aaaaaa

The server responds:

HTTP/1.1 200 OK
Content-Length: 671
Content-Type: application/json;charset=UTF-8
Set-Cookie: .AspNet.Cookies=Ud0iQNZazLq-K8C; path=/; HttpOnly

{
"access_token":"YPo047a0sqJUmle6tkeKmIaRUS",
"token_type":"bearer",
"expires_in":1200,
"userName":"a",
".issued":"Thu, 19 Sep 2013 05:56:32 GMT",
".expires":"Thu, 19 Sep 2013 06:16:32 GMT"
}

In order to support the above password login flow, the  SPA template calls ApplicationOAuthProvider and exposes the token endpoint as “/Token”. (See OAuthAuthorizationServerOptions in the Startup.Auth.cs file).

Code Snippet
  1. public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
  2. {
  3.     using (IdentityManager identityManager = _identityManagerFactory.CreateStoreManager())
  4.     {
  5.         if (!await identityManager.Passwords.CheckPasswordAsync(context.UserName, context.Password))
  6.         {
  7.             context.SetError("invalid_grant", "The user name or password is incorrect.");
  8.             return;
  9.         }
  10.  
  11.         string userId = await identityManager.Logins.GetUserIdForLocalLoginAsync(context.UserName);
  12.         IEnumerable<Claim> claims = await GetClaimsAsync(identityManager, userId);
  13.         ClaimsIdentity oAuthIdentity = CreateIdentity(identityManager, claims,
  14.             context.Options.AuthenticationType);
  15.         ClaimsIdentity cookiesIdentity = CreateIdentity(identityManager, claims,
  16.             _cookieOptions.AuthenticationType);
  17.         AuthenticationProperties properties = await CreatePropertiesAsync(identityManager, userId);
  18.         AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);
  19.         context.Validated(ticket);
  20.         context.Request.Context.Authentication.SignIn(cookiesIdentity);
  21.     }
  22. }

What the code above does is quite straightforward:

  1. Checks the password with the Identity API .
  2. Create a user identity for the bearer token.
  3. Creates a user identity for the cookie.
  4. Calls the context.Validated(ticket) to tell the OAuthZ server to protect the ticket as an access token and send it out in JSON payload.
  5. Signs the cookie identity so it can send the authentication cookie.

 

External login flow (Microsoft, Facebook, Twitter and Google)

This flow is a little bit complicated compared with password flow.  The SPA template uses OAuth 2.0 implicit flow to convert an external sign in cookie to an access token and send it back to the browser by URL fragment. Here is a simplified flow diagram for a Facebook login:

image

  1. The User clicks the Facebook Login button and it will trigger a browser redirect to the authorization endpoint with a parameter response_type as token and redirect_uri as application URL. For example: http://localhost:47948/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=%2F&state=E2VRvHyopw%2BdYR5uKHbHAyYFmK06Pd%2Fw5LRWO243Tdk%3D
  2. The application knows that user wants to login as Facebook so it sends a challenge to the Facebook authentication middleware to modify the response as 302 and redirect browser to facebook.com. To enable an external login service, see Create an ASP.NET MVC 5 App with Facebook and Google OAuth2 and OpenID Sign-on or  External Authentication Service.
  3. The user signs in with her facebook account and which grants access to this application.
  4. Facebook sends a HTTP 302 back and redirects the browser back to Facebook callback URL on server, by default it’s “/signin-facebook”. You can change it with Facebook authentication options.
  5. The Facebook authentication middleware asks for access token and user data from facebook.com.
  6. The Facebook authentication middleware redirects the browser back to the authorization endpoint and converts the facebook data into claims and sets external sign in cookie.
  7. The user agents redirects to the authorization endpoint with the external cookie in the request.
  8. The authorization endpoint checks the external sign in cookie principal and finds the associated application user, then signs in the user as Bearer authentication type into the authorization server middleware. Since the authorization server sees that the request parameter response_type is token (in step 1), it will trigger implicit flow, which will create access the token and append it to the redirect_uri (step 1) as URL fragment. For example:

    HTTP/1.1 302 Found
    Cache-Control: no-cache
    Pragma: no-cache
    Expires: -1
    Location: /#access_token=asd2342SDIUKJdsfjk3234&token_type=bearer&expires_in=1200&state=06hwltIjvnTn44hc
    Set-Cookie: .AspNet.External=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT
    Set-Cookie: .AspNet.Cookies=WJgdyZQs9N8TG20EWnik-j0_; path=/; HttpOnly
    Content-Length: 0

 

The key logic here is in the authorization endpoint, which is “api/Account/ExternalLogin” in the SPA template (See OAuthAuthorizationServerOptions in the Startup.Auth.cs file).

Code Snippet
  1. // GET api/Account/ExternalLogin
  2. [OverrideAuthentication]
  3. [HostAuthentication(Startup.ExternalCookieAuthenticationType)]
  4. [AllowAnonymous]
  5. [HttpGet("ExternalLogin", RouteName = "ExternalLogin")]
  6. public async Task<IHttpActionResult> ExternalLogin(string provider)
  7. {
  8.     if (!User.Identity.IsAuthenticated)
  9.     {
  10.         return new ChallengeResult(provider, this);
  11.     }
  12.  
  13.     ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
  14.  
  15.     if (externalLogin == null)
  16.     {
  17.         return InternalServerError();
  18.     }
  19.  
  20.     string userId = await IdentityManager.Logins.GetUserIdForLoginAsync(externalLogin.LoginProvider,
  21.         externalLogin.ProviderKey);
  22.  
  23.     bool hasRegistered = userId != null;
  24.  
  25.     if (hasRegistered)
  26.     {
  27.         Authentication.SignOut(Startup.ExternalCookieAuthenticationType);
  28.         IEnumerable<Claim> claims = await ApplicationOAuthProvider.GetClaimsAsync(IdentityManager, userId);
  29.         ClaimsIdentity oAuthIdentity = ApplicationOAuthProvider.CreateIdentity(IdentityManager, claims,
  30.             OAuthOptions.AuthenticationType);
  31.         ClaimsIdentity cookieIdentity = ApplicationOAuthProvider.CreateIdentity(IdentityManager, claims,
  32.             CookieOptions.AuthenticationType);
  33.         AuthenticationProperties properties = await ApplicationOAuthProvider.CreatePropertiesAsync(
  34.             IdentityManager, userId);
  35.         Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
  36.     }
  37.     else
  38.     {
  39.         IEnumerable<Claim> claims = externalLogin.GetClaims();
  40.         ClaimsIdentity identity = ApplicationOAuthProvider.CreateIdentity(IdentityManager, claims,
  41.             OAuthOptions.AuthenticationType);
  42.         Authentication.SignIn(identity);
  43.     }
  44.  
  45.     return Ok();
  46. }

The OverrideAuthentication attribute is used to suppress global authentication filters. It suppresses the application bearer token host authentication filter in the SPA template.

This action enables ExternalCookieAuthenticationType host authentication, which represents the user’s external sign in state. With this setting, the User.Identity will be set as the external login identity, for example, Facebook identity.

AllowAnonymous enables a user to reach this endpoint without an external sign in state. It will trigger an external sign in challenge when the user is anonymous. That’s the scenario when the unauthorized user clicks the Facebook button to trigger a redirection to facebook.com.

After the browser redirects back from facebook.com and gets the external sign in cookie from the Facebook authentication middleware, this action will check if the external login data has already been associated with existing user.

If it has, it will sign in with both the application bearer token identity and the application cookie identity. As described in the step 8 of external login flow above, it will trigger a redirection and add an access token in URL fragment.

If not, it will sign in with the external bearer token identity and it will also be sent to the client by implicit flow. The client code will check if the user is registered by the code and show up the register external user page as needed. After the user is registered, the client code will trigger the external login flow again to get the application bearer token.

Acknowledgements

I’d like to thank Rick Anderson: (twitter @RickAndMSFT ) for the help on the blog. Rick  is a senior programming writer for Microsoft focusing on Azure and MVC.

 

Thanks,

Hongye Sun (ASP.NET MVC and Web API team)

Leave a Comment
  • Please add 3 and 6 and type the answer here:
  • Post
  • @Hogye Sun,

    You confirm my thoughts :) . Opening a child window, or using an iframe may solve the problem for a specific application, but I was mainly interested in writing a generic js library able to handle the whole login process on the client side in a black-box fashion, in such a way that the developer using it (and its server side counterpart) should not worry about the way the external login process works.  I was able to implement this black-box login for Local login, and was trying to extend my solution also to the external login. However, it appears that the problem has no solution since the usage of iframe or child windows, or other similar tricks would break the idea of black-box login....

  • Where is the encryption key coming from? how do I set it so I can have two load balanced servers handle login?

  • @Francesco, I see your point. You can make it black box but you need two server endpoints to handle the flow. The first one is to redirect user to external login page. The second one is to handle callback and send data back to javascript. You can make those two endpoints in your library so that user doesn't need to know about them. So from your javascript code, you just need to call window.open to open a new window to first endpoint. And when it calls back, your second endpoint sends out a javascript code to pass user data to some callback function on window.opener. You may also provide a server side callback to let user to convert external identity to application identity.

  • @Aaron, Owin OAuth middleware use machinekey as default encryption method. So you can configure it in web.config for load balance. msdn.microsoft.com/.../ff649308.aspx

  • @Hogye Sun,

    I got your idea, an I will try to implement. However, a good implementation requires some experimentation,  and a deeper analisys,...and mainly more time. Thus, for the moment I will close the product I am working on, and in a short time I will release a Nuget package that "do the job" for the benefit of the whole community.

    Among the problems: the user might have popup disabled(which is quite common expecially in browsers used in business applications), thus in this case the library should revert on an iframe.

    I was thinking also about using CORS in case the browser support it (the last versions of all mainstream browsers supports it), but I don't wnow if there are aready existing OWIN modules that supports CORS on the server side. What do you thing about using CORS?

  • @Francesco, you can use OWIN Cors package www.nuget.org/.../Microsoft.Owin.Cors

  • @Hogye Sun,

    Thxs, now I have everything I need to go on ....

  • Hosting the template in regular IIS is kind of painful. It's full of absolute paths, like /Token or /api/Account/ExternalLogin. I tried updating them all (hopefully) to use my site's path, but it still gives a 404 when trying /sitepath/Token. I also tried removing the first / to make them relative, but OAuthAuthorizationServerOptions.TokenEndpointPath requires an absolute path. Surely I'm not the first person to want to host this outside of IIS Express, or in a sub folder.

  • Maybe this has to do with trying to host the app in a subfolder in IIS (jmlocalhost.com/aspspa, jmlocalhost.com = 127.0.0.1), but using Microsoft sign on leaves my browser at

    jmlocalhost.com/.../ExternalLogin...

    This is a call to the Account controller, which just responds OK (HTTP 200). There is no redirect that brings up the registerExternal view. Did I miss changing a URL to have the subfolder /aspspa in it?

  • It looks like the OWIN MS authentication component has an issue when the app is not hosted in the root of the website, and doesn't respond properly when live.com redirects to "aspspa/signon-microsoft". Looking at the OWIN code for MicrosoftAccountAuthenticationHandler I think there is a bug where it generates the correct return_uri (e.g. aspspa/signon-microsoft"), but then it doesn't include the base URI in InvokeAsyc when live.com redirects back. If I'm right, then all 4 auth handlers appear to have the same bug (Twitter, Facebook, MS, Google OpenID).  I'll open a work item on the OWIN CodePlex site tomorrow.

  • Hi James, Thanks for reporting these issues. We know that SPA doesn't work well with virtual directory as many places are absolute paths. However, it shouldn't block you for the scenario after you changing all the paths.

    Please feel free to file a bug on OWIN codeplex site and we will investigate in it.

  • I think I found the issue, and reported it here: katanaproject.codeplex.com/.../158. It was deeper than I thought. I didn't know that some OWIN/Katana components run on every request and can rewrite the response. In this instance a class is looking for calls to /api/Account/ExternalLogin or /Token and will take certain actions, like redirecting you to the register page.

  • Hongye, I'm seeing a lot of browser based auth options coming out from web api but why is there  no options to provide the fastest growing mechanism of authentication in the below use case.

    User gets fb token on ios device and sends it to web api. How can we handle this situation?

  • Damian, I am working on an sample to support windows live sdk client login flow. The flow is that user uses live  SDK to get security token on client and send token back to Web API service to authenticate the user and exchange it to an application token which has the permission to access application resources. I implemented an assertion extension grant for OWIN authorization server to support this scenario. For live SDK, it has a JWT security token which can be used to verify the user. For Facebook, the access token can be verified by API from Facebook. The sample code is still under review and I will create a blog about it after it's released.

  • @Hogye,   I am trying to setup Oauth inside of a Owin application(No MVC) useing either the Host.SystemWeb or the Host.IIS and I have configured webapi and can debug into the action methods but the middleware is never picking up the ChallengeResult and changing the response code to a 302 redirect to perform authentication via google.    I have pretty much copied the code line from line from the SPA template.   Are there any examples of Authentication with only using webapi+owin outside of an mvc project?   Also what piece of middleware is supposed to be picking up the request and changing it to a 302?

    Below is my starup class.  The only thing I changed in the AccountController is the path "/login" for the ExternalLogin Method.

       public class Startup

       {

           static Startup()

           {

               PublicClientId = "self";

               UserManagerFactory = () => new UserManager<IdentityUser>(new My.Security.MyUserStore<IdentityUser>("myConnection","my"));

               OAuthOptions = new OAuthAuthorizationServerOptions

               {

                   TokenEndpointPath = new PathString("/Token"),

                   Provider = new ApplicationOAuthProvider(PublicClientId, UserManagerFactory),

                   AuthorizeEndpointPath = new PathString("/login"),

                   AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),

                   AllowInsecureHttp = true

               };

           }

           public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

           public static Func<UserManager<IdentityUser>> UserManagerFactory { get; set; }

           public static string PublicClientId { get; private set; }

           public void Configuration(IAppBuilder app)

           {

               //app.Use((ctx, continuation) =>

               //{

               //    ((Action)ctx.Environment["server.DisableResponseBuffering"])();

               //    return continuation();

               //});

               var config = new HttpConfiguration();

               config.SuppressDefaultHostAuthentication();

               config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

               config.MapHttpAttributeRoutes();

               app.UseWebApi(config);

               app.UseCookieAuthentication(new CookieAuthenticationOptions());

               app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

               app.UseOAuthBearerTokens(OAuthOptions);

               app.UseGoogleAuthentication();

           }

       }

Page 2 of 3 (35 items) 123