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.
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:
- Password login flow
- External/Social login flow (Microsoft, Facebook, Twitter and Google)
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:
- static Startup()
- {
- OAuthOptions = new OAuthAuthorizationServerOptions
- {
- TokenEndpointPath = "/Token",
- AuthorizeEndpointPath = "/api/Account/ExternalLogin",
- Provider = new ApplicationOAuthProvider(PublicClientId, IdentityManagerFactory, CookieOptions)
- };
- }
- // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
- publicvoid ConfigureAuth(IAppBuilder app)
- {
- // Enable the application to use bearer tokens to authenticate users
- app.UseOAuthBearerTokens(OAuthOptions, ExternalOAuthAuthenticationType);
- }
The UseOAuthBearerTokens extension method actually enables 3 OWIN middleware components:
- Authorization Server middleware.
- Application bearer token middleware.
- External bearer token middleware.
Here is the pseudo-code for the extension method:
- publicstaticvoid UseOAuthBearerTokens(this IAppBuilder app, OAuthAuthorizationServerOptions options, string externalAuthenticationType)
- {
- app.UseOAuthAuthorizationServer(options);
- app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
- {
- AccessTokenFormat = options.AccessTokenFormat,
- AccessTokenProvider = options.AccessTokenProvider,
- AuthenticationMode = options.AuthenticationMode,
- AuthenticationType = options.AuthenticationType,
- Description = options.Description,
- Provider = new AppBuilderExtensions.ApplicationOAuthBearerProvider(),
- SystemClock = options.SystemClock
- });
- app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
- {
- AccessTokenFormat = options.AccessTokenFormat,
- AccessTokenProvider = options.AccessTokenProvider,
- AuthenticationMode = AuthenticationMode.Passive,
- AuthenticationType = externalAuthenticationType,
- Description = options.Description,
- Provider = new AppBuilderExtensions.ExternalOAuthBearerProvider(),
- SystemClock = options.SystemClock
- });
- }
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:
- // Web API configuration and services
- // Configure Web API to use only bearer token authentication.
- config.SuppressDefaultHostAuthentication();
- config.Filters.Add(newHostAuthenticationFilter(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:47948grant_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).
- publicoverrideasyncTask GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
- {
- using (IdentityManager identityManager = _identityManagerFactory.CreateStoreManager())
- {
- if (!await identityManager.Passwords.CheckPasswordAsync(context.UserName, context.Password))
- {
- context.SetError("invalid_grant", "The user name or password is incorrect.");
- return;
- }
- string userId = await identityManager.Logins.GetUserIdForLocalLoginAsync(context.UserName);
- IEnumerable<Claim> claims = await GetClaimsAsync(identityManager, userId);
- ClaimsIdentity oAuthIdentity = CreateIdentity(identityManager, claims,
- context.Options.AuthenticationType);
- ClaimsIdentity cookiesIdentity = CreateIdentity(identityManager, claims,
- _cookieOptions.AuthenticationType);
- AuthenticationProperties properties = await CreatePropertiesAsync(identityManager, userId);
- AuthenticationTicket ticket = newAuthenticationTicket(oAuthIdentity, properties);
- context.Validated(ticket);
- context.Request.Context.Authentication.SignIn(cookiesIdentity);
- }
- }
What the code above does is quite straightforward:
- Checks the password with the Identity API .
- Create a user identity for the bearer token.
- Creates a user identity for the cookie.
- 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.
- 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:
- 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
- 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.
- The user signs in with her facebook account and which grants access to this application.
- 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.
- The Facebook authentication middleware asks for access token and user data from facebook.com.
- 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.
- The user agents redirects to the authorization endpoint with the external cookie in the request.
- 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).
- // GET api/Account/ExternalLogin
- [OverrideAuthentication]
- [HostAuthentication(Startup.ExternalCookieAuthenticationType)]
- [AllowAnonymous]
- [HttpGet("ExternalLogin", RouteName = "ExternalLogin")]
- publicasyncTask<IHttpActionResult> ExternalLogin(string provider)
- {
- if (!User.Identity.IsAuthenticated)
- {
- returnnewChallengeResult(provider, this);
- }
- ExternalLoginData externalLogin = ExternalLoginData.FromIdentity(User.Identity asClaimsIdentity);
- if (externalLogin == null)
- {
- return InternalServerError();
- }
- string userId = await IdentityManager.Logins.GetUserIdForLoginAsync(externalLogin.LoginProvider,
- externalLogin.ProviderKey);
- bool hasRegistered = userId != null;
- if (hasRegistered)
- {
- Authentication.SignOut(Startup.ExternalCookieAuthenticationType);
- IEnumerable<Claim> claims = awaitApplicationOAuthProvider.GetClaimsAsync(IdentityManager, userId);
- ClaimsIdentity oAuthIdentity = ApplicationOAuthProvider.CreateIdentity(IdentityManager, claims,
- OAuthOptions.AuthenticationType);
- ClaimsIdentity cookieIdentity = ApplicationOAuthProvider.CreateIdentity(IdentityManager, claims,
- CookieOptions.AuthenticationType);
- AuthenticationProperties properties = awaitApplicationOAuthProvider.CreatePropertiesAsync(
- IdentityManager, userId);
- Authentication.SignIn(properties, oAuthIdentity, cookieIdentity);
- }
- else
- {
- IEnumerable<Claim> claims = externalLogin.GetClaims();
- ClaimsIdentity identity = ApplicationOAuthProvider.CreateIdentity(IdentityManager, claims,
- OAuthOptions.AuthenticationType);
- Authentication.SignIn(identity);
- }
- return Ok();
- }
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
Rick Anderson: (twitter @RickAndMSFT ) Rick is a senior programming writer for Microsoft focusing on Azure and MVC.