Skip to content

Commit 829a7a6

Browse files
authored
Configure built-in auth for Azure Container Apps (#2205)
* initial changes to support ACA built-in auth * Get MSI token store working * Add conditional for secrets * Configure Azure Developer Pipeline * Fix secret name
1 parent 116258c commit 829a7a6

File tree

6 files changed

+141
-20
lines changed

6 files changed

+141
-20
lines changed

app/backend/app.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@
102102

103103
@bp.route("/")
104104
async def index():
105+
# TODO: use msal loginRedirect on a blankish page
105106
return await bp.send_static_file("index.html")
106107

107108

@@ -753,6 +754,9 @@ def create_app():
753754
logging.getLogger("scripts").setLevel(app_level)
754755

755756
if allowed_origin := os.getenv("ALLOWED_ORIGIN"):
756-
app.logger.info("ALLOWED_ORIGIN is set, enabling CORS for %s", allowed_origin)
757-
cors(app, allow_origin=allowed_origin, allow_methods=["GET", "POST"])
757+
allowed_origins = allowed_origin.split(";")
758+
if len(allowed_origins) > 0:
759+
app.logger.info("CORS enabled for %s", allowed_origins)
760+
cors(app, allow_origin=allowed_origins, allow_methods=["GET", "POST"])
761+
758762
return app

infra/core/host/appservice.bicep

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ param publicNetworkAccess string = 'Enabled'
5050
param enableUnauthenticatedAccess bool = false
5151
param disableAppServicesAuthentication bool = false
5252

53-
var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ]
54-
var loginEndpoint = environment().authentication.loginEndpoint
55-
var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint
56-
var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins
57-
5853
// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly
5954
// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent
6055
var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ]
@@ -72,7 +67,7 @@ var coreConfig = {
7267
functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null
7368
healthCheckPath: healthCheckPath
7469
cors: {
75-
allowedOrigins: union(allMsftAllowedOrigins, allowedOrigins)
70+
allowedOrigins: allowedOrigins
7671
}
7772
}
7873

infra/core/host/container-app-upsert.bicep

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ param keyvaultIdentities object = {}
6767
@description('The environment variables for the container in key value pairs')
6868
param env object = {}
6969

70+
@description('The environment variables with secret references')
71+
param envSecrets array = []
72+
7073
@description('Specifies if the resource ingress is exposed externally')
7174
param external bool = true
7275

@@ -85,6 +88,13 @@ resource existingApp 'Microsoft.App/containerApps@2023-05-02-preview' existing =
8588
name: name
8689
}
8790

