Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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).
* 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).
3 changes: 3 additions & 0 deletions infra/aca.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
91 changes: 91 additions & 0 deletions infra/appregistration.bicep
Original file line number Diff line number Diff line change
@@ -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/[email protected]' existing = {
appId: msGraphAppId
}

var graphScopes = msGraphSP.oauth2PermissionScopes
resource clientApp 'Microsoft.Graph/[email protected]' = {
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 '[email protected]' = {
name: '${clientApp.uniqueName}/miAsFic'
audiences: [
audiences[cloudEnvironment].uri
]
issuer: issuer
subject: webAppIdentityId
}
}

resource clientSp 'Microsoft.Graph/[email protected]' = {
appId: clientApp.appId
}

output clientAppId string = clientApp.appId
output clientSpId string = clientSp.id
62 changes: 62 additions & 0 deletions infra/appupdate.bicep
Original file line number Diff line number Diff line change
@@ -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
}
: {}
}
}
}
}
9 changes: 9 additions & 0 deletions infra/bicepconfig.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
28 changes: 28 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -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'
Expand Down
3 changes: 3 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
},
"disableKeyBasedAuth": {
"value": "${DISABLE_KEY_BASED_AUTH=true}"
},
"serviceManagementReference": {
"value": "${AZURE_SERVICE_MANAGEMENT_REFERENCE}"
}
}
}
Loading