Skip to content

Commit

Permalink
Merge pull request #197 from Azure/users/landonpierce/permauthapikey
Browse files Browse the repository at this point in the history
Add support for Permissions API Key Authentication and remove Certificate Authentication
  • Loading branch information
landonpierce authored Jul 25, 2022
2 parents 6a927a9 + 601b719 commit 14d1f53
Show file tree
Hide file tree
Showing 26 changed files with 163 additions and 326 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -133,27 +133,9 @@ Note: Collaborators are other developers you wish to help manage your services,

---

### Step 5. Create self-signed certificate
### Step 5. Create a random API Key

>Note: A self-signed certificate is not recommended for production environments. Read more [here](https://azure.github.io/azure-saas/components/identity/permissions-service/#authentication).
- Create an RSA self-signed certificate to be referenced and uploaded to your Azure AD B2C Tenant
- The .pfx file will be uploaded in a later step to your Azure AD B2C to allow it access to integrate with your Permissions API
- OpenSSL with [Powershell Core (v7.0+)](https://github.com/PowerShell/PowerShell) Example:

```
$pswd = Read-Host -Prompt "Please enter a password to encrypt the self signed certificate with"
# Create a new certificate in .crt format with the subject name as *.azurewebsites.net. You could also provide a specific subject name of your permissions api.
openssl req -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out certificate.crt -keyout certificate.key -subj "/CN=*.azurewebsites.net"
# Export the .crt and .key to a .pfx format
openssl pkcs12 -export -out selfSignedCertificate.pfx -inkey certificate.key -in certificate.crt -password pass:$pswd
# Encode pfx to base 64 string to reference in step 13. You may output this when you're ready to copy it with `Write-Host $pfxString`
$pfxBytes = Get-Content "selfSignedCertificate.pfx" -AsByteStream
$pfxString = [System.Convert]::ToBase64String($pfxBytes)
```
An API Key is required to secure communication with the Permissions API. We suggest using a random string at least 20 characters in length consisting of a good mix of uppercase, lowercase, numbers and special characters.

---

Expand All @@ -176,11 +158,11 @@ $pfxString = [System.Convert]::ToBase64String($pfxBytes)
- Name: TokenEncryptionKeyContainer
- Key type: RSA
- Key usage: Encryption
- RestApiClientCertificate
- Options: Upload
- Name: *.azurewebsites.net
- File upload: (The .pfx created in step 5)
- Password: (The password created in step 5)
- RestApiKey
- Options: Manual
- Name: RestApiKey
- Secret: (The API Key you created in step 5)
- Key usage: Signature

---

Expand Down Expand Up @@ -209,7 +191,7 @@ $pfxString = [System.Convert]::ToBase64String($pfxBytes)
| azureAdB2cTenantIdSecretValue| (Your Tenant Subscription ID found on your AD B2C dashboard)|
| azureAdB2cPermissionsApiClientIdSecretValue| (The Client ID found on your registered Permissions API app page)|
| azureAdB2cPermissionsApiClientSecretSecretValue| (The Client Secret Value created in step 8)|
| permissionsApiSslThumbprintSecretValue| (Thumbprint created in step 5)|
| permissionsApiApiKeySecretValue | (API Key created in step 5)|
| saasProviderName| (Select a provider name. This name will be used to name the Azure Resources. (e.g. contoso, myapp). Max Length is 8 characters.)|
| saasEnvironment| (Select an environment name. (e.g. 'prd', 'stg', 'dev', 'tst'))|
| saasInstanceNumber| (Select an instance number. This number will be appended to most Azure Resources created. (e.g. 001, 002, 003))|
Expand Down Expand Up @@ -241,7 +223,7 @@ $pfxString = [System.Convert]::ToBase64String($pfxBytes)
"azureAdB2cPermissionsApiClientSecretSecretValue": {
"value": "FooBar"
},
"permissionsApiSslThumbprintSecretValue": {
"permissionsApiApiKeySecretValue": {
"value": "FooBar"
},
"saasProviderName": {
Expand Down Expand Up @@ -322,8 +304,7 @@ $pfxString = [System.Convert]::ToBase64String($pfxBytes)
| azureAdB2cSignupAdminClientSecretSecretValue| (Secret value created in step 4)|
| azureAdB2cTenantIdSecretValue| (Your Tenant Subscription ID found on your AD B2C dashboard)|
| permissionsApiHostName| (The FQDN of the Permissions API) |
| permissionsApiCertificateSecretValue| (Certificate encoded to base64 string generated in step 5)|
| permissionsApiCertificatePassphraseSecretValue| (Certificate password generated in step 5)|
| permissionsApiApiKeySecretValue | (API Key generated in step 5)|
| saasAppApiScopes| (space delimited string of SaaS App Api scope names (e.g. "test.scope tenant.delete tenant.global.delete tenant.global.read tenant.global.write tenant.read tenant.write")) |
| saasProviderName| (created in step 8) |
| saasEnvironment| (created in step 8)|
Expand Down Expand Up @@ -377,18 +358,12 @@ $pfxString = [System.Convert]::ToBase64String($pfxBytes)
"azureAdB2cPermissionsApiClientSecretSecretValue": {
"value": "FooBar"
},
"permissionsApiSslThumbprintSecretValue": {
"value": "FooBar"
},
"permissionsApiHostName": {
"value": "https://asdkdemopermissions.com"
},
"permissionsApiCertificateSecretValue": {
"permissionsApiApiKeySecretValue": {
"value": "FooBar"
},
"permissionsApiCertificatePassphraseSecretValue": {
"value": "FooBar1"
},
"saasAppApiScopes": {
"value": "test.scope tenant.delete tenant.global.delete tenant.global.read tenant.global.write tenant.read tenant.write"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ When deployed to Azure, the application is configured to load its secrets from [

### Authentication

The Permissions Service is secured using [TLS Mutual Authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-6.0#configure-certificate-validation) (Certificate Authentication). The application layer is configured to verify the authenticity of the certificate by comparing the thumbprint of the certificate against a configuration value. For TLS Mutual Authentication to work properly, the web server must also be configured to forward the certificate to the application layer. This is done for you if you deploy the application following the steps in the [Quick Start](../../../quick-start) guide, but if you choose to run the code in an environment you provision, you will need to do [this configuration](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth) yourself.

> **Important!**
> The certificate that gets deployed out of the box by following the Quick Start is a self signed certificate. Self signed certificates are not meant for production use, and it is highly recommended that you replace this certificate with one signed by a Certificate Authority before using this module in production.
The Permissions Service is secured using API Key Authentication. The API Key is set using the `AppSettings:ApiKey` secret and there is middleware on the API that will verify that all incoming requests have an API key that matches on the `x-api-key` header. If you deploy the application following the steps in the [Quick Start](../../../quick-start) guide, an API key is randomly generated for you and uploaded to Azure Key Vault.

### Database

Expand All @@ -54,10 +51,7 @@ The Permissions Service uses [Swashbuckle](https://www.nuget.org/packages/Swashb

## FAQ and Design Considerations

- Q: Why did we choose to secure the permissions service with certificate authentication over API Keys/JWT Tokens/Another Method?
- A: The communication between Azure AD B2C (our default Identity Provider) and the permissions service must be secured with either [Basic or Certificate Auth](https://docs.microsoft.com/en-us/azure/active-directory-b2c/add-api-connector-token-enrichment?pivots=b2c-custom-policy#configure-the-restful-api-technical-profile) and it is not considered best practice to use Basic authentication in a production environment.

- Permissions are stored in the database in a single table (dbo.Permissions) with 3 pieces of data: Tenant ID, User ID (Email), and PermissionString. All 3 together make the row unique (i.e., you cannot have the same Permission for the same user on the same tenant more than once). Permissions are stored as a string (ex: Admin, User.Read, User.Write) for simplicity and extensibility. You may choose to store these in a separate database table and reference them by ID number if you have a large number of permissions and you want to enforce the types of permissions being assigned.
- We have purposefully chosen to flow all CRUD operations on permissions through the [Admin Service](../../../components/admin-service). This is for a number of reasons:
1. It removes the burden of authorization from the permissions service. All the permissions service needs to worry about is accepting a valid certificate, which only the identity provider and admin service possess. For higher security applications, you may choose to preform more authorization checks before adding permissions
1. It removes the burden of authorization from the permissions service. All the permissions service needs to worry about is accepting a valid API Key, which only the identity provider and admin service possess. For higher security applications, you may choose to preform more authorization checks before adding permissions
2. It simplifies the architecture. The frontend applications do not need to have any knowledge of the permissions service existing. When a tenant is created, the applications make 1 call to the admin service, and it handles the subsequent call to update the permissions records.
6 changes: 3 additions & 3 deletions docs/azure-saas-docs/content/en/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ Requirements:
Running our pre-built docker image is the recommended way to set up the identity framework, as the image comes pre-installed with all the dependencies necessary for execution. To start, run the following commands:

```bash
docker pull ghcr.io/azure/azure-saas/asdk-identity-setup:v1.0
docker run -it -v "$(pwd):/data" --name asdk-b2c-deployment ghcr.io/azure/azure-saas/asdk-identity-setup:v1.0
docker pull ghcr.io/azure/azure-saas/asdk-identity-setup:v1.1
docker run -it -v "$(pwd):/data" --name asdk-b2c-deployment ghcr.io/azure/azure-saas/asdk-identity-setup:v1.1
```

This will automatically pull and run the container image and its entrypoint is the [B2C-Create](https://github.com/Azure/azure-saas/blob/main/src/Saas.Identity/Saas.IdentityProvider/scripts/B2C-Create.ps1) powershell script.
Expand Down Expand Up @@ -86,7 +86,7 @@ Deploying to Azure is easy thanks to our pre-configured ARM (Azure Resource Mana

This button will take you to the Azure portal and will pass it the ARM template. You will need the parameters file output from step 1.

1. Click here: [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fv1.0%2Fsrc%2FSaas.IaC%2Fmain.json).
1. Click here: [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Fazure-saas%2Fv1.1%2Fsrc%2FSaas.IaC%2Fmain.json).
2. Select "Edit Parameters".
3. Select "Load File" and upload the `parameters.json` file output from the Identity Framework Deployment (step 1 above). Click "Save".
4. From the dropdown, select the subscription and resource group you'd like to deploy the resources to.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 1 addition & 23 deletions src/Saas.Admin/Saas.Admin.Service/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@
new DefaultAzureCredential(),
new CustomPrefixKeyVaultSecretManager("admin"));

// Get certificate from secret imported above and parse it into an X509Certificate
permissionsApiCertificate = new X509Certificate2(Convert.FromBase64String(builder.Configuration["KeyVault:PermissionsApiCert"]), builder.Configuration["KeyVault:PermissionsApiCertPassphrase"]);
}
else
{
// If running locally, you must first set the certificate as a base 64 encoded string in your .NET secrets manager.
var certString = builder.Configuration["PermissionsApi:LocalCertificate"];
permissionsApiCertificate = new X509Certificate2(Convert.FromBase64String(certString));
}

builder.Services.AddDbContext<TenantsContext>(options =>
Expand Down Expand Up @@ -108,24 +100,10 @@
builder.Services.AddScoped<IPermissionService, PermissionService>();

builder.Services.AddHttpClient<IPermissionServiceClient, PermissionServiceClient>()
// Configure outgoing HTTP requests to include certificate for permissions API
.ConfigurePrimaryHttpMessageHandler(() =>
{
HttpClientHandler handler = new HttpClientHandler();
handler.ClientCertificates.Add(permissionsApiCertificate);
return handler;
})
.ConfigureHttpClient(options =>
{
options.BaseAddress = new Uri(builder.Configuration["PermissionsApi:BaseUrl"]);
if (builder.Environment.IsDevelopment())
{
// The permissions API expects the certificate to be provided to the application layer by the web server after the TLS handshake
// Since this doesn't happen locally, we need to do it ourselves
options.DefaultRequestHeaders.Add("X-ARR-ClientCert", Convert.ToBase64String(permissionsApiCertificate.GetRawCertData()));
}
options.DefaultRequestHeaders.Add("x-api-key", builder.Configuration["PermissionsApi:ApiKey"]);
});

// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
Expand Down
3 changes: 1 addition & 2 deletions src/Saas.Admin/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,11 @@ Default values for non secret app settings can be found in [appsettings.json](Sa
| ClaimToRoleTransformer:RoleClaimtype | Type of the claim to use in the new Identity, works alongside built-in | false | MyCustomRoles |
| ClaimToRoleTransformer:SourceClaimType | Name of the claim custom roles are in | false | permissions |
| ConnectionStrings:TenantsContext | Connection String to SQL server database used to store permission data. | true | (localdb connnection string) |
| KeyVault:PermissionsApiCert | The name of the secret in Azure Key Vault that contains a base64 encoded certificate to use for authentication with the permissions api | false | |
| KeyVault:Url | KeyVault URL to pull secret values from in production | false | |
| Logging:LogLevel:Default | Logging level when no configured provider is matched | false | Information |
| Logging:LogLevel:Microsoft.AspNetCore | Logging level for AspNetCore logging | false | Warning |
| PermissionsApi:BaseUrl | URL for downstream [Permissions API](../Saas.Identity/Saas.Permissions/readme.md) | false | |
| PermissionsApi:LocalCertificate | A Base64 encoded certificate (.CER) used to authenticate with the permissions API. | true | |
| PermissionsApi:ApiKey | API Key to use for authentication with the downstream [Permissions API](../Saas.Identity/Saas.Permissions/readme.md) | true | |

### iv. Starting the App

Expand Down
31 changes: 7 additions & 24 deletions src/Saas.IaC/keyVault.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,11 @@ param keyVaultName string
@description('The location for all resources.')
param location string

@description('The name of the Permissions Api Certificate Key Vault Secret.')
param permissionsApiCertificateSecretName string = 'admin-KeyVault--PermissionsApiCert'
@description('The name of the Permissions Api Api Key Key Vault Secret.')
param permissionsApiApiKeySecretName string = 'admin-PermissionsApi--ApiKey'

@description('The value of the Permissions Api Certificate Key Vault Secret.')
param permissionsApiCertificateSecretValue string


@description('The name of the Permissions Api Certificate Passphrase Key Vault Secret.')
param permissionsApiCertificatePassphraseSecretName string = 'admin-KeyVault--PermissionsApiCertPassphrase'

@description('The value of the Permissions Api Certificate Passphrase Key Vault Secret.')
param permissionsApiCertificatePassphraseSecretValue string
@description('The value of the Permissions Api Api Key Key Vault Secret.')
param permissionsApiApiKeySecretValue string


// Resource - Key Vault
Expand All @@ -114,21 +107,11 @@ resource keyVault 'Microsoft.KeyVault/vaults@2021-11-01-preview' = {

// Resource - Key Vault - Secret - Permissions Api Certificate
//////////////////////////////////////////////////
resource permissionsApiCertificateSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
parent: keyVault
name: permissionsApiCertificateSecretName
properties: {
value: permissionsApiCertificateSecretValue
}
}

// Resource - Key Vault - Secret - Permissions Api Certificate Passphrase
//////////////////////////////////////////////////
resource permissionsApiCertificatePassphraseSecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
resource permissionsApiApiKeySecret 'Microsoft.KeyVault/vaults/secrets@2021-11-01-preview' = {
parent: keyVault
name: permissionsApiCertificatePassphraseSecretName
name: permissionsApiApiKeySecretName
properties: {
value: permissionsApiCertificatePassphraseSecretValue
value: permissionsApiApiKeySecretValue
}
}

Expand Down
16 changes: 6 additions & 10 deletions src/Saas.IaC/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ param adminApiScopes string
param adminApiScopeBaseUrl string

@description('The tag of the container image to deploy to the Admin api app service.')
param adminApiContainerImageTag string = 'ghcr.io/azure/azure-saas/asdk-admin:v1.0'
param adminApiContainerImageTag string = 'ghcr.io/azure/azure-saas/asdk-admin:v1.1'

@description('The tag of the container image to deploy to the SaaS Application api app service.')
param applicationContainerImageTag string = 'ghcr.io/azure/azure-saas/asdk-web:v1.0'
param applicationContainerImageTag string = 'ghcr.io/azure/azure-saas/asdk-web:v1.1'

@description('The value of the Azure AD B2C Admin Api Client Id Key Vault Secret.')
param azureAdB2cAdminApiClientIdSecretValue string
Expand Down Expand Up @@ -57,18 +57,15 @@ param location string = resourceGroup().location
@description('The Host Name of the Permissions Api to point the Admin Api to.')
param permissionsApiHostName string

@description('The base64 encoded certificate to save in the keyvault for securing communication with the permissions API.')
param permissionsApiCertificateSecretValue string

@description('The passphrase fopr the certificate to save in the keyvault for securing communication with the permissions API.')
@description('The API Key used to authenticate with the Permissions Api.')
@secure()
param permissionsApiCertificatePassphraseSecretValue string
param permissionsApiApiKeySecretValue string

@description('Scopes to authorize SaaS App user for the admin service.')
param saasAppApiScopes string

@description('The tag of the container image to deploy to the SignupAdmin app service.')
param signupAdminContainerImageTag string = 'ghcr.io/azure/azure-saas/asdk-signup:v1.0'
param signupAdminContainerImageTag string = 'ghcr.io/azure/azure-saas/asdk-signup:v1.1'

@description('The SaaS Provider name.')
param saasProviderName string
Expand Down Expand Up @@ -163,8 +160,7 @@ module keyVaultModule 'keyVault.bicep' = {
azureAdB2cTenantIdSecretValue: azureAdB2cTenantIdSecretValue
keyVaultName: keyVaultName
location: location
permissionsApiCertificateSecretValue: permissionsApiCertificateSecretValue
permissionsApiCertificatePassphraseSecretValue: permissionsApiCertificatePassphraseSecretValue
permissionsApiApiKeySecretValue: permissionsApiApiKeySecretValue
}
}

Expand Down
Loading

0 comments on commit 14d1f53

Please sign in to comment.