Skip to content
Closed
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
4 changes: 2 additions & 2 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -1119,7 +1119,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.19.0' = {
}
]
}
ingressExternal: true
ingressExternal: !enablePrivateNetworking
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When enablePrivateNetworking is true, this change makes the API Container App internal-only (ingressExternal: false). However, the Web Container App is still configured with APP_API_BASE_URL = https://${avmContainerApp_API.outputs.fqdn} (see the Web app env block below), which is consumed by the browser-side SPA. If the API FQDN becomes internal/private-only, end-user browsers won't be able to reach it and the UI will break unless traffic is routed via the WAF/app gateway (e.g., by setting APP_API_BASE_URL to the public WAF hostname/relative path and configuring the gateway to forward to the internal API). Please update the Web app API base URL (or add a proxy) for the private-networking/WAF path.

Copilot uses AI. Check for mistakes.
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down Expand Up @@ -1771,7 +1771,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.19.0' =
}
]
}
ingressExternal: true
ingressExternal: !enablePrivateNetworking
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as the initial API module: with ingressExternal: !enablePrivateNetworking, the API update deployment becomes internal-only in WAF/private-networking mode, but the Web app’s APP_API_BASE_URL is still set to the API FQDN. Ensure the frontend uses a WAF-routable base URL (or a reverse proxy) so browser requests can still reach the API when the API ingress is internal.

Suggested change
ingressExternal: !enablePrivateNetworking
// Keep API ingress external so the frontend's API base URL remains browser-routable.
// If a WAF/reverse proxy is introduced later, the frontend base URL can be switched to that route instead.
ingressExternal: true

Copilot uses AI. Check for mistakes.
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down
4 changes: 2 additions & 2 deletions infra/main_custom.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -1132,7 +1132,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.19.0' = {
}
]
}
ingressExternal: true
ingressExternal: !enablePrivateNetworking
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When enablePrivateNetworking is true, this change makes the API Container App internal-only (ingressExternal: false). In WAF/private-networking deployments, the Web app is still configured to call the API using https://${avmContainerApp_API.outputs.fqdn}, which is used from the browser. If that FQDN is internal/private-only, the SPA will fail unless requests are routed through the WAF/app gateway (e.g., set APP_API_BASE_URL to the WAF hostname or a relative path and configure routing accordingly). Please adjust the frontend API base URL/proxy behavior for the private-networking path.

Suggested change
ingressExternal: !enablePrivateNetworking
// Keep the API externally reachable until the frontend is updated to use
// WAF/app gateway routing or a same-origin relative path for private-networking deployments.
ingressExternal: true

Copilot uses AI. Check for mistakes.
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down Expand Up @@ -1804,7 +1804,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.19.0' =
}
]
}
ingressExternal: true
ingressExternal: !enablePrivateNetworking
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern as the initial API module: with ingressExternal: !enablePrivateNetworking, the API update deployment becomes internal-only in WAF/private-networking mode, but the frontend is still configured to call the API via the API FQDN. Ensure the SPA uses a WAF-routable base URL (or a reverse proxy) so browser traffic can reach the API when ingress is internal.

Suggested change
ingressExternal: !enablePrivateNetworking
ingressExternal: true

Copilot uses AI. Check for mistakes.
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down
4 changes: 2 additions & 2 deletions src/ContentProcessorWeb/azure_cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ steps:
--image $(acrLoginServer)/$(imageRepository):$(Build.BuildId) \
--cpu 4.0 \
--memory 8.0Gi \
--set-env-vars APP_API_BASE_URL=$cpsApiBaseUrl APP_WEB_CLIENT_ID=$(appWebClientId) APP_WEB_AUTHORITY=$(appWebAuthority) APP__WEB_SCOPE=$(appWebScope) APP_API_SCOPE=$(appApiScope) APP_AUTH_ENABLED=false
--set-env-vars APP_API_BASE_URL=$cpsApiBaseUrl APP_WEB_CLIENT_ID=$(appWebClientId) APP_WEB_AUTHORITY=$(appWebAuthority) APP_WEB_SCOPE=$(appWebScope) APP_API_SCOPE=$(appApiScope) APP_AUTH_ENABLED=false
else
# Create the container app with the new image and registry settings
az containerapp create \
Expand All @@ -104,7 +104,7 @@ steps:
--registry-server $(acrLoginServer) \
--registry-identity $managedIdentityResourceId \
--ingress external \
--env-vars APP_API_BASE_URL=$cpsApiBaseUrl APP_WEB_CLIENT_ID=$(appWebClientId) APP_WEB_AUTHORITY=$(appWebAuthority) APP__WEB_SCOPE=$(appWebScope) APP_API_SCOPE=$(appApiScope) APP_AUTH_ENABLED=false
--env-vars APP_API_BASE_URL=$cpsApiBaseUrl APP_WEB_CLIENT_ID=$(appWebClientId) APP_WEB_AUTHORITY=$(appWebAuthority) APP_WEB_SCOPE=$(appWebScope) APP_API_SCOPE=$(appApiScope) APP_AUTH_ENABLED=false

