Menu

This blog post covers: How to implement a small ASP.NET Core application which redirects user to the PHR Sandbox server and retrieves access token from the PHR endpoint. This application is used to demonstrate OAuth 2 authorization flow. Before creating this application read my previous blog post about PHR.

Authorization User interface

Let's first look about user interfaces which are related to this application and PHR Sandbox authorization. 

Authorization link in the Client application

This simple app has an authorization link in the Index view. User is redirected to the PHR Authorization service when link is clicked. In the PHR Authorization service user give's permission to PHR client application to transfer well being data (observations) to the PHR Sandbox and as well search data.

Authorization views in the PHR Sandbox

The following login view is shown after redirection in the PHR Sandbox service. In the login phase user inputs first name, last name and Finnish social security number. When testing a service you can generate Finnish social security numbers from here. In the production and QA-environment (aka. AT-test) login will be handled via Suomi.fi.

After login user can give or refuse the permissions which were declared in the application settings and scopes. This application uses only Observation data.

After approval or refusal user will be redirect to the address which was declared in the authorization url (redirect URL). If user has approved the permissions then authorization code will be passed to the redirect url with code-parameter.

This application has an view which only shows the content of the access token after authorization. Real production application will store access token information to the permanent store (ex. database).

Authorization client application

Let's start. Open Visual Studio and select ASP.NET Core Web Application.

This sample app is created with ASP.NET MVC template.

First verify the SSL application URL and port from the project properties. In the previous blog post the test application client was determined to use port 44365.

Prerequisites 

App Settings 

The following app settings are declared in the appsettings.json

PHRTokenApiUrl = https://fhirsandbox2-auth.kanta.fi/phr-authserver-sandbox
PHRAuthServerUrl = https://fhirsandbox2-auth.kanta.fi/phr-authserver-sandbox/authorize
PHRClientId = [ClientId of the PHR application]
PHRRedirectUrl = https://localhost:44365/phr/authorization
PHRScopes = patient/Observation.read offline_access patient/Observation.write
PHRClientSecret = [Client secret of the PHR application]

Startup

This application uses the following startup configuration:

public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions();
            services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddScoped<IPHRSandboxService, PHRSandboxService>();
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddHttpClient();
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }

What has changed when comparing the default configuration:

1) Added possibility to use AppSettings configurations from the appsettings.json file

public void ConfigureServices(IServiceCollection services)
{
   services.AddOptions();
   services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
}

2) Added custom service class called PHRSandboxService, HttpContextAccessor and HttpClient.

PHRSandboxService is a custom service class which handles HTTP requests to the PHR Sandbox service. More information about this service class later.

public void ConfigureServices(IServiceCollection services)
{
   services.AddScoped<IPHRSandboxService, PHRSandboxService>();
   services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
   services.AddHttpClient();
}

Views

Index

In the Index View authorization link is an URL action which points to PhrAuthorization action of the Home Controller. This Action produces an authorization link which redirects user to the PHR authorization server.

Authorization

Authorization view receives PHRTokenResponse model and renders the values of the model to the screen.

HomeController

PhrAuthorization action creates authorization redirection to PHR Sandbox Authorization service with the following querystring parameters:

Parameter Description

response_type

Hard coded value "code"

client_id

Client Id of your application

redirect_uri

Client Application URL where user is redirected after authorization. This application will redirect user back to the following address https://localhost:44365/phr/authorization (PhrController)

scope

Scopes which are required in the client application (ex. Observation read/write)

state

State value generated by client application. During authentication, the application sends this parameter in the authorization request, and the Authorization Server will return this parameter unchanged in the response. State parameter is used to make sure that the response belongs to a request that was initiated by the same user. Therefore, state helps mitigate CSRF attacks. More information about state parameter can be find from here (Auth0).

This sample application stores state value to the cookie.

public class HomeController : Controller
    {
        private readonly AppSettings _appSettings;

        public HomeController(IOptions<AppSettings> settings)
        {
            _appSettings = settings.Value;
        }
        public IActionResult Index()
        {
            return View();
        }

        public IActionResult PhrAuthorization()
        {
            var phrAuthServerUrl = _appSettings.PHRAuthServerUrl;
            var responseType = "code";
            var clientId = _appSettings.PHRClientId;
            var redirectUri = WebUtility.UrlEncode(_appSettings.PHRRedirectUrl);
            var scopes = WebUtility.UrlEncode(_appSettings.PHRScopes);
            //create a new state parameter per request
            var state = Guid.NewGuid().ToString();
            //set state value to the cookies which expires after 1 minute
            SetCookie(PHRConstants.AuthorizationStateCookie, state, 1);
            //create a redirect URL
            var redirectUrl = $"{phrAuthServerUrl}?response_type={responseType}&client_id={clientId}&redirect_uri={redirectUri}&scope={scopes}&state={state}";
            //redirect user to the PHR
            return Redirect(redirectUrl);
        }

        /// <summary>  
        /// set the cookie  
        /// </summary>  
        /// <param name="key">key (unique indentifier)</param>  
        /// <param name="value">value to store in cookie object</param>  
        /// <param name="expireTime">expiration time</param>  
        public void SetCookie(string key, string value, int? expireTime)
        {
            CookieOptions option = new CookieOptions();

            if (expireTime.HasValue)
                option.Expires = DateTime.Now.AddMinutes(expireTime.Value);
            else
                option.Expires = DateTime.Now.AddMilliseconds(10);

            Response.Cookies.Append(key, value, option);
        }
    }

