Web API  

Using OAuth2 and OpenID Connect in Full-Stack Applications

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

  • Resource Owner – user who owns the data

  • Client – application requesting access (Angular frontend)

  • Authorization Server – issues access tokens (Identity Provider like Azure AD, Auth0)

  • Resource Server – API server serving protected resources (ASP.NET Core backend)

OAuth2 defines several grant types:

  • Authorization Code Flow – most secure, used in SPAs with backend

  • Implicit Flow – older SPA flow, less secure, now discouraged

  • Client Credentials Flow – machine-to-machine authentication

  • Password Grant – legacy, not recommended

1.2 OpenID Connect

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

  • sub – unique user identifier

  • name, email – user information

  • roles – optional, for authorization

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
  • Frontend: initiates login, receives ID Token and Access Token

  • Backend: validates access tokens, serves protected APIs

  • Identity Provider: issues tokens and manages user authentication

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

  • XSS Protection – prevent malicious scripts from accessing tokens

  • CSRF Protection – use same-site cookies or OAuth2 PKCE

  • Secure Storage – store tokens in secure storage, not in plain localStorage unless necessary

  • Scope Management – request minimum scopes required by frontend

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

  • Log failed token validations in backend

  • Monitor token expiry and refresh failures

  • Use APM tools to track API request latency due to auth validation

10. Summary

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

  • Secure authentication and authorization

  • Seamless login experiences with third-party providers

  • Role-based access and claim-based authorization

  • Separation of identity (frontend) and resource access (backend)

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.