fi
displayName: 'Deploy container to Azure Container App'
Expand Down
24 changes: 22 additions & 2 deletions src/ContentProcessorWeb/src/Services/httpUtility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@

const api: string = process.env.REACT_APP_API_BASE_URL as string;

const isAuthEnabled = (): boolean =>
process.env.REACT_APP_AUTH_ENABLED?.toLowerCase() !== 'false';

const isUsableToken = (token: string | null): token is string => {
if (!token) return false;
const value = token.trim();
if (!value) return false;
if (value.toLowerCase() === 'null' || value.toLowerCase() === 'undefined') {
return false;
}
if (value.startsWith('APP_')) return false;
return true;
};

interface FetchResponse<T> {
data: T | null;
status: number;
Expand Down Expand Up @@ -73,11 +87,14 @@ const fetchWithAuth = async <T>(
const token = localStorage.getItem('token');

const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Cache-Control': 'no-cache',
};

if (isAuthEnabled() && isUsableToken(token)) {
headers['Authorization'] = `Bearer ${token}`;
}

let processedBody: BodyInit | null = null;
if (body instanceof FormData) {
processedBody = body;
Expand Down Expand Up @@ -132,12 +149,15 @@ const fetchHeadersWithAuth = async <T>(
const token = localStorage.getItem('token');

const headers: Record<string, string> = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Cache-Control': 'no-cache',
};

if (isAuthEnabled() && isUsableToken(token)) {
headers['Authorization'] = `Bearer ${token}`;
}

if (body instanceof FormData) {
delete headers['Content-Type'];
} else {
Expand Down
25 changes: 23 additions & 2 deletions src/ContentProcessorWeb/src/msal-auth/msaConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
*/
import { Configuration, LogLevel } from '@azure/msal-browser';

const isUsableScope = (scope?: string): scope is string => {
if (!scope) return false;
const value = scope.trim();
if (!value) return false;
// Guard against unresolved placeholders from env substitution.
if (value.startsWith('APP_')) return false;
if (value.startsWith('<') && value.endsWith('>')) return false;
return true;
};

export const msalConfig: Configuration = {
auth: {
clientId: process.env.REACT_APP_WEB_CLIENT_ID as string,
Expand All @@ -31,14 +41,25 @@ export const msalConfig: Configuration = {
const loginScope = process.env.REACT_APP_WEB_SCOPE as string;
const tokenScope = process.env.REACT_APP_API_SCOPE as string;

const loginScopes = ['user.read'];
if (isUsableScope(loginScope)) {
loginScopes.push(loginScope);
}

const tokenScopes = isUsableScope(tokenScope)
? [tokenScope]
: isUsableScope(loginScope)
? [loginScope]
: ['user.read'];

Comment on lines +49 to +54
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tokenRequest.scopes falls back to ['user.read'] when both REACT_APP_API_SCOPE and REACT_APP_WEB_SCOPE are unusable. Since tokenRequest is later used to acquire the access token that gets sent to the backend API, this fallback can silently produce a Microsoft Graph token instead of an API token, leading to confusing 401s. Consider failing fast (e.g., throw/log a clear configuration error when auth is enabled but no API scope is configured) or set tokenScopes to an empty array and handle the missing-scope case explicitly in useAuth.getToken().

Suggested change
const tokenScopes = isUsableScope(tokenScope)
? [tokenScope]
: isUsableScope(loginScope)
? [loginScope]
: ['user.read'];
const resolvedTokenScope = isUsableScope(tokenScope)
? tokenScope
: isUsableScope(loginScope)
? loginScope
: undefined;
if (!resolvedTokenScope) {
throw new Error(
'MSAL configuration error: a usable API scope must be configured in REACT_APP_API_SCOPE (or REACT_APP_WEB_SCOPE if intentionally reused) to acquire access tokens for the backend API.',
);
}
const tokenScopes = [resolvedTokenScope];

Copilot uses AI. Check for mistakes.
export const loginRequest = {
scopes: ["user.read", loginScope],
scopes: loginScopes,
};

export const graphConfig = {
graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
};

export const tokenRequest = {
scopes: [tokenScope],
scopes: tokenScopes,
};
Loading