Mobile App with Corporate Authentication (Ionic + ASP.NET Web API + OAuth 2.0 tokens + Okta)
Recently we had to build a mobile application and one of the requirements was to implement a corporate authentication for managing the user accounts. We think the approach that we used could be interesting and helpful in similar scenarios that is why we would like the share our experience.
The app that we built is simple and allows pulling and displaying some corporate information for authenticated users. Following are some details about the technologies that we used to build the app:
- Ionic – We used the Ionic framework to build the mobile app itself. Ionic is a great framework for building hybrid mobile apps. It is based on AngularJS and provides handful components and tools. The apps build with Ionic are easily distributed to iOS and Android devices.
- ASP.NET Web API – We used the ASP.NET Web API 2.2 to build the server part. It pulls the data from the database and sends it to the mobile app in JSON format by implementing a RESTFul API. It also handles the authentication part by implementing part of the OAuth 2.0 protocol.
- Okta – Okta is a third-party authentication provider and our client had already been using it for managing their Active Directory accounts.
In short, the mobile app sends the credentials to the API server which is responsible to validate them by communicating with Okta. Once the credentials are validated then it generates a token which is stored on the mobile device and used for further communication between the mobile app and the API server.
Following is a diagram depicting the idea:
The client mobile app is built with Ionic which is based on AngularJS. So, we have built a couple of AngularJS services that are responsible for handling the authentication on the client side.
We use the Web API infrastructure at the server side. It is OWIN based and we use the OAuth 2.0 related classes for OWIN to build our specific authentication functionality. For more information you could check the Microsoft.Owin.Security.OAuth package.
The basic concept could be described with the following steps:
- If the user is not authenticated then show the Login form. Otherwise, try to load some of the corporate app data.
- On the Login form the user enters his credentials and then an Access Token Request is sent to the server.
- The server validates the user credentials by checking them against the Okta identity provider.
- If the credentials are valid the server generates an access token (expires after 30 minutes) and a refresh token (doesn’t expire in practice). The refresh token is stored into the database together with some encrypted info about the user.
- The client app receives the tokens and it stores them locally in the device.
- With each data request to the server, the client app is sending the access token.
- If the access token is still valid (less than 30 minutes have passed) then the server will return the data.
- If the access token has expired the server will return error 401 (unauthorized). In that case the client app will send the refresh token to the server.
- The server tries to validate the refresh token by searching it into the database. If the refresh token is found then it tries to check the user credentials info against Okta. This is done to catch any deactivated users, for example. If everything is valid then a new access token is generated and sent back to the client. Otherwise the user is redirected to the Login form.
In order to provide some more details I will walk through the most important parts of the implementation. The code snippets are provided to get an idea of what is happening. This is not the complete code and some extra functionality was removed to avoid cluttering the article as much as possible:
- In the Ionic mobile app, the user is prompted to enter his credentials in a simple login form. When he clicks on the Login button then the following code is used to send an OAuth 2.0 request to the server:
var data = "grant_type=password&username=" + loginData.userName + "&password=" + loginData.password + "&client_id=" + appSettings.oauth2ClientId; var deferred = $q.defer(); $http.post(appSettings.apiTokenUrl, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).success(function (response, status, headers, config) { localStorageService.set('authorizationData', { token: response.access_token, refreshToken: response.refresh_token }); deferred.resolve(response); }).error(function (err, status) { deferred.reject(err); }); return deferred.promise;
It sends an Access Token Request according to the OAuth 2.0 specs. When the request completes successfully we put the received tokens into the local storage. There are two types of tokens:
a) Access token – It is used with each request to authenticate the client on the server.
b) Refresh token – It is used when the access token expires. In that case the refresh token is used to extend the login session and generate a new access token.As noticed, we have configured the access token to expire every 30 minutes. The refresh token is configured to expire after a very long time (almost infinitely). As a result, when the user logs into the app on the mobile device then he remains logged in until he manually logs out or his corporate account gets deactivated, for example.
- On the server side we use the Startup class to configure the Web API to use OAuth 2.0 for authentication purposes. Following is a code snipped showing that:
public class Startup { public void Configuration(IAppBuilder app) { ConfigureOAuth(app); } public void ConfigureOAuth(IAppBuilder app) { OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions() { AllowInsecureHttp = false, TokenEndpointPath = new PathString("/token"), AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30), Provider = new SimpleAuthorizationServerProvider(), RefreshTokenProvider = new SimpleRefreshTokenProvider() }; app.UseOAuthAuthorizationServer(OAuthServerOptions); app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); } }
The SimpleAuthorizationServerProvider class is used to handle the access token generation and validation and the SimpleRefreshTokenProvider class is used to handle the refresh token generation and validation. More details about those providers are shown below.
- The SimpleAuthorizationServerProvider class is inherited from the OAuthAuthorizationServerProvider class. We override some of its methods where we implement our custom functionality as follows:
- ValidateClientAuthentication() – this methods is called to validate the Client Id, according to the OAuth 2.0 specs. We just check if the provided Client Id exists into the database:
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context) { string clientId = string.Empty; string clientSecret = string.Empty; context.TryGetFormCredentials(out clientId, out clientSecret); if (context.ClientId == null) { context.SetError("invalid_clientId", "ClientId should be sent"); return; } ClientAppRepository repository = new ClientAppRepository(); ClientApp clientApp = repository.GetClientApp(clientId); if (clientApp == null) { context.SetError("invalid_clientId", "Unknown ClientId"); return; } else if (!clientApp.IsActive) { context.SetError("invalid_clientId", "The client application is inactive"); return; } context.Validated(); }
If the Client Id is invalid then an error is sent, otherwise the context is validated successfully.
- GrantResourceOwnerCredentials() – this method is called to validate the user credentials. In our scenario the credentials are validated against the Okta identity provider and this is done by using the Okta API. Following is part of this method:
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { bool isValidUser = false; try { OktaAuthentication oktaAuthentication = new OktaAuthentication(); isValidUser = oktaAuthentication.isUserValid(context.UserName, context.Password); } catch (Exception e) { context.SetError("invalid_grant", "There was a problem with Authentication Service"); return; } if (!isValidUser) { context.SetError("invalid_grant", "The username or password is incorrect"); return; } var identity = new ClaimsIdentity(context.Options.AuthenticationType); var ticket = new AuthenticationTicket(identity); context.Validated(ticket); }
- GrantRefreshToken() – this method is called when generating a new access token by using the refresh token:
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context) { var newIdentity = new ClaimsIdentity(context.Ticket.Identity); var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties); context.Validated(newTicket); return Task.FromResult<object>(null); }
- ValidateClientAuthentication() – this methods is called to validate the Client Id, according to the OAuth 2.0 specs. We just check if the provided Client Id exists into the database:
- The SimpleRefreshTokenProvider class implements the IAuthenticationTokenProvider interface. Following are some details about the two methods that are implemented:
- CreateAsync() – this method is called when creating the refresh token:
public async Task CreateAsync(AuthenticationTokenCreateContext context) { var refreshTokenId = Guid.NewGuid().ToString("n"); var refreshTokenLifeTimeMinutes = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTimeMinutes"); RefreshToken refreshToken = new RefreshToken() { ... }; refreshToken.ProtectedTicket = context.SerializeTicket(); RefreshTokensRepository refreshTokensRepository = new RefreshTokensRepository(); repository.AddToken(refreshToken); context.Ticket.Properties.IssuedUtc = new DateTimeOffset(refreshToken.IssuedUtc, TimeSpan.Zero); context.Ticket.Properties.ExpiresUtc = new DateTimeOffset(refreshToken.ExpiresUtc, TimeSpan.Zero); context.SetToken(refreshTokenId); }
The refresh token is actually a GUID. When a new refresh token is generated we generate a new GUID and then we store the refresh token into the database. In addition to the token itself, we store information about the client credentials in an encrypted way. This information is used later to validate the user credentials against the Okta API each time when the refresh token is validated.
- ReceiveAsync() – this method is called when validating the refresh token.
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context) { string hashedTokenId = Cryptography.ComputeHash(context.Token); RefreshTokensRepository repository = new RefreshTokensRepository(); var refreshToken = repository.GetToken(hashedTokenId); bool isValidUser = false; if (refreshToken != null) { OktaAuthentication oktaAuthentication = new OktaAuthentication(); isValidUser = oktaAuthentication.isUserValid(Cryptography.Decrypt(refreshToken.UserName), Cryptography.Decrypt(refreshToken.UserPassword)); } if (isValidUser) { context.DeserializeTicket(refreshToken.ProtectedTicket); } }
The refresh token is pulled from the database by the token provided in the request. If the refresh token is found then the user credentials are validated against the Okta service. If everything is valid then the ticket is deserialized which is loading the ticket into the context and the request is processed successfully.
- CreateAsync() – this method is called when creating the refresh token:
- The client mobile app is sending the access token with each request. If the server returns error 401 (unauthorized) then the client app is trying to request a new access token by sending the refresh token. This is implemented as an AngularJS interceptor service as follows:
angular.module('app.authInterceptorService', ['LocalStorageModule']) .factory('authInterceptorService', ['$q', '$location', '$window', 'localStorageService', '$injector', function ($q, $location, $window, localStorageService, $injector) { var authInterceptorServiceFactory = {}; var _request = function (config) { config.headers = config.headers || {}; var authData = localStorageService.get('authorizationData'); if (authData) { config.headers.Authorization = 'Bearer ' + authData.token; } return config; } var _responseError = function (rejection) { if (rejection.status === 401) { //Try refreshing the access token by using the available refresh token var authService = $injector.get('authService'); var deferred = $q.defer(); authService.refreshToken().then(function (response) { //Now try sending the original request again $injector.get("$http")(rejection.config).then(function (response) { deferred.resolve(response); }, function (response) { deferred.reject(); }); }, function (err) { authService.logOut().then(function (response) { $location.path('/login'); deferred.reject(); }, function (err) { deferred.reject(); }); }); return deferred.promise; } return $q.reject(rejection); } authInterceptorServiceFactory.request = _request; authInterceptorServiceFactory.responseError = _responseError; return authInterceptorServiceFactory; }]);
The client app is sending the access token by utilizing the Authorization HTTP header.
The service is checking the error code when a particular request to the server fails. If the error code is 401 (unauthorized) then it is sending a refresh token request. If the refresh token request is successful then the code is trying to resend the original request, otherwise the user is redirected to the login form.
The interceptor service is registered when configuring the AngularJS app as follows:
.config(['$httpProvider', function ($httpProvider) { $httpProvider.interceptors.push('authInterceptorService'); }])
Following is a code snippet of how the refresh token request is sent:
var deferred = $q.defer(); var authData = localStorageService.get('authorizationData'); if (authData) { var data = "grant_type=refresh_token&refresh_token=" + authData.refreshToken + "&client_id=" + appSettings.oauth2ClientId; $http.post(appSettings.apiTokenUrl, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).success(function (response) { localStorageService.remove('authorizationData'); localStorageService.set('authorizationData', { token: response.access_token, refreshToken: response.refresh_token }); deferred.resolve(response); }).error(function (err, status) { deferred.reject(err); }); } return deferred.promise;
In our scenario the user credentials are validated against the third-party identity provider Okta. However, similar functionality could be used to authenticate the users when using another identity provider like Azure Active Directory, for example. Of course, the same idea is valid even for the simpler scenario when the users are stored into the local database on the server.
That covers the process of implementing tokens based authentication for a hybrid mobile application. Thank you for reading it!