PHR Controller

PHR Controller is used in this application to receive an authorization code and get access token after user has given the permission in the PHR Authorization service. PHR Controller uses service called PHRSandboxService to communicate with PHR Token endpoint.

Authorization action gets the authorization code and state values as a querystring parameters. Action checks that state parameter equals with the same value which was stored to the cookie before user was redirected to the PHR Authorization service. If state value is missing or not match then operation will be canceled. Like said earlier state helps mitigate CSRF attacks.

public class PHRController : Controller
    {
        private readonly IPHRSandboxService _phrSandboxService;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly AppSettings _appSettings;

        public PHRController(IPHRSandboxService phrSandboxService, IHttpContextAccessor httpContextAccessor, IOptions<AppSettings> settings)
        {
            _phrSandboxService = phrSandboxService;
            _httpContextAccessor = httpContextAccessor;
            _appSettings = settings.Value;
        }

        public async Task<IActionResult> Authorization(string code, string state)
        {
            if (string.IsNullOrEmpty(code) || string.IsNullOrEmpty(state))
                throw new Exception("Authorization state or code was null or empty");

            //get state parameter from the cookie
            string authorizationStateCookie = _httpContextAccessor.HttpContext.Request.Cookies[PHRConstants.AuthorizationStateCookie];

            if (string.IsNullOrEmpty(authorizationStateCookie))
                throw new Exception("Authorization state in the cookie was null");

            if (!state.Equals(authorizationStateCookie))
                throw new Exception("Authorization states not matched");

            var parameters = new PHRTokenRequestParams() {
                Client_id = _appSettings.PHRClientId,
                Code = code,
                Grant_type = "authorization_code",
                Redirect_uri = _appSettings.PHRRedirectUrl
            };

            var data = await _phrSandboxService.GetTokenResponse(parameters);

            return View(data);
        }
    }

PHR Token Request Parameter model

This model is used to wrap required token end point parameters.

public class PHRTokenRequestParams
    {
        public string Grant_type { get; set; }
        public string Code { get; set; }
        public string Redirect_uri { get; set; }
        public string Client_id { get; set; }
    }

PHR Sandbox Service class

This class handles all HTTP request to the PHR Authorization server. 

Service implements the following interface:

public interface IPHRSandboxService
    {
        Uri AuthApiBaseUri { get; set; }
        Uri TokenApiBaseUri { get; set; }
        Task<PHRTokenResponse> GetTokenResponse(PHRTokenRequestParams requestParams);
    }

This service class communicates with PHR Sandbox service end points (in this phase only Token endpoint is implemented). Client certificate is not required to use in Sandbox environment. Basic authentication header should be added to the request otherwise you will receive 401 unauthorized. Basic authentication header value is a combination of your ClientId and Client secret. 

 public class PHRSandboxService : IPHRSandboxService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly AppSettings _appSettings;

        public Uri AuthApiBaseUri { get; set; }
        public Uri TokenApiBaseUri { get; set; }

        public PHRSandboxService(IOptions<AppSettings> settings, IHttpContextAccessor httpContextAccessor, IHttpClientFactory clientFactory)
        {
            //Sandbox uses one url for auth and token endpoint
            TokenApiBaseUri = new Uri(settings.Value.PHRTokenApiUrl + (settings.Value.PHRTokenApiUrl.EndsWith("/") ? "" : "/"));
            _appSettings = settings.Value;
            _clientFactory = clientFactory;
        }

        public async Task<PHRTokenResponse> GetTokenResponse(PHRTokenRequestParams requestParams)
        {
            string url = $"token";
            string stringcont = $"grant_type={WebUtility.UrlEncode(requestParams.Grant_type)}&code={WebUtility.UrlEncode(requestParams.Code)}&redirect_uri={WebUtility.UrlEncode(requestParams.Redirect_uri)}&client_id={WebUtility.UrlEncode(requestParams.Client_id)}";
            var content = new StringContent(stringcont, Encoding.UTF8, "application/x-www-form-urlencoded");
            var response = await CreateTokenSendRequest(HttpMethod.Post, url, content);
            response.EnsureSuccessStatusCode();
            var stringResponse = await response.Content.ReadAsStringAsync();
            var tokenResponse = JsonConvert.DeserializeObject<PHRTokenResponse>(stringResponse);

            return tokenResponse;
        }


        private async Task<HttpResponseMessage> CreateTokenSendRequest(HttpMethod method, string uri, StringContent content = null)
        {
            var request = new HttpRequestMessage(method, new Uri(TokenApiBaseUri + uri));
            request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(System.Text.ASCIIEncoding.ASCII.GetBytes(string.Format("{0}:{1}", _appSettings.PHRClientId, _appSettings.PHRClientSecret))));
            if (content != null)
            {
                request.Content = content;
            }

            var _httpClient = _clientFactory.CreateClient();

            var response = await _httpClient.SendAsync(request);
            return response;
        }
    }

PHR Token response model

This model follows the token end point response.

public class PHRTokenResponse
    {
        public string Access_token { get; set; }
        public string Token_type { get; set; }
        public string Refresh_token { get; set; }
        public string Expires_in { get; set; }
        public string Scope { get; set; }
        public string Sub { get; set; }
        public string State { get; set; }
    }

Now the most crucial parts of the authorization process are covered up. Next blog post handles PHR resource server related issues.