91+
var envAsArray = [
92+
for key in objectKeys(env): {
93+
name: key
94+
value: '${env[key]}'
95+
}
96+
]
97+
8898
module app 'container-app.bicep' = {
8999
name: '${deployment().name}-update'
90100
params: {
@@ -110,12 +120,7 @@ module app 'container-app.bicep' = {
110120
keyvaultIdentities: keyvaultIdentities
111121
allowedOrigins: allowedOrigins
112122
external: external
113-
env: [
114-
for key in objectKeys(env): {
115-
name: key
116-
value: '${env[key]}'
117-
}
118-
]
123+
env: concat(envAsArray, envSecrets)
119124
imageName: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : ''
120125
targetPort: targetPort
121126
serviceBinds: serviceBinds
@@ -128,3 +133,4 @@ output name string = app.outputs.name
128133
output uri string = app.outputs.uri
129134
output id string = app.outputs.id
130135
output identityPrincipalId string = app.outputs.identityPrincipalId
136+
output identityResourceId string = app.outputs.identityResourceId

infra/core/host/container-app.bicep

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01'
178178

179179
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
180180
output identityPrincipalId string = normalizedIdentityType == 'None' ? '' : (empty(identityName) ? app.identity.principalId : userIdentity.properties.principalId)
181+
output identityResourceId string = normalizedIdentityType == 'UserAssigned' ? resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', userIdentity.name) : ''
181182
output imageName string = imageName
182183
output name string = app.name
183184
output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
metadata description = 'Creates an Azure Container Apps Auth Config using Microsoft Entra as Identity Provider.'
2+
3+
@description('The name of the container apps resource within the current resource group scope')
4+
param name string
5+
6+
param additionalScopes array = []
7+
param additionalAllowedAudiences array = []
8+
param allowedApplications array = []
9+
10+
param clientAppId string = ''
11+
param serverAppId string = ''
12+
@secure()
13+
param clientSecretSettingName string = ''
14+
param authenticationIssuerUri string = ''
15+
param enableUnauthenticatedAccess bool = false
16+
param blobContainerUri string = ''
17+
param appIdentityResourceId string = ''
18+
19+
// .default must be the 1st scope for On-Behalf-Of-Flow combined consent to work properly
20+
// Please see https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#default-and-combined-consent
21+
var requiredScopes = [ 'api://${serverAppId}/.default', 'openid', 'profile', 'email', 'offline_access' ]
22+
var requiredAudiences = [ 'api://${serverAppId}' ]
23+
24+
resource app 'Microsoft.App/containerApps@2023-05-01' existing = {
25+
name: name
26+
}
27+
28+
resource auth 'Microsoft.App/containerApps/authConfigs@2024-10-02-preview' = {
29+
parent: app
30+
name: 'current'
31+
properties: {
32+
platform: {
33+
enabled: true
34+
}
35+
globalValidation: {
36+
redirectToProvider: 'azureactivedirectory'
37+
unauthenticatedClientAction: enableUnauthenticatedAccess ? 'AllowAnonymous' : 'RedirectToLoginPage'
38+
}
39+
identityProviders: {
40+
azureActiveDirectory: {
41+
enabled: true
42+
registration: {
43+
clientId: clientAppId
44+
clientSecretSettingName: clientSecretSettingName
45+
openIdIssuer: authenticationIssuerUri
46+
}
47+
login: {
48+
loginParameters: [ 'scope=${join(union(requiredScopes, additionalScopes), ' ')}' ]
49+
}
50+
validation: {
51+
allowedAudiences: union(requiredAudiences, additionalAllowedAudiences)
52+
defaultAuthorizationPolicy: {
53+
allowedApplications: allowedApplications
54+
}
55+
}
56+
}
57+
}
58+
login: {
59+
// https://learn.microsoft.com/en-us/azure/container-apps/token-store
60+
tokenStore: {
61+
enabled: true
62+
azureBlobStorage: {
63+
blobContainerUri: blobContainerUri
64+
managedIdentityResourceId: appIdentityResourceId
65+
}
66+
}
67+
}
68+
}
69+
}

infra/main.bicep

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ param storageSkuName string // Set in main.parameters.json
4040
param userStorageAccountName string = ''
4141
param userStorageContainerName string = 'user-content'
4242

43+
param tokenStorageContainerName string = 'tokens'
44+
4345
param appServiceSkuName string // Set in main.parameters.json
4446

4547
@allowed(['azure', 'openai', 'azure_custom'])
@@ -248,6 +250,16 @@ param containerRegistryName string = deploymentTarget == 'containerapps'
248250
? '${replace(toLower(environmentName), '-', '')}acr'
249251
: ''
250252

253+
// Configure CORS for allowing different web apps to use the backend
254+
// For more information please see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
255+
var msftAllowedOrigins = [ 'https://portal.azure.com', 'https://ms.portal.azure.com' ]
256+
var loginEndpoint = environment().authentication.loginEndpoint
257+
var loginEndpointFixed = lastIndexOf(loginEndpoint, '/') == length(loginEndpoint) - 1 ? substring(loginEndpoint, 0, length(loginEndpoint) - 1) : loginEndpoint
258+
var allMsftAllowedOrigins = !(empty(clientAppId)) ? union(msftAllowedOrigins, [ loginEndpointFixed ]) : msftAllowedOrigins
259+
var allowedOrigins = union(split(allowedOrigin, ';'), allMsftAllowedOrigins)
260+
// Filter out any empty origin strings and remove any duplicate origins
261+
var allowedOriginsEnv = join(reduce(filter(allowedOrigins, o => length(trim(o)) > 0), [], (cur, next) => union(cur, [next])), ';')
262+
251263
// Organize resources in a resource group
252264
resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = {
253265
name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
@@ -376,14 +388,12 @@ var appEnvVariables = {
376388
AZURE_ENABLE_GLOBAL_DOCUMENT_ACCESS: enableGlobalDocuments
377389
AZURE_ENABLE_UNAUTHENTICATED_ACCESS: enableUnauthenticatedAccess
378390
AZURE_SERVER_APP_ID: serverAppId
379-
AZURE_SERVER_APP_SECRET: serverAppSecret
380391
AZURE_CLIENT_APP_ID: clientAppId
381-
AZURE_CLIENT_APP_SECRET: clientAppSecret
382392
AZURE_TENANT_ID: tenantId
383393
AZURE_AUTH_TENANT_ID: tenantIdForAuth
384394
AZURE_AUTHENTICATION_ISSUER_URI: authenticationIssuerUri
385395
// CORS support, for frontends on other hosts
386-
ALLOWED_ORIGIN: allowedOrigin
396+
ALLOWED_ORIGIN: allowedOriginsEnv
387397
USE_VECTORS: useVectors
388398
USE_GPT4V: useGPT4V
389399
USE_USER_UPLOAD: useUserUpload
@@ -412,7 +422,7 @@ module backend 'core/host/appservice.bicep' = if (deploymentTarget == 'appservic
412422
managedIdentity: true
413423
virtualNetworkSubnetId: isolation.outputs.appSubnetId
414424
publicNetworkAccess: publicNetworkAccess
415-
allowedOrigins: [allowedOrigin]
425+
allowedOrigins: allowedOrigins
416426
clientAppId: clientAppId
417427
serverAppId: serverAppId
418428
enableUnauthenticatedAccess: enableUnauthenticatedAccess
@@ -421,7 +431,10 @@ module backend 'core/host/appservice.bicep' = if (deploymentTarget == 'appservic
421431
authenticationIssuerUri: authenticationIssuerUri
422432
use32BitWorkerProcess: appServiceSkuName == 'F1'
423433
alwaysOn: appServiceSkuName != 'F1'
424-
appSettings: appEnvVariables
434+
appSettings: union(appEnvVariables, {
435+
AZURE_SERVER_APP_SECRET: serverAppSecret
436+
AZURE_CLIENT_APP_SECRET: clientAppSecret
437+
})
425438
}
426439
}
427440

@@ -472,11 +485,40 @@ module acaBackend 'core/host/container-app-upsert.bicep' = if (deploymentTarget
472485
targetPort: 8000
473486
containerCpuCoreCount: '1.0'
474487
containerMemory: '2Gi'
475-
allowedOrigins: [allowedOrigin]
488+
allowedOrigins: allowedOrigins
476489
env: union(appEnvVariables, {
477490
// For using managed identity to access Azure resources. See https://github.com/microsoft/azure-container-apps/issues/442
478491
AZURE_CLIENT_ID: (deploymentTarget == 'containerapps') ? acaIdentity.outputs.clientId : ''
479492
})
493+
secrets: useAuthentication ? {
494+
azureclientappsecret: clientAppSecret
495+
azureserverappsecret: serverAppSecret
496+
} : {}
497+
envSecrets: useAuthentication ? [
498+
{
499+
name: 'AZURE_CLIENT_APP_SECRET'
500+
secretRef: 'azureclientappsecret'
501+
}
502+
{
503+
name: 'AZURE_SERVER_APP_SECRET'
504+
secretRef: 'azureserverappsecret'
505+
}
506+
] : []
507+
}
508+
}
509+
510+
module acaAuth 'core/host/container-apps-auth.bicep' = if (deploymentTarget == 'containerapps' && !empty(clientAppId)) {
511+
name: 'aca-auth'
512+
scope: resourceGroup
513+
params: {
514+
name: acaBackend.outputs.name
515+
clientAppId: clientAppId
516+
serverAppId: serverAppId
517+
clientSecretSettingName: !empty(clientAppSecret) ? 'azureclientappsecret' : ''
518+
authenticationIssuerUri: authenticationIssuerUri
519+
enableUnauthenticatedAccess: enableUnauthenticatedAccess
520+
blobContainerUri: 'https://${storageAccountName}.blob.${environment().suffixes.storage}/${tokenStorageContainerName}'
521+
appIdentityResourceId: (deploymentTarget == 'appservice') ? '' : acaBackend.outputs.identityResourceId
480522
}
481523
}
482524

@@ -661,6 +703,10 @@ module storage 'core/storage/storage-account.bicep' = {
661703
name: storageContainerName
662704
publicAccess: 'None'
663705
}
706+
{
707+
name: tokenStorageContainerName
708+
publicAccess: 'None'
709+
}
664710
]
665711
}
666712
}

0 commit comments

Comments
 (0)