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:
OAuth2 – an authorization framework that allows applications to access resources on behalf of a user
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:
Create an Auth0 account and application
Configure Allowed Callback URLs (e.g., http://localhost:4200/callback)
Configure Allowed Logout URLs (e.g., http://localhost:4200)
Note down:
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
Use Authorization Code Flow with PKCE – secure SPA authentication
Do not store secrets in frontend – backend should validate tokens
Use Refresh Tokens Carefully – SPAs need rotating refresh tokens to avoid token expiry issues
Validate JWTs in backend – check signature, issuer, audience, expiration
Leverage Role-based Access Control (RBAC) – use claims in tokens for authorization
Handle Token Expiry Gracefully – refresh tokens or prompt re-login
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
User clicks Login → Angular redirects to Identity Provider
User authenticates → ID Token + Access Token returned to frontend
Angular stores tokens securely and updates UI
Angular sends Access Token in Authorization header to backend
Backend validates token → grants access to protected resources
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
Use Authorization Code Flow with PKCE for SPAs
Use JWT access tokens for API authentication
Validate tokens and enforce role/claim-based access
Store tokens securely and handle expiry gracefully
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.