Introduction
Modern enterprise applications are rarely made up of just one application. A typical setup may include an Angular frontend running in the browser, an API Gateway, multiple backend APIs, and several downstream business systems. As more components are added, authentication and authorization need to be planned carefully.
One common mistake is to think that authentication only happens at the frontend. In reality, each layer has a different role to play.
The Angular SPA represents the user and starts the login process. The Identity Provider authenticates the user and issues the required tokens. The API Gateway serves as the controlled entry point to the backend systems and validates the access tokens it receives.
The downstream APIs are also responsible for protecting their own business capabilities. They should not automatically trust a request simply because it came through the API Gateway. Each service should apply the appropriate authorization checks before allowing access to its business operations.
The resulting architecture looks like this:
User → Angular SPA → Identity Provider → Access Token → API Gateway → .NET API → Downstream Services
For browser-based applications, the recommended OAuth 2.0 approach is Authorization Code Flow with Proof Key for Code Exchange (PKCE).
Why PKCE?
An Angular SPA runs in the browser, so it cannot safely store a client secret. Anything stored in browser code can potentially be viewed by users or attackers.
PKCE helps solve this problem by using a temporary code_verifier and a related code_challenge.
The flow is simple:
Angular creates a temporary code_verifier.
Angular creates a code_challenge from the verifier and sends it to the Identity Provider.
The user signs in and completes authentication.
The Identity Provider returns an authorization code.
Angular sends the authorization code along with the original code_verifier.
The Identity Provider verifies that the verifier matches the original challenge.
If everything is valid, Angular receives an access token and uses it to call the API Gateway.
The important point is that even if someone intercepts the authorization code, they cannot exchange it for an access token without the original code_verifier.
Angular SPA
Using MSAL, the Angular application can be configured as a public client.
MSAL (Microsoft Authentication Library) simplifies authentication in the Angular application by handling the OAuth 2.0 and OpenID Connect flows with Microsoft Entra ID. For a browser-based SPA, MSAL also handles the Authorization Code flow with PKCE internally, so the application does not need to manually generate or manage the code_verifier and code_challenge.
export const msalConfig = {
auth: {
clientId: 'angular-client-id',
authority: 'https://login.microsoftonline.com/tenant-id',
redirectUri: window.location.origin
},
cache: {
cacheLocation: 'sessionStorage'
}
};
The application requests an access token for the gateway API:
const request = {
scopes: ['api://gateway-api/access_as_user']
};
this.msalService
.acquireTokenSilent(request)
.subscribe(result => {
const accessToken = result.accessToken;
});
The token is then sent to the gateway:
GET /api/deals/123
Authorization: Bearer <access_token>
Note: MSAL handles the PKCE flow internally. Your Angular application mainly needs to configure the client, request the required scopes, and use acquireTokenSilent() to obtain an access token.
Gateway Authentication
The gateway is responsible for validating the incoming access token before allowing the request to continue to the backend services.
This gateway can be implemented as a custom .NET service or by using a managed API management platform such as Azure API Management (APIM).
For a .NET-based gateway, JWT bearer authentication can be configured as follows:
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = configuration["Identity:Authority"];
options.Audience = configuration["Identity:Audience"];
});
builder.Services.AddAuthorization();
The gateway can also enforce scopes or policies:
[Authorize(Policy = "Deals.Read")]
[HttpGet("deals/{id}")]
public async Task<IActionResult> GetDeal(string id)
{
return Ok(await dealService.GetDealAsync(id));
}
If Azure API Management is used instead, token validation can be configured using APIM policies. In this case, APIM becomes the gateway layer responsible for validating the JWT token, checking claims or scopes, routing requests, applying rate limits, and providing logging and monitoring capabilities.
Regardless of whether the gateway is implemented using a custom .NET service or APIM, it serves as the controlled entry point into the backend ecosystem.
Typical gateway responsibilities include:
Routing requests to the appropriate backend service
Validating access tokens
Enforcing authorization rules
Applying rate limiting and throttling
Logging and monitoring
Adding correlation IDs for request tracing
The choice between a custom .NET gateway and APIM depends on the application's requirements. A custom gateway provides more flexibility and control over custom business logic, while APIM provides many common API management capabilities out of the box.
Calling the Downstream API
The gateway can use a typed HttpClient to call a downstream service:
public async Task<DealResponse?> GetDealAsync(string id)
{
using var response =
await httpClient.GetAsync($"api/deals/{id}");
response.EnsureSuccessStatusCode();
return await response.Content
.ReadFromJsonAsync<DealResponse>();
}
The gateway should not automatically forward the original user token unless the downstream API is specifically designed to validate that token and the audience is appropriate.
For service-to-service communication, the gateway can obtain a separate access token using a confidential client or managed identity.
Downstream API
The downstream .NET 10 API independently validates the token it receives:
builder.Services
.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = configuration["Identity:Authority"];
options.Audience = configuration["Identity:Audience"];
});
Authorization can then be applied to business endpoints:
[Authorize(Policy = "Deals.Read")]
[HttpGet("{id}")]
public async Task<IActionResult> Get(string id)
{
var deal = await dealService.GetAsync(id);
return deal is null
? NotFound()
: Ok(deal);
}
Important Security Principle
Authentication should not be treated as a single validation step at the frontend.
The Angular SPA obtains the token. The gateway validates the incoming request. The downstream API should also validate the identity and authorization context required for its own business capabilities.
The overall architecture is therefore:
User → Angular + PKCE → Identity Provider → Access Token → Gateway → Downstream API
PKCE protects the browser-based authorization flow, while token validation, scopes, roles, and service-to-service authentication protect the APIs.
The result is a secure and scalable architecture for modern Angular and .NET 10 enterprise applications.
Conclusion
In this architecture, the Angular SPA uses the Authorization Code + PKCE flow to authenticate the user with the Identity Provider. MSAL manages the login flow and redirects the user back to the Angular application using the configured redirectUri.
Once Angular receives the access token, it sends API requests to Azure API Management (APIM). APIM validates the JWT token, checks the required scopes, applies rate limits and other policies, and routes the request to the correct backend API.
The request is then forwarded to the .NET 10 API. The backend handles business-level authorization and orchestration before calling the required downstream services.
This approach keeps authentication and common API management responsibilities centralized in APIM, while allowing the backend APIs to focus on business logic and domain-specific authorization.
Happy Coding!