diff --git a/README.md b/README.md index 783602a97a..f19abc526b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A .NET Core implementation of health storage based on the FHIR standard. [![CI Status](https://microsofthealthoss.vsrm.visualstudio.com/_apis/public/Release/badge/7621b231-1a7d-4364-935b-2f72b911c43d/1/1)](https://microsofthealthoss.visualstudio.com/FhirServer/_releases2) +## Deployment + +Please see the [deployment instructions](docs/DefaultDeployment.md) for detailed instructions. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a @@ -16,4 +20,4 @@ provided by the bot. You will only need to do this once across all repos using o This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. \ No newline at end of file +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/docs/DefaultDeployment.md b/docs/DefaultDeployment.md index 3f5db1793d..a005b12ec8 100644 --- a/docs/DefaultDeployment.md +++ b/docs/DefaultDeployment.md @@ -3,99 +3,37 @@ Microsoft Open Source FHIR Server Deployment This document describes how to deploy the [Microsoft Open Source FHIR Server](https://github.com/Microsoft/fhir-server). The deployment is a two-step process: -1. (Optional) Create and Azure Active Directory (AAD) application registration to secure access to the FHIR server. +1. (Optional) Create and Azure Active Directory (AAD) Application registration to secure access to the FHIR server. 2. Deploy Cosmos DB, Azure Web App, and source code using an Azure Resource Manager template. -Azure Active Directory Application Registration +Azure Active Directory Application (AAD) Registration ----------------------------------------------- -The FHIR server supports token based (JWT) authorization. If authorization is enabled, a user or an application accessing the server must present a token from a specified authority and with a specified audience. +The FHIR server supports token based (JWT) authorization. If authorization is enabled, a client (e.g. a user) accessing the server must present a token from a specified authority and with a specified audience. -To use the FHIR server, two Azure Active Directory applications must be registered, one for the FHIR server itself and one for a client application accessing the FHIR server. Please refer to the Azure Active Directory documentation for details on the [Web application to Web API scenario](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios#web-application-to-web-api). +To use the FHIR server with AAD authentication, two Azure Active Directory (AAD) Applications must be registered, one for the FHIR server itself and one for each client accessing the FHIR server. Please refer to the AAD documentation for details on the [Web application to Web API scenario](https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios#web-application-to-web-api). -Both Azure Active Directory applications can be registered using the Azure Portal. Please refer to the [Azure Active Directory application registration documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v1-integrate-apps-with-azure-ad). +Both AAD Applications can be registered using the Azure Portal. Please refer to the [AAD Application registration documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-v1-integrate-apps-with-azure-ad). -You can also register the applications using the [AzureAD PowerShell module](https://docs.microsoft.com/en-us/powershell/module/azuread/). First register an app for the FHIR server API: +You can also register the AAD Applications using the [AzureAD PowerShell module](https://docs.microsoft.com/en-us/powershell/module/azuread/). This repository includes a PowerShell module with some wrapper functions that help with the AAD Application registration process. -```PowerShell -# Make sure you are connected to Azure AD: -Connect-AzureAd - -# FHIR audience URL. -# A good choice is the URL of your FHIR service, e.g: -$fhirServiceName = "myfhirservice" - -# Note: use "https://${fhirServiceName}.azurewebsites.us" for US Government -$fhirApiAudience = "https://${fhirServiceName}.azurewebsites.net" - -# Create the App Registration: -$apiAppReg = New-AzureADApplication -DisplayName $fhirApiAudience -IdentifierUris $fhirApiAudience -New-AzureAdServicePrincipal -AppId $apiAppReg.AppId +First import the module with the wrapper functions: -# Gather some information for the deployment: -$aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] -$aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() - -$securityAuthenticationAuthority = "${aadEndpoint}${aadTenantId}" -$securityAuthenticationAudience = $fhirApiAudience - -# Display deployment information -Write-Host @" -FHIR Service Name (serviceName): ${fhirServiceName} -Authority (securityAuthenticationAuthority): ${securityAuthenticationAuthority} -Audience (securityAuthenticationAudience): ${securityAuthenticationAudience} -"@ +```PowerShell +Import-Module .\samples\scripts\PowerShell\FhirServer\FhirServer.psd1 ``` -To access the FHIR server from a client application, you will also need a client app registration with a client secret. This client application registration will need to have appropriate application permissions and reply URLs configured. Here is how to register a client app for use with [Postman](https://getpostman.com): +Register an AAD Application for the FHIR server API: ```PowerShell -# First specify which permissions this app should have. - -# Required App permission for Azure AD sign-in -$reqAad = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" -$reqAad.ResourceAppId = "00000002-0000-0000-c000-000000000000" -$reqAad.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" ` --ArgumentList "311a71cc-e848-46a1-bdf8-97ff7156d8e6","Scope" - -# Required App Permission for the API application registration. -$reqApi = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" -$reqApi.ResourceAppId = $apiAppReg.AppId #From API App registration above - -# Just add the first scope (user impersonation) -$reqApi.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" ` --ArgumentList $apiAppReg.Oauth2Permissions[0].id,"Scope" - -# Application registration for API -$postmanReplyUrl="https://www.getpostman.com/oauth2/callback" - -$clientAppReg = New-AzureADApplication -DisplayName "${fhirServiceName}-postman" ` --IdentifierUris "https://${fhirServiceName}-postman" ` --RequiredResourceAccess $reqAad,$reqApi -ReplyUrls $postmanReplyUrl - -# Create a client secret -$clientAppPassword = New-AzureADApplicationPasswordCredential -ObjectId $clientAppReg.ObjectId - -# Create Service Principal -New-AzureAdServicePrincipal -AppId $clientAppReg.AppId - -# Write some information needed to use client app -$clientId = $clientAppReg.AppId -$clientSecret = $clientAppPassword.Value -$replyUrl = $clientAppReg.ReplyUrls[0] -$authUrl = "${securityAuthenticationAuthority}/oauth2/authorize?resource=${securityAuthenticationAudience}" -$tokenUrl = "${securityAuthenticationAuthority}/oauth2/token" - -Write-Host @" -=== Settings for Postman OAuth2 authentication === +$fhirServiceName = "myfhirservice" +$apiAppReg = New-FhirServerApiApplicationRegistration -FhirServiceName $fhirServiceName +``` - Callback URL: ${replyUrl} - Auth URL: ${authUrl} - Access Token URL: ${tokenUrl} - Client ID: ${clientId} - Client Secret: ${clientSecret} +To access the FHIR server from a client, you will also need a client AAD Application registration with a client secret. This client AAD Application registration will need to have appropriate application permissions and reply URLs configured. Here is how to register a client AAD Application for use with [Postman](https://getpostman.com): -"@ +```PowerShell +$clientAppReg = New-FhirServerClientApplicationRegistration -ApiAppId $apiAppReg.AppId -DisplayName "myfhirclient" -ReplyUrl "https://www.getpostman.com/oauth2/callback" ``` Deploying the FHIR Server Template @@ -118,9 +56,10 @@ $rg = New-AzureRmResourceGroup -Name "RG-NAME" -Location westus2 New-AzureRmResourceGroupDeployment ` -TemplateUri "https://raw.githubusercontent.com/Microsoft/fhir-server/master/samples/templates/default-azuredeploy.json" ` --ResourceGroupName $rg.ResourceGroupName -serviceName $fhirServiceName ` --securityAuthenticationAuthority $securityAuthenticationAuthority ` --securityAuthenticationAudience $securityAuthenticationAudience +-ResourceGroupName $rg.ResourceGroupName ` +-serviceName $fhirServiceName ` +-securityAuthenticationAuthority $apiAppReg.Authority ` +-securityAuthenticationAudience $apiAppReg.Audience ``` To deploy without Authentication/Authorization: @@ -133,29 +72,31 @@ New-AzureRmResourceGroupDeployment ` -ResourceGroupName $rg.ResourceGroupName -serviceName $fhirServiceName ``` +Cleaning up Azure AD App Registrations +-------------------------------------- + +To remove the AAD Application registrations: + +```PowerShell +Remove-FhirServerApplicationRegistration -AppId $clientAppReg.AppId +Remove-FhirServerApplicationRegistration -AppId $apiAppReg.AppId +``` + Testing FHIR Server with Postman -------------------------------- You can use [Postman](https://getpostman.com) to test the FHIR server. If you have deployed with Authentication/Authorization enabled, you will need to [configure Azure AD authorization](https://blog.jongallant.com/2017/03/azure-active-directory-access-tokens-postman/ ) to obtain a token. Use type "OAuth 2.0" and set the following parameters: -*Grant Type*: `Auhorization Code` - -*Callback URL*: `https://www.getpostman.com/oauth2/callback` - -*Auth URL*: `https://login.microsoftonline.com/{TENANT-ID}/oauth2/authorize?resource={AUDIENCE}` - -*Access Token URL*: `https://login.microsoftonline.com/{TENANT-ID}/oauth2/token` - -*Client ID*: `CLIENT-APP-ID` (see above) - -*Client Secret*: `SECRET` (see above) - -*Scope*: `Ignored` (not used for Azure AD v1.0 endpoints) - -*State*: e.g., `12345` - -*Client Authentication*: `Send client credentials in body` +* Grant Type: `Auhorization Code` +* Callback URL: `https://www.getpostman.com/oauth2/callback` +* Auth URL: `https://login.microsoftonline.com/{TENANT-ID}/oauth2/authorize?resource={AUDIENCE}` +* Access Token URL: `https://login.microsoftonline.com/{TENANT-ID}/oauth2/token` +* Client ID: `CLIENT-APP-ID` (`$clientAppReg.AppId`) +* Client Secret: `SECRET` (`$clientAppReg.AppSecret`) +* Scope: `Ignored` (not used for Azure AD v1.0 endpoints) +* State: e.g., `12345` +* Client Authentication: `Send client credentials in body` Verify that the following requests return status `200 OK`: diff --git a/samples/scripts/PowerShell/FhirServer/FhirServer.psd1 b/samples/scripts/PowerShell/FhirServer/FhirServer.psd1 new file mode 100644 index 0000000000..016771f84c --- /dev/null +++ b/samples/scripts/PowerShell/FhirServer/FhirServer.psd1 @@ -0,0 +1,16 @@ +# +# Module manifest for module 'FhirServer' +# +@{ + RootModule = 'FhirServer.psm1' + ModuleVersion = '0.0.1' + GUID = '8d82e68c-0121-478c-9e81-62bced8d2a68' + Author = 'Microsoft Healthcare NExT' + CompanyName = 'https://microsoft.com' + Description = 'PowerShell Module for managing Azure Active Directory registrations and users for Microsoft FHIR Server.' + PowerShellVersion = '3.0' + FunctionsToExport = 'Remove-FhirServerApplicationRegistration', 'New-FhirServerClientApplicationRegistration', 'New-FhirServerApiApplicationRegistration' + CmdletsToExport = @() + AliasesToExport = @() +} + \ No newline at end of file diff --git a/samples/scripts/PowerShell/FhirServer/FhirServer.psm1 b/samples/scripts/PowerShell/FhirServer/FhirServer.psm1 new file mode 100644 index 0000000000..3225dcefe7 --- /dev/null +++ b/samples/scripts/PowerShell/FhirServer/FhirServer.psm1 @@ -0,0 +1,10 @@ +$Public = @( Get-ChildItem -Path "$PSScriptRoot\Public\*.ps1" ) +$Private = @( Get-ChildItem -Path "$PSScriptRoot\Private\*.ps1" ) + +@($Public + $Private) | ForEach-Object { + Try { + . $_.FullName + } Catch { + Write-Error -Message "Failed to import function $($_.FullName): $_" + } +} diff --git a/samples/scripts/PowerShell/FhirServer/Private/.gitkeep b/samples/scripts/PowerShell/FhirServer/Private/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/samples/scripts/PowerShell/FhirServer/Public/New-FhirServerApiApplicationRegistration.ps1 b/samples/scripts/PowerShell/FhirServer/Public/New-FhirServerApiApplicationRegistration.ps1 new file mode 100644 index 0000000000..bda2816628 --- /dev/null +++ b/samples/scripts/PowerShell/FhirServer/Public/New-FhirServerApiApplicationRegistration.ps1 @@ -0,0 +1,59 @@ +function New-FhirServerApiApplicationRegistration { + <# + .SYNOPSIS + Create an AAD Application registration for a FHIR server instance. + .DESCRIPTION + Create a new AAD Application registration for a FHIR server instance. + A FhirServiceName or FhirServiceAudience must be supplied. + .EXAMPLE + New-FhirServerApiApplicationRegistration -FhirServiceName "myfhiservice" + .EXAMPLE + New-FhirServerApiApplicationRegistration -FhirServiceAudience "https://myfhirservice.azurewebsites.net" + .PARAMETER FhirServiceName + Name of the FHIR service instance. + .PARAMETER FhirServiceAudience + Full URL of the FHIR service. + .PARAMETER WebAppSuffix + Will be appended to FHIR service name to form the FhirServiceAudience if one is not supplied, + e.g., azurewebsites.net or azurewebsites.us (for US Government cloud) + #> + [CmdletBinding(DefaultParameterSetName='ByFhirServiceName')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'ByFhirServiceName' )] + [string]$FhirServiceName, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByFhirServiceAudience' )] + [string]$FhirServiceAudience, + + [Parameter(Mandatory = $false, ParameterSetName = 'ByFhirServiceName' )] + [String]$WebAppSuffix = "azurewebsites.net" + ) + + # Get current AzureAd context + try { + $session = Get-AzureADCurrentSessionInfo -ErrorAction Stop + } + catch { + Write-Host "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" + Break + } + + if ([string]::IsNullOrEmpty($FhirServiceAudience)) { + $FhirServiceAudience = "https://${FhirServiceName}.${WebAppSuffix}" + } + + # Create the App Registration + $apiAppReg = New-AzureADApplication -DisplayName $FhirServiceAudience -IdentifierUris $FhirServiceAudience + $ignored = New-AzureAdServicePrincipal -AppId $apiAppReg.AppId + + $aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] + $aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() + + #Return Object + @{ + AppId = $apiAppReg.AppId; + TenantId = $aadTenantId; + Authority = "${aadEndpoint}${aadTenantId}"; + Audience = $FhirServiceAudience; + } +} \ No newline at end of file diff --git a/samples/scripts/PowerShell/FhirServer/Public/New-FhirServerClientApplicationRegistration.ps1 b/samples/scripts/PowerShell/FhirServer/Public/New-FhirServerClientApplicationRegistration.ps1 new file mode 100644 index 0000000000..c42e4129b7 --- /dev/null +++ b/samples/scripts/PowerShell/FhirServer/Public/New-FhirServerClientApplicationRegistration.ps1 @@ -0,0 +1,82 @@ +function New-FhirServerClientApplicationRegistration { + <# + .SYNOPSIS + Create an AAD Application registration for a client application. + .DESCRIPTION + Create a new AAD Application registration for a client application that consumes an API. + .EXAMPLE + New-FhirServerClientApplicationRegistration -DisplayName "clientapplication" -ApiAppId 9125e524-1509-XXXX-XXXX-74137cc75422 + .PARAMETER ApiAppId + API AAD Application registration Id + .PARAMETER DisplayName + Display name for the client AAD Application registration + .PARAMETER ReplyUrl + Reply URL for the client AAD Application registration + .PARAMETER IdentifierUri + Identifier URI for the client AAD Application registration + #> + param( + [Parameter(Mandatory = $true)] + [string]$ApiAppId, + + [Parameter(Mandatory = $true)] + [string]$DisplayName, + + [Parameter(Mandatory = $false)] + [string]$ReplyUrl = "https://www.getpostman.com/oauth2/callback", + + [Parameter(Mandatory = $false)] + [string]$IdentifierUri = "https://${DisplayName}" + ) + + # Get current AzureAd context + try { + $session = Get-AzureADCurrentSessionInfo -ErrorAction Stop + } + catch { + Write-Host "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" + Break + } + + $apiAppReg = Get-AzureADApplication -Filter "AppId eq '${ApiAppId}'" + + # Some GUID values for Azure Active Directory + # https://blogs.msdn.microsoft.com/aaddevsup/2018/06/06/guid-table-for-windows-azure-active-directory-permissions/ + # Windows AAD Resource ID: + $windowsAadResourceId = "00000002-0000-0000-c000-000000000000" + # 'Sign in and read user profile' permission (scope) + $signInScope = "311a71cc-e848-46a1-bdf8-97ff7156d8e6" + + # Required App permission for Azure AD sign-in + $reqAad = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" + $reqAad.ResourceAppId = $windowsAadResourceId + $reqAad.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $signInScope, "Scope" + + # Required App Permission for the API application registration. + $reqApi = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess" + $reqApi.ResourceAppId = $apiAppReg.AppId #From API App registration above + + # Just add the first scope (user impersonation) + $reqApi.ResourceAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess" -ArgumentList $apiAppReg.Oauth2Permissions[0].id, "Scope" + + $clientAppReg = New-AzureADApplication -DisplayName $DisplayName -IdentifierUris $IdentifierUri -RequiredResourceAccess $reqAad, $reqApi -ReplyUrls $ReplyUrl + + # Create a client secret + $clientAppPassword = New-AzureADApplicationPasswordCredential -ObjectId $clientAppReg.ObjectId + + # Create Service Principal + $ignored = New-AzureAdServicePrincipal -AppId $clientAppReg.AppId + + $securityAuthenticationAudience = $apiAppReg.IdentifierUris[0] + $aadEndpoint = (Get-AzureADCurrentSessionInfo).Environment.Endpoints["ActiveDirectory"] + $aadTenantId = (Get-AzureADCurrentSessionInfo).Tenant.Id.ToString() + $securityAuthenticationAuthority = "${aadEndpoint}${aadTenantId}" + + @{ + AppId = $clientAppReg.AppId; + AppSecret = $clientAppPassword.Value; + ReplyUrl = $clientAppReg.ReplyUrls[0] + AuthUrl = "${securityAuthenticationAuthority}/oauth2/authorize?resource=${securityAuthenticationAudience}" + TokenUrl = "${securityAuthenticationAuthority}/oauth2/token" + } +} \ No newline at end of file diff --git a/samples/scripts/PowerShell/FhirServer/Public/Remove-FhirServerApplicationRegistration.ps1 b/samples/scripts/PowerShell/FhirServer/Public/Remove-FhirServerApplicationRegistration.ps1 new file mode 100644 index 0000000000..651f79e3c2 --- /dev/null +++ b/samples/scripts/PowerShell/FhirServer/Public/Remove-FhirServerApplicationRegistration.ps1 @@ -0,0 +1,32 @@ +function Remove-FhirServerApplicationRegistration { + <# + .SYNOPSIS + Remove (delete) an AAD Application registration + .DESCRIPTION + Deletes an AAD Application registration with a specific AppId + .EXAMPLE + Remove-FhirServerApplicationRegistration -AppId 9125e524-1509-XXXX-XXXX-74137cc75422 + #> + param( + [Parameter(Mandatory = $true)] + [string]$AppId + ) + + # Get current AzureAd context + try { + $session = Get-AzureADCurrentSessionInfo -ErrorAction Stop + } + catch { + Write-Host "Please log in to Azure AD with Connect-AzureAD cmdlet before proceeding" + Break + } + + $appReg = Get-AzureADApplication -Filter "AppId eq '${AppId}'" + + if (!$appReg) { + Write-Host "Application with AppId = ${AppId} was not found." + Break + } + + Remove-AzureADApplication -ObjectId $appReg.ObjectId +} \ No newline at end of file