React Authentication on ASP.NET Core with OAuth and Identity
React Authentication
In this post I explain how React authentication on ASP.NET Core in the RealTimeWeb.NET application is implemented.
The application supports two ways to authenticate:
- By registration and entering username and password
- By connecting to an external login provider like Facebook and Google.
ASP.NET Core supports cookie authentication out-of-the-box. While this is enough to create classic web-application and protect controller view actions, it is not secure enough to protect the API’s used by a Single Page Application (SPA). The recommended way to protect an API is to use authentication tokens. The tokens we use here to are defined by the JWT standard in RFC 7519, JSON Web Tokens.
To generate these tokens we use the AspNet.Security.OpenIdConnect.Server (ASOS). ASOS is an advanced OAuth2/OpenID Connect server framework for ASP.NET Core. The generated tokens are validated by using ASP.NET Jwt Bearer authentication.
Configuration
All authentication configuration is placed in Infrastructure/AuthenticationConfiguration.cs:
1 2 3 4 5 6 7 8 9 10 11 12 | public static IApplicationBuilder ConfigureAuthentication(this IApplicationBuilder app, IConfiguration configuration) { if (app == null) throw new ArgumentNullException(nameof(app)); app.UseWhen(IsApi, ApiAuthentication(configuration)) .UseWhen(IsWeb, WebAuthentication) .ConfigureWhen(configuration.AuthenticationFacebookConfigured(), FacebookAuthentication(configuration)) .ConfigureWhen(configuration.AuthenticationGoogleConfigured(), GoogleAuthentication(configuration)) .UseOpenIdConnectServer(ServerOptions); return app; } |
It will configure:
- JWT bearer token authentication for API calls.1234567891011121314151617private static bool IsApi(HttpContext context){return context.Request.Path.StartsWithSegments(new PathString("/api"));}private static Action ApiAuthentication(IConfiguration configuration){return branch => branch.UseJwtBearerAuthentication(options =>{options.AutomaticAuthenticate = true;options.AutomaticChallenge = true;options.RequireHttpsMetadata = false;options.Audience = configuration.ApiHostName();options.Authority = configuration.ApiHostName();});}
- Cookie authentication for WEB calls.1234567891011121314151617private static bool IsWeb(HttpContext context){return !IsApi(context);}private static void WebAuthentication(IApplicationBuilder branch){branch.UseCookieAuthentication(options =>{options.AutomaticAuthenticate = true;options.AutomaticChallenge = true;options.AuthenticationScheme = "ServerCookie";options.CookieName = CookieAuthenticationDefaults.CookiePrefix + "ServerCookie";options.ExpireTimeSpan = TimeSpan.FromMinutes(5);options.LoginPath = new PathString("/signin");});}
- Google and Facebook external authentication providers. These providers are only configured when valid configuration is found in the application configuration.12345678910111213141516171819202122private static Action>IApplicationbuilder> GoogleAuthentication(IConfiguration configuration){return app => app.UseGoogleAuthentication(options =>{options.ClientId = configuration.AuthenticationGoogleClientId();options.ClientSecret = configuration.AuthenticationGoogleClientSecret();});}private static Action>IApplicationbuilder> FacebookAuthentication(IConfiguration configuration){return app => app.UseFacebookAuthentication(options =>{options.AppId = configuration.AuthenticationFacebookAppId();options.AppSecret = configuration.AuthenticationFacebookAppSecret();options.BackchannelHttpHandler = new HttpClientHandler();options.UserInformationEndpoint = "https://graph.facebook.com/v2.5/me?fields=id,name,email";options.Scope.Add("email");});}The last three lines in FacebookAuthentication are added in order to get the e-mail address from Facebook.
- OpenIdConnect.Server that provide us with an authorization and token endpoints. These are used to authenticate from the React client application.12345678910private static void ServerOptions(OpenIdConnectServerOptions options){options.Provider = new AuthorizationServerProvider();options.AllowInsecureHttp = true;options.AuthorizationEndpointPath = "/account/authorize";options.TokenEndpointPath = "/token";options.AccessTokenLifetime = TimeSpan.FromMinutes(20);options.RefreshTokenLifetime = TimeSpan.FromHours(24);}
Username/password login
The flow used for username and password authentication is rather simple:
- The user enters username and password in a textbox. (source)
- The token endpoint is called with the entered credentials. (source)
- When the credentials are correct an access_token and refresh_token are returned. Both tokens are stored in the local storage of the browser. (source)
- When an API call is made the access_token is added in the header of the HTTP call to authenticate. When a valid access_token is used a valid response is returned with HTTP code 200 (OK). (source)
External provider login
OAuth also support authentication with an external provider. In the example both Google and Facebook are supported. But online you also find adapters for most other providers like GitHub, LinkedIn, … in the AspNet contrib repository.
The flow to authenticate with an external provider is a bit more complex:
- The user clicks on a specific external provider. (source)
- A new window is opened and loads the authorize endpoint: /account/authorize/connect?grant_type=authorization_code. (client and server source)
- This request is handled by the registered providers and redirects to the external provider login page.
- When the login in successful the external provider redirects back to our authorization complete endpoint: /account/authorize/complete. (source)
- When the the authorization is completed we redirect to the authorized enpoind with the generated authorization_code. This additional step is needed to get both an access_token and refresh_token because the authorization endpoint is not allowed to return the refresh_token by the RFC.
- The token endpoint is called with the authentication code. (source)
- When the authentication code is correct the access_token and refresh_token are returned. Both tokens are stored in the local storage of the browser. (source)
- When an API call is made the access_token is added in the header of the HTTP call to authenticate. When a valid access_token is used a valid response is returned with HTTP code 200 (OK). (source)
Using a refresh_token
When a token is requested both an access_token and refresh_token are returned. The validation lifetime of an access_token is much shorter (20 minutes) as the lifetime of the refresh_token (24 hours). The goal is to request a new access_token by providing the refresh_token when the access_token is expired.
Refresh_token are recommended because the user can stay logged-in for a longer period of time without the need to resend the credentials over the wire again. And they can also be revoked by the server. The advantages and disadvantages of the revocation support are explained in this StackOverflow post.
- When the access_token expires the API returns an HTTP 401 response.
- When the HTTP 401 response is received the token endpoit is called with the refres_token. (source)
- When the refresh_token is not expired the new access_token and refresh_token are returned. Both tokens are stored in the local storage of the browser. (source)
- When an API call is made the access_token is added in the header of the HTTP call to authenticate. When a valid access_token is used a valid response is returned with HTTP code 200 (OK). (source)
ASP.NET Identity
To manage users ASP.NET Identity is used in combination with a custom user store that targets PostgreSQL. This will be explained in a future blog post. To be able to run the application locally without the need to install PostgreSQL, an in-memory stored can be used by leaving the connections string empty. For mor information about hot to run the example locally go to the getting started post.
Credits
I like to give credit to Taiseer Joudeh for his AngularJS Token Authentication blog posts. And I also would like to thank Kévin Chalet for the support on getting ASOS to work!
Source code
The source code of the application can be found on github:
https://github.com/tim-cools/RealTimeWeb.NET
Warning!
The application is still work in progress and new features will be added in the next following weeks…
Some of the technologies and frameworks used in this application are pre-release and most likely to change. Currently it is based on RC1 of .NET Core. I try to update the code as soon as possible whenever the final version is released. Ping me on Twitter if you have questions or issues.
RealTimeWeb.NET Blog Posts
This post is part of a blog post series about RealTimeWeb.NET application.
- RealTimeWeb.NET – A real time ASP.NET Core application
- Getting started with RealTimeWeb.NET
- RealTimeWeb.NET Front-end
- Creating an ASP.NET Core web application
- Single page application in React on ASP.NET Core
- React Authentication on ASP.NET Core with OAuth and Identity
- Real-time data pushed by web-sockets (SignalR) on ASP.NET Core
- Server-side rendering
- Real-time back-end
- Operations
- ...