Modern full-stack applications often require secure authentication and authorization. Users expect seamless login experiences, integration with third-party identity providers (Google, Microsoft, Okta), and secure access control.

Two key standards for authentication and authorization are:

  1. OAuth2 – an authorization framework that allows applications to access resources on behalf of a user

  2. OpenID Connect (OIDC) – an authentication layer built on top of OAuth2, providing user identity information

This article explains how to implement OAuth2 and OpenID Connect in a full-stack Angular + ASP.NET Core application, with emphasis on security, scalability, and real-world best practices.

1. Understanding OAuth2 and OIDC

1.1 OAuth2 Roles

OAuth2 defines several grant types:

1.2 OpenID Connect

OIDC extends OAuth2 by adding an ID Token (JWT) that contains user identity claims:

OIDC allows your frontend to know the user identity while OAuth2 tokens are used to access APIs securely.

2. Architecture Overview

Angular Frontend (Client SPA)
        |
        | OAuth2/OIDC
        v
Identity Provider (Auth0 / Azure AD / Keycloak)
        |
ASP.NET Core Backend (Resource Server)
        |
SQL Server or API Services

3. Configuring Identity Provider

For demonstration, we use Auth0:

  1. Create an Auth0 account and application

  2. Configure Allowed Callback URLs (e.g., http://localhost:4200/callback)

  3. Configure Allowed Logout URLs (e.g., http://localhost:4200)

  4. Note down:

    • Client ID

    • Domain (Auth0 domain)

    • Client Secret (used by backend for token validation)

For Azure AD or Keycloak, similar steps apply.

4. Angular Frontend Implementation

4.1 Install Dependencies

npm install @auth0/auth0-angular

4.2 Configure Auth Module

import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
  imports: [
    AuthModule.forRoot({
      domain: 'YOUR_DOMAIN',
      clientId: 'YOUR_CLIENT_ID',
      redirectUri: window.location.origin,
      cacheLocation: 'localstorage', // persists login across refresh
      useRefreshTokens: true // recommended for SPAs
    })
  ]
})
export class AppModule {}

4.3 Login and Logout

export class LoginComponent {
  constructor(public auth: AuthService) {}

  login() {
    this.auth.loginWithRedirect();
  }

  logout() {
    this.auth.logout({ returnTo: window.location.origin });
  }
}

4.4 Accessing User Profile

this.auth.user$.subscribe(user => {
  console.log('User profile:', user);
});

4.5 Sending Access Token to Backend

this.auth.idTokenClaims$.subscribe(token => {
  const accessToken = token?.__raw;
  this.http.get('/api/protected', {
    headers: { Authorization: `Bearer ${accessToken}` }
  }).subscribe(console.log);
});

Best practice: never store access tokens in memory alone. Use refresh tokens with local storage if required.

5. ASP.NET Core Backend Implementation

5.1 Install NuGet Packages

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

5.2 Configure JWT Authentication

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://YOUR_DOMAIN/";
        options.Audience = "YOUR_API_IDENTIFIER"; // API ID from Auth0
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireClaim("roles", "admin"));
});

5.3 Protect API Endpoints

[Authorize]
[HttpGet("protected")]
public IActionResult GetProtectedData()
{
    var userId = User.FindFirst("sub")?.Value;
    return Ok(new { Message = "Hello, user " + userId });
}

[Authorize(Policy = "AdminOnly")]
[HttpGet("admin")]
public IActionResult GetAdminData()
{
    return Ok(new { Message = "Admin data" });
}

Backend only needs access tokens, not ID tokens. ID tokens are for frontend identity.

6. Real-world Best Practices

  1. Use Authorization Code Flow with PKCE – secure SPA authentication

  2. Do not store secrets in frontend – backend should validate tokens

  3. Use Refresh Tokens Carefully – SPAs need rotating refresh tokens to avoid token expiry issues

  4. Validate JWTs in backend – check signature, issuer, audience, expiration

  5. Leverage Role-based Access Control (RBAC) – use claims in tokens for authorization

  6. Handle Token Expiry Gracefully – refresh tokens or prompt re-login

  7. Use HTTPS Everywhere – prevent token interception

7. Security Considerations

8. Token Flow Summary

  1. User clicks Login → Angular redirects to Identity Provider

  2. User authenticates → ID Token + Access Token returned to frontend

  3. Angular stores tokens securely and updates UI

  4. Angular sends Access Token in Authorization header to backend

  5. Backend validates token → grants access to protected resources

  6. Refresh token used for silent re-authentication without user interaction

9. Monitoring and Logging

10. Summary

Integrating OAuth2 and OpenID Connect in full-stack apps provides:

Key takeaways

  1. Use Authorization Code Flow with PKCE for SPAs

  2. Use JWT access tokens for API authentication

  3. Validate tokens and enforce role/claim-based access

  4. Store tokens securely and handle expiry gracefully

  5. Regularly audit token and authentication flows for security

A well-implemented OAuth2 + OIDC integration ensures secure, scalable, and user-friendly full-stack applications, protecting both users and backend resources.