Menu

Experiences about Finnish Personal Health Record data repository (part 3)

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 code flow. Before creating this application read my previous blog post about PHR.

Overview

undefined

User redirection to PHR authorization UI

Application creates authorization redirection to PHR Sandbox Authorization UI with the following query string 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);
        }
    }

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.

Handling of authorization code and access token

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 UI. PHR Controller uses service called PHRSandboxService to communicate with PHR Token endpoint. After approval or refusal in PHR Authorization UI 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.

Authorization action gets the authorization code and state values as a query string 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. 

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 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;
        }
    }

Now application has authorized user and can communicate with PHR resource endpoint by using the access token.

Comments