diff --git a/README.md b/README.md index 40d947a..36700fd 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,12 @@ since the local app needs credentials for Azure AI to work properly. ## Important Security Notice -This template, the application code and configuration it contains, has been built to showcase Microsoft Azure specific services and tools. We strongly advise our customers not to make this code part of their production environments without implementing or enabling additional security features. When you deploy this app, it will be **publicly accessible on the internet**. See [Security Guidelines](#security-guidelines) for more information on how to secure your deployment. +This template, the application code and configuration it contains, has been built to showcase Microsoft Azure specific services and tools. We strongly advise our customers not to make this code part of their production environments without implementing or enabling additional security features. See [Security Guidelines](#security-guidelines) for more information on how to secure your deployment. ## Features * A Python [Quart](https://quart.palletsprojects.com/en/latest/) that uses the [Azure AI Inference SDK](https://learn.microsoft.com/python/api/overview/azure/ai-inference-readme?view=azure-python-preview) package to generate responses to user messages. -* A basic HTML/JS frontend that streams responses from the backend using [JSON Lines](http://jsonlines.org/) over a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream). +* A basic HTML/JS frontend that streams responses from the backend using [JSON Lines](http://jsonlines.org/) over a [ReadableStream](https://developer.mozilla.org/docs/Web/API/ReadableStream). * [Bicep files](https://docs.microsoft.com/azure/azure-resource-manager/bicep/) for provisioning Azure resources, including Azure AI Services, Azure Container Apps, Azure Container Registry, Azure Log Analytics, and RBAC roles. ![Screenshot of the chat app](docs/screenshot_chatapp.png) @@ -126,7 +126,7 @@ Once you've opened the project in [Codespaces](#github-codespaces), in [Dev Cont azd up ``` - It will prompt you to provide an `azd` environment name (like "chat-app"), select a subscription from your Azure account, and select a [location where DeepSeek-R1 is available](https://learn.microsoft.com/en-us/azure/ai-studio/how-to/deploy-models-serverless-availability#deepseek-models-from-microsoft) (like "westus"). Then it will provision the resources in your account and deploy the latest code. If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the Azure AI resource. + It will prompt you to provide an `azd` environment name (like "chat-app"), select a subscription from your Azure account, and select a [location where DeepSeek-R1 is available](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-serverless-availability#deepseek-models-from-microsoft) (like "westus"). Then it will provision the resources in your account and deploy the latest code. If you get an error or timeout with deployment, changing the location can help, as there may be availability constraints for the Azure AI resource. 3. When `azd` has finished deploying, you'll see an endpoint URI in the command output. Visit that URI, and you should see the chat app! 🎉 4. Remember to take down your app once you're no longer using it, either by deleting the resource group in the Portal or running this command: @@ -197,9 +197,11 @@ either by deleting the resource group in the Portal or running `azd down`. This template uses [Managed Identity](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview) for authenticating to the Azure OpenAI service. +This template also enables the Container Apps [built-in authentication feature](https://learn.microsoft.com/azure/container-apps/authentication) with a Microsoft Entra ID identity provider. The Bicep files use the new [Microsoft Graph extension (public preview)](https://learn.microsoft.com/graph/templates/overview-bicep-templates-for-graph) to create the Entra application registration using [managed identity with Federated Identity Credentials](https://learn.microsoft.com/azure/container-apps/managed-identity), so that no client secrets or certificates are necessary. + Additionally, we have added a [GitHub Action](https://github.com/microsoft/security-devops-action) that scans the infrastructure-as-code files and generates a report containing any detected issues. To ensure continued best practices in your own repository, we recommend that anyone creating solutions based on our templates ensure that the [Github secret scanning](https://docs.github.com/code-security/secret-scanning/about-secret-scanning) setting is enabled. You may want to consider additional security measures, such as: * Protecting the Azure Container Apps instance with a [firewall](https://learn.microsoft.com/azure/container-apps/waf-app-gateway) and/or [Virtual Network](https://learn.microsoft.com/azure/container-apps/networking?tabs=workload-profiles-env%2Cazure-cli). -* Adding user login to the app, to restrict access only to users within your organization. See [this example for adding user login with the built-in auth feature of Container Apps](https://github.com/Azure-Samples/openai-chat-app-entra-auth-builtin). \ No newline at end of file +* Enabling Microsoft Defender for Cloud on the resource group and setting up [security policies](https://learn.microsoft.com/azure/defender-for-cloud/security-policy-concept). \ No newline at end of file diff --git a/infra/aca.bicep b/infra/aca.bicep index b57c1f4..d5837ca 100644 --- a/infra/aca.bicep +++ b/infra/aca.bicep @@ -47,6 +47,9 @@ module app 'core/host/container-app-upsert.bicep' = { containerRegistryName: containerRegistryName env: env targetPort: 50505 + secrets: { + 'override-use-mi-fic-assertion-client-id': acaIdentity.properties.clientId + } } } diff --git a/infra/appregistration.bicep b/infra/appregistration.bicep new file mode 100644 index 0000000..ed99dc6 --- /dev/null +++ b/infra/appregistration.bicep @@ -0,0 +1,91 @@ +extension microsoftGraphV1 + +@description('Specifies the name of cloud environment to run this deployment in.') +param cloudEnvironment string = environment().name + +// NOTE: Microsoft Graph Bicep file deployment is only supported in Public Cloud +@description('Audience uris for public and national clouds') +param audiences object = { + AzureCloud: { + uri: 'api://AzureADTokenExchange' + } + AzureUSGovernment: { + uri: 'api://AzureADTokenExchangeUSGov' + } + USNat: { + uri: 'api://AzureADTokenExchangeUSNat' + } + USSec: { + uri: 'api://AzureADTokenExchangeUSSec' + } + AzureChinaCloud: { + uri: 'api://AzureADTokenExchangeChina' + } +} + +@description('Specifies the ID of the user-assigned managed identity.') +param webAppIdentityId string + +@description('Specifies the unique name for the client application.') +param clientAppName string + +@description('Specifies the display name for the client application') +param clientAppDisplayName string + +@description('Specifies the scopes that the client application requires.') +param clientAppScopes array = ['User.Read', 'offline_access', 'openid', 'profile'] + +param serviceManagementReference string = '' + +param issuer string + +param webAppEndpoint string + +// Get the MS Graph Service Principal based on its application ID: +// https://learn.microsoft.com/troubleshoot/entra/entra-id/governance/verify-first-party-apps-sign-in +var msGraphAppId = '00000003-0000-0000-c000-000000000000' +resource msGraphSP 'Microsoft.Graph/servicePrincipals@v1.0' existing = { + appId: msGraphAppId +} + +var graphScopes = msGraphSP.oauth2PermissionScopes +resource clientApp 'Microsoft.Graph/applications@v1.0' = { + uniqueName: clientAppName + displayName: clientAppDisplayName + signInAudience: 'AzureADMyOrg' + serviceManagementReference: empty(serviceManagementReference) ? null : serviceManagementReference + web: { + redirectUris: [ + 'http://localhost:50505/.auth/login/aad/callback' + '${webAppEndpoint}/.auth/login/aad/callback' + ] + implicitGrantSettings: { enableIdTokenIssuance: true } + } + requiredResourceAccess: [ + { + resourceAppId: msGraphAppId + resourceAccess: [ + for (scope, i) in clientAppScopes: { + id: filter(graphScopes, graphScopes => graphScopes.value == scope)[0].id + type: 'Scope' + } + ] + } + ] + + resource clientAppFic 'federatedIdentityCredentials@v1.0' = { + name: '${clientApp.uniqueName}/miAsFic' + audiences: [ + audiences[cloudEnvironment].uri + ] + issuer: issuer + subject: webAppIdentityId + } +} + +resource clientSp 'Microsoft.Graph/servicePrincipals@v1.0' = { + appId: clientApp.appId +} + +output clientAppId string = clientApp.appId +output clientSpId string = clientSp.id diff --git a/infra/appupdate.bicep b/infra/appupdate.bicep new file mode 100644 index 0000000..c87c94d --- /dev/null +++ b/infra/appupdate.bicep @@ -0,0 +1,62 @@ +metadata description = 'Creates an Azure Container Apps Auth Config using Microsoft Entra as Identity Provider.' + +@description('The name of the container apps resource within the current resource group scope') +param containerAppName string + +@description('The client ID of the Microsoft Entra application.') +param clientId string + +param openIdIssuer string + +@description('Enable token store for the Container App.') +param includeTokenStore bool = false + +@description('The URI of the Azure Blob Storage container to be used for token storage.') +param blobContainerUri string = '' +@description('The resource ID of the managed identity to be used for accessing the Azure Blob Storage.') +param appIdentityResourceId string = '' + +resource app 'Microsoft.App/containerApps@2023-05-01' existing = { + name: containerAppName +} + +resource auth 'Microsoft.App/containerApps/authConfigs@2024-10-02-preview' = { + parent: app + name: 'current' + properties: { + platform: { + enabled: true + } + globalValidation: { + redirectToProvider: 'azureactivedirectory' + unauthenticatedClientAction: 'RedirectToLoginPage' + } + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + clientId: clientId + clientSecretSettingName: 'override-use-mi-fic-assertion-client-id' + openIdIssuer: openIdIssuer + } + validation: { + defaultAuthorizationPolicy: { + allowedApplications: [] + } + } + } + } + login: { + // https://learn.microsoft.com/azure/container-apps/token-store + tokenStore: { + enabled: includeTokenStore + azureBlobStorage: includeTokenStore + ? { + blobContainerUri: blobContainerUri + managedIdentityResourceId: appIdentityResourceId + } + : {} + } + } + } +} diff --git a/infra/bicepconfig.json b/infra/bicepconfig.json new file mode 100644 index 0000000..17492ab --- /dev/null +++ b/infra/bicepconfig.json @@ -0,0 +1,9 @@ +{ + "experimentalFeaturesEnabled": { + "extensibility": true + }, + // specify an alias for the version of the v1.0 dynamic types package you want to use + "extensions": { + "microsoftGraphV1": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.8-preview" + } +} diff --git a/infra/main.bicep b/infra/main.bicep index 19a2514..cdf701e 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -36,6 +36,9 @@ param disableKeyBasedAuth bool = true // Parameters for the specific Azure AI deployment: param aiServicesDeploymentName string = 'DeepSeek-R1' +@description('Service Management Reference for the Entra app registration') +param serviceManagementReference string = '' + var resourceToken = toLower(uniqueString(subscription().id, name, location)) var tags = { 'azd-env-name': name } @@ -125,6 +128,31 @@ module aca 'aca.bicep' = { } } +var issuer = '${environment().authentication.loginEndpoint}${tenant().tenantId}/v2.0' +module registration 'appregistration.bicep' = { + name: 'reg' + scope: resourceGroup + params: { + clientAppName: '${prefix}-entra-client-app' + clientAppDisplayName: 'DeepSeek Entra Client App' + webAppEndpoint: aca.outputs.uri + webAppIdentityId: aca.outputs.identityPrincipalId + issuer: issuer + serviceManagementReference: serviceManagementReference + } +} + +module appupdate 'appupdate.bicep' = { + name: 'appupdate' + scope: resourceGroup + params: { + containerAppName: aca.outputs.name + clientId: registration.outputs.clientAppId + openIdIssuer: issuer + includeTokenStore: false + } +} + module aiServicesRoleBackend 'core/security/role.bicep' = { scope: resourceGroup name: 'aiservices-role-backend' diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 6db3299..32912f0 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -19,6 +19,9 @@ }, "disableKeyBasedAuth": { "value": "${DISABLE_KEY_BASED_AUTH=true}" + }, + "serviceManagementReference": { + "value": "${AZURE_SERVICE_MANAGEMENT_REFERENCE}" } } }