Experiences about Finnish Personal Health Record data repository (part 3)
This blog post covers: How to implement a small ASP.NET Core application that redirects users to the PHR Sandbox server and retrieves access tokens 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
User redirection to PHR authorization UI
The 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 that 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 an access token after the user has permitted the PHR Authorization UI. PHR Controller uses a service called PHRSandboxService to communicate with the PHR Token endpoint. After approval or refusal in the PHR Authorization UI user will be redirected to the address that was declared in the authorization URL (redirect URL). If the user has approved the permissions then the authorization code will be passed to the redirect url with code-parameter.
Authorization action gets the authorization code and state values as query string parameters. Action checks that the state parameter equals the same value that was stored in the cookie before the user was redirected to the PHR Authorization service. If the state value is missing or does not match then the 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 requests 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 endpoints (in this phase only the Token endpoint is implemented). The client certificate is not required to be used in the Sandbox environment. A basic authentication header should be added to the request otherwise you will receive 401 unauthorized. The 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 the application has an authorized user and can communicate with the PHR resource endpoint by using the access token.
Comments