From eea365032b28692e4f1c6eea7790ea42fbc1043c Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Tue, 26 Nov 2024 17:52:11 -0800 Subject: [PATCH 1/5] initial changes to support ACA built-in auth --- app/backend/app.py | 8 ++- infra/core/host/appservice.bicep | 7 +-- infra/core/host/container-app-upsert.bicep | 17 +++--- infra/core/host/container-apps-auth.bicep | 63 ++++++++++++++++++++++ infra/main.bicep | 50 ++++++++++++++--- 5 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 infra/core/host/container-apps-auth.bicep diff --git a/app/backend/app.py b/app/backend/app.py index b83efefe62..1ab1ff9a10 100644 --- a/app/backend/app.py +++ b/app/backend/app.py @@ -102,6 +102,7 @@ @bp.route("/") async def index(): + # TODO: use msal loginRedirect on a blankish page return await bp.send_static_file("index.html") @@ -753,6 +754,9 @@ def create_app(): logging.getLogger("scripts").setLevel(app_level) if allowed_origin := os.getenv("ALLOWED_ORIGIN"): - app.logger.info("ALLOWED_ORIGIN is set, enabling CORS for %s", allowed_origin) - cors(app, allow_origin=allowed_origin, allow_methods=["GET", "POST"]) + allowed_origins = allowed_origin.split(";") + if len(allowed_origins) > 0: + app.logger.info("CORS enabled for %s", allowed_origins) + cors(app, allow_origin=allowed_origins, allow_methods=["GET", "POST"]) + return app diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep index cd00e3f774..4bf13b3cb3 100644 --- a/infra/core/host/appservice.bicep +++ b/infra/core/host/appservice.bicep @@ -50,11 +50,6 @@ param publicNetworkAccess string = 'Enabled' param enableUnauthenticatedAccess bool = false param disableAppServicesAuthentication bool = false -var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ] -var loginEndpoint = environment().authentication.loginEndpoint -var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint -var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins - // .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly // Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ] @@ -72,7 +67,7 @@ var coreConfig = { functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null healthCheckPath: healthCheckPath cors: { - allowedOrigins: union(allMsftAllowedOrigins, allowedOrigins) + allowedOrigins: allowedOrigins } } diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep index ac2c77db1d..bb4a1860e9 100644 --- a/infra/core/host/container-app-upsert.bicep +++ b/infra/core/host/container-app-upsert.bicep @@ -67,6 +67,9 @@ param keyvaultIdentities object = {} @description('The environment variables for the container in key value pairs') param env object = {} +@description('The environment variables with secret references') +param envSecrets array = [] + @description('Specifies if the resource ingress is exposed externally') param external bool = true @@ -85,6 +88,13 @@ resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing = name: name } +var envAsArray = [ + for key in objectKeys(env): { + name: key + value: '${env[key]}' + } +] + module app 'container-app.bicep' = { name: '${deployment().name}-update' params: { @@ -110,12 +120,7 @@ module app 'container-app.bicep' = { keyvaultIdentities: keyvaultIdentities allowedOrigins: allowedOrigins external: external - env: [ - for key in objectKeys(env): { - name: key - value: '${env[key]}' - } - ] + env: concat(envAsArray, envSecrets) imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : '' targetPort: targetPort serviceBinds: serviceBinds diff --git a/infra/core/host/container-apps-auth.bicep b/infra/core/host/container-apps-auth.bicep new file mode 100644 index 0000000000..5dd4629b25 --- /dev/null +++ b/infra/core/host/container-apps-auth.bicep @@ -0,0 +1,63 @@ +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 name string + +param additionalScopes array = [] +param additionalAllowedAudiences array = [] +param allowedApplications array = [] + +param clientAppId string = '' +param serverAppId string = '' +@secure() +param clientSecretSettingName string = '' +param authenticationIssuerUri string = '' +param enableUnauthenticatedAccess bool = false + +// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly +// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent +var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ] +var requiredAudiences = [ 'api://${serverAppId}' ] + +resource app 'Microsoft.App/containerApps@2023-05-01' existing = { + name: name +} + +resource auth 'Microsoft.App/containerApps/authConfigs@2024-03-01' = { + parent: app + name: 'current' + properties: { + platform: { + enabled: true + } + globalValidation: { + redirectToProvider: 'azureactivedirectory' + unauthenticatedClientAction: enableUnauthenticatedAccess ? 'AllowAnonymous' : 'RedirectToLoginPage' + } + identityProviders: { + azureActiveDirectory: { + enabled: true + registration: { + clientId: clientAppId + clientSecretSettingName: clientSecretSettingName + openIdIssuer: authenticationIssuerUri + } + login: { + loginParameters: [ 'scope=${join(union(requiredScopes, additionalScopes), ' ')}' ] + } + validation: { + allowedAudiences: union(requiredAudiences, additionalAllowedAudiences) + defaultAuthorizationPolicy: { + allowedApplications: allowedApplications + } + } + } + } + /*login: { + https://learn.microsoft.com/en-us/azure/container-apps/token-store + tokenStore: { + enabled: true + } + }*/ + } +} diff --git a/infra/main.bicep b/infra/main.bicep index 344623df20..ed8ef662f3 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -248,6 +248,16 @@ param containerRegistryName string = deploymentTarget == 'containerapps' ? '${replace(environmentName, '-', '')}acr' : '' +// Configure CORS for allowing different web apps to use the backend +// For more information please see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS +var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ] +var loginEndpoint = environment().authentication.loginEndpoint +var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint +var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins +var allowedOrigins = union(split(allowedOrigin, ';'), allMsftAllowedOrigins) +// Filter out any empty origin strings and remove any duplicate origins +var allowedOriginsEnv = join(reduce(filter(allowedOrigins, o => length(trim(o)) > 0), [], (cur, next) => union(cur, [next])), ';') + // Organize resources in a resource group resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' @@ -376,14 +386,12 @@ var appEnvVariables = { AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS: enableGlobalDocuments AZURE_ENABLE_UNAUTHENTICATED_ACCESS: enableUnauthenticatedAccess AZURE_SERVER_APP_ID: serverAppId - AZURE_SERVER_APP_SECRET: serverAppSecret AZURE_CLIENT_APP_ID: clientAppId - AZURE_CLIENT_APP_SECRET: clientAppSecret AZURE_TENANT_ID: tenantId AZURE_AUTH_TENANT_ID: tenantIdForAuth AZURE_AUTHENTICATION_ISSUER_URI: authenticationIssuerUri // CORS support, for frontends on other hosts - ALLOWED_ORIGIN: allowedOrigin + ALLOWED_ORIGIN: allowedOriginsEnv USE_VECTORS: useVectors USE_GPT4V: useGPT4V USE_USER_UPLOAD: useUserUpload @@ -412,7 +420,7 @@ module backend 'core/host/appservice.bicep' = if (deploymentTarget == 'appservic managedIdentity: true virtualNetworkSubnetId: isolation.outputs.appSubnetId publicNetworkAccess: publicNetworkAccess - allowedOrigins: [allowedOrigin] + allowedOrigins: allowedOrigins clientAppId: clientAppId serverAppId: serverAppId enableUnauthenticatedAccess: enableUnauthenticatedAccess @@ -421,7 +429,10 @@ module backend 'core/host/appservice.bicep' = if (deploymentTarget == 'appservic authenticationIssuerUri: authenticationIssuerUri use32BitWorkerProcess: appServiceSkuName == 'F1' alwaysOn: appServiceSkuName != 'F1' - appSettings: appEnvVariables + appSettings: union(appEnvVariables, { + AZURE_SERVER_APP_SECRET: serverAppSecret + AZURE_CLIENT_APP_SECRET: clientAppSecret + }) } } @@ -472,11 +483,38 @@ module acaBackend 'core/host/container-app-upsert.bicep' = if (deploymentTarget targetPort: 8000 containerCpuCoreCount: '1.0' containerMemory: '2Gi' - allowedOrigins: [allowedOrigin] + allowedOrigins: allowedOrigins env: union(appEnvVariables, { // For using managed identity to access Azure resources. See https://github.com/microsoft/azure-container-apps/issues/442 AZURE_CLIENT_ID: (deploymentTarget == 'containerapps') ? acaIdentity.outputs.clientId : '' }) + secrets: { + azureclientappsecret: clientAppSecret + azureserverappsecret: serverAppSecret + } + envSecrets: [ + { + name: 'AZURE_CLIENT_APP_SECRET' + secretRef: 'azureclientappsecret' + } + { + name: 'AZURE_SERVER_APP_SECRET' + secretRef: 'azureclientappsecret' + } + ] + } +} + +module acaAuth 'core/host/container-apps-auth.bicep' = if (deploymentTarget == 'containerapps' && !empty(clientAppId)) { + name: 'aca-auth' + scope: resourceGroup + params: { + name: acaBackend.outputs.name + clientAppId: clientAppId + serverAppId: serverAppId + clientSecretSettingName: !empty(clientAppSecret) ? 'azureclientappsecret' : '' + authenticationIssuerUri: authenticationIssuerUri + enableUnauthenticatedAccess: enableUnauthenticatedAccess } } From 1fb4135d3e21d3ed493c87e5d57f8f8aec5369d1 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Thu, 28 Nov 2024 15:58:35 -0800 Subject: [PATCH 2/5] Get MSI token store working --- infra/core/host/container-app-upsert.bicep | 1 + infra/core/host/container-app.bicep | 1 + infra/core/host/container-apps-auth.bicep | 14 ++++++++++---- infra/main.bicep | 8 ++++++++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/infra/core/host/container-app-upsert.bicep b/infra/core/host/container-app-upsert.bicep index bb4a1860e9..aa162c3ade 100644 --- a/infra/core/host/container-app-upsert.bicep +++ b/infra/core/host/container-app-upsert.bicep @@ -133,3 +133,4 @@ output name string = app.outputs.name output uri string = app.outputs.uri output id string = app.outputs.id output identityPrincipalId string = app.outputs.identityPrincipalId +output identityResourceId string = app.outputs.identityResourceId diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep index 053f570745..6fcff1dfc4 100644 --- a/infra/core/host/container-app.bicep +++ b/infra/core/host/container-app.bicep @@ -178,6 +178,7 @@ resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' output defaultDomain string = containerAppsEnvironment.properties.defaultDomain output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId) +output identityResourceId string = normalizedIdentityType == 'UserAssigned' ? resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', userIdentity.name) : '' output imageName string = imageName output name string = app.name output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {} diff --git a/infra/core/host/container-apps-auth.bicep b/infra/core/host/container-apps-auth.bicep index 5dd4629b25..6552232093 100644 --- a/infra/core/host/container-apps-auth.bicep +++ b/infra/core/host/container-apps-auth.bicep @@ -13,6 +13,8 @@ param serverAppId string = '' param clientSecretSettingName string = '' param authenticationIssuerUri string = '' param enableUnauthenticatedAccess bool = false +param blobContainerUri string = '' +param appIdentityResourceId string = '' // .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly // Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent @@ -23,7 +25,7 @@ resource app 'Microsoft.App/containerApps@2023-05-01' existing = { name: name } -resource auth 'Microsoft.App/containerApps/authConfigs@2024-03-01' = { +resource auth 'Microsoft.App/containerApps/authConfigs@2024-10-02-preview' = { parent: app name: 'current' properties: { @@ -53,11 +55,15 @@ resource auth 'Microsoft.App/containerApps/authConfigs@2024-03-01' = { } } } - /*login: { - https://learn.microsoft.com/en-us/azure/container-apps/token-store + login: { + // https://learn.microsoft.com/en-us/azure/container-apps/token-store tokenStore: { enabled: true + azureBlobStorage: { + blobContainerUri: blobContainerUri + managedIdentityResourceId: appIdentityResourceId + } } - }*/ + } } } diff --git a/infra/main.bicep b/infra/main.bicep index ed8ef662f3..e9d8fec97b 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -40,6 +40,8 @@ param storageSkuName string // Set in main.parameters.json param userStorageAccountName string = '' param userStorageContainerName string = 'user-content' +param tokenStorageContainerName string = 'tokens' + param appServiceSkuName string // Set in main.parameters.json @allowed(['azure', 'openai', 'azure_custom']) @@ -515,6 +517,8 @@ module acaAuth 'core/host/container-apps-auth.bicep' = if (deploymentTarget == ' clientSecretSettingName: !empty(clientAppSecret) ? 'azureclientappsecret' : '' authenticationIssuerUri: authenticationIssuerUri enableUnauthenticatedAccess: enableUnauthenticatedAccess + blobContainerUri: 'https://${storageAccountName}.blob.${environment().suffixes.storage}/${tokenStorageContainerName}' + appIdentityResourceId: (deploymentTarget == 'appservice') ? '' : acaBackend.outputs.identityResourceId } } @@ -699,6 +703,10 @@ module storage 'core/storage/storage-account.bicep' = { name: storageContainerName publicAccess: 'None' } + { + name: tokenStorageContainerName + publicAccess: 'None' + } ] } } From a4adaba659a5f8acfea32ab61ad76f09f91375a0 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Fri, 29 Nov 2024 16:50:19 -0800 Subject: [PATCH 3/5] Add conditional for secrets --- infra/main.bicep | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index e9d8fec97b..2a7341a8ce 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -490,11 +490,11 @@ module acaBackend 'core/host/container-app-upsert.bicep' = if (deploymentTarget // For using managed identity to access Azure resources. See https://github.com/microsoft/azure-container-apps/issues/442 AZURE_CLIENT_ID: (deploymentTarget == 'containerapps') ? acaIdentity.outputs.clientId : '' }) - secrets: { + secrets: useAuthentication ? { azureclientappsecret: clientAppSecret azureserverappsecret: serverAppSecret - } - envSecrets: [ + } : {} + envSecrets: useAuthentication ? [ { name: 'AZURE_CLIENT_APP_SECRET' secretRef: 'azureclientappsecret' @@ -503,7 +503,7 @@ module acaBackend 'core/host/container-app-upsert.bicep' = if (deploymentTarget name: 'AZURE_SERVER_APP_SECRET' secretRef: 'azureclientappsecret' } - ] + ] : [] } } From 9089ba2e0f4b7d558ab3d7f07f6820eda3069559 Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Fri, 29 Nov 2024 17:01:03 -0800 Subject: [PATCH 4/5] Configure Azure Developer Pipeline From 65d47a16c46cdbc9c82d8f259164f4efc1a74c3c Mon Sep 17 00:00:00 2001 From: Pamela Fox Date: Sat, 30 Nov 2024 19:56:53 -0800 Subject: [PATCH 5/5] Fix secret name --- infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 2a7341a8ce..8616ffa1ee 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -501,7 +501,7 @@ module acaBackend 'core/host/container-app-upsert.bicep' = if (deploymentTarget } { name: 'AZURE_SERVER_APP_SECRET' - secretRef: 'azureclientappsecret' + secretRef: 'azureserverappsecret' } ] : [] }