English | Español
Authentication template for Angular applications using Keycloak as an OIDC (OpenID Connect) identity provider. This project is ready to use and can serve as a foundation for implementing secure authentication in your Angular applications.
- Complete OIDC authentication with Keycloak
- Automatic token renewal (silent renew)
- Guards for route protection
- HTTP interceptor to automatically add tokens
- Logout with token revocation
- Environment configuration (development, production)
- Docker Compose for Keycloak and PostgreSQL
- Dev Container with Docker-in-Docker for zero-config setup
- TailwindCSS for styling
- ESLint + Prettier for code quality
- Vitest for testing
The fastest way to get started — no local tools required beyond VS Code and Docker.
Requirements: VS Code + Dev Containers extension + Docker Desktop
git clone <repository-url>
# Open the folder in VS Code, then: F1 → "Dev Containers: Reopen in Container"VS Code will build the container and automatically:
- Install Node.js and pnpm via mise (versions pinned in
.mise.toml) - Run
pnpm install - Configure Angular Language Service, ESLint, Prettier and Tailwind extensions
Keycloak + PostgreSQL run inside the container (Docker-in-Docker), fully isolated per project instance:
docker compose up -d # run this once inside the container terminalPorts forwarded automatically: 4200 (Angular → opens browser) · 8080 (Keycloak → notification)
Then jump directly to Configure Keycloak.
- Node.js 22+ and pnpm 10+
- Docker and Docker Compose v2
- Angular CLI 21+
git clone <repository-url>
cd angular-auth-keycloack
pnpm installdocker compose up -dThis will start:
- Keycloak at
http://localhost:8080 - PostgreSQL at
localhost:5432
Keycloak admin credentials:
- Username:
admin - Password:
admin
- Go to
http://localhost:8080 - Click on "Administration Console"
- Login with admin/admin
- In the top-left menu, click "Create realm"
- Name:
angular-auth-realm - Click "Create"
- Go to "Clients" in the sidebar
- Click "Create client"
- Configure:
- Client type: OpenID Connect
- Client ID:
angular-client
- Click "Next"
- Configure:
- Client authentication: OFF
- Authorization: OFF
- Authentication flow: Check "Standard flow" and "Direct access grants"
- Click "Next"
- Configure URLs:
- Root URL:
http://localhost:4200 - Home URL:
http://localhost:4200 - Valid redirect URIs:
http://localhost:4200/* - Valid post logout redirect URIs:
http://localhost:4200/* - Web origins:
http://localhost:4200
- Root URL:
- Click "Save"
- In the
angular-clientclient, go to the "Client scopes" tab - Ensure these are assigned:
openidprofileemailoffline_access(for refresh tokens)
- Go to "Users" in the sidebar
- Click "Create new user"
- Configure:
- Username:
testuser - Email:
test@example.com - Email verified: ON
- First name: Test
- Last name: User
- Username:
- Click "Create"
- Go to the "Credentials" tab
- Click "Set password"
- Set password:
test123 - Uncheck "Temporary"
- Click "Save"
pnpm startThe application will be available at http://localhost:4200
Dev Container users: the dev server is not started automatically — run
pnpm startin the VS Code terminal.
angular-auth-keycloack/
├── .devcontainer/
│ ├── devcontainer.json # Dev Container config (DinD, ports, extensions)
│ └── Dockerfile # Base image + build tools + GitHub CLI + mise
├── src/
│ ├── app/
│ │ ├── auth/ # Authentication module
│ │ │ ├── auth.config.ts # OIDC configuration
│ │ │ ├── auth.guard.ts # Guard for protected routes
│ │ │ ├── auth.interceptor.ts # Interceptor to add tokens
│ │ │ └── auth.service.ts # Authentication service
│ │ ├── components/
│ │ │ ├── callback/ # OIDC callback component
│ │ │ ├── dashboard/ # Protected page (example)
│ │ │ └── login/ # Login page
│ │ ├── app.config.ts # Main app configuration
│ │ ├── app.routes.ts # Route definitions
│ │ ├── app.ts # Root component
│ │ └── app.html # Root template
│ ├── environments/ # Environment variables
│ │ ├── environment.ts # Default env
│ │ ├── environment.development.ts # Development env
│ │ └── environment.production.ts # Production env
│ ├── main.ts # Entry point
│ └── index.html # Main HTML
├── keycloak/ # Configuration screenshots
├── .mise.toml # Node and pnpm version pinning (via mise)
├── docker-compose.yml # Keycloak + PostgreSQL
├── package.json # Dependencies and scripts
├── angular.json # Angular configuration
├── tsconfig.json # TypeScript configuration
└── tailwind.config.js # TailwindCSS configuration
export const environment = {
production: false,
keycloak: {
clientId: 'angular-client',
authority: 'http://localhost:8080/realms/angular-auth-realm',
redirectUrl: 'http://localhost:4200/callback',
postLogoutRedirectUri: 'http://localhost:4200',
},
apiUrl: 'http://localhost:3000/api',
};Update with production URLs:
export const environment = {
production: true,
keycloak: {
clientId: 'angular-client',
authority: 'https://your-keycloak.com/realms/angular-auth-realm',
redirectUrl: 'https://your-app.com/callback',
postLogoutRedirectUri: 'https://your-app.com',
},
apiUrl: 'https://your-api.com/api',
};# Development
pnpm start # Start development server at http://localhost:4200
# Build
pnpm build # Production build
pnpm watch # Build in watch mode
# Testing
pnpm test # Run tests with Vitest
# Linting and formatting
pnpm lint # Run ESLint
pnpm format # Format code with Prettier- Unauthenticated user accesses the application
- Redirected to
/login - When clicking "Login", the OIDC flow starts:
- Redirect to Keycloak
- User enters credentials
- Keycloak validates and redirects to
/callbackwith authorization code
- CallbackComponent processes the code and obtains tokens
- User is redirected to the dashboard
- AuthGuard protects routes that require authentication
- AuthInterceptor automatically adds the token to HTTP requests
- Tokens are automatically renewed before expiring (silent renew)
The project uses a layered architecture for authentication:
Components (Login, Dashboard, etc.)
↓
AuthService (Abstraction)
↓
OidcSecurityService (angular-auth-oidc-client)
↓
Keycloak
Why use AuthService instead of OidcSecurityService directly?
- Abstraction: If you change OIDC library, only modify
AuthService - Consistency: All components use the same interface
- Maintainability: Centralized logic in one place
- Testing: Easier to mock
AuthService
The project already uses AuthService consistently across all components. Examples:
import { AuthService } from '../../auth/auth.service';
export class LoginComponent {
private authService = inject(AuthService);
login() {
this.authService.login(); // Start OIDC flow
}
}import { AuthService } from '../../auth/auth.service';
export class DashboardComponent {
private authService = inject(AuthService);
userData$ = this.authService.getUserData$();
accessToken$ = this.authService.getAccessToken$();
logout() {
this.authService.logout(); // Logout and revoke tokens
}
}import { AuthService } from '../../auth/auth.service';
export class CallbackComponent {
private authService = inject(AuthService);
ngOnInit() {
this.authService.checkAuth().subscribe(result => {
if (result.isAuthenticated) {
// Redirect to dashboard
}
});
}
}// Start login
authService.login(): void
// Logout and revoke tokens
authService.logout(): void
// Check authentication
authService.checkAuth(): Observable<LoginResponse>
authService.isAuthenticated(): Observable<boolean>
// Get user data
authService.getUserData$(): Observable<UserDataResult>
// Get tokens
authService.getAccessToken$(): Observable<string>
authService.getIdToken$(): Observable<string>The project includes an example of a protected route in src/app/app.routes.ts:11:
import { authGuard } from './auth/auth.guard';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'callback', component: CallbackComponent },
{
path: 'dashboard', // Protected route in the project
component: DashboardComponent,
canActivate: [authGuard], // Guard that verifies authentication
},
];The authGuard verifies that the user is authenticated. If not, it automatically redirects to /login.
To add more protected routes, simply add canActivate: [authGuard]:
{
path: 'admin',
component: AdminComponent,
canActivate: [authGuard], // New protected route
}The AuthInterceptor (configured in src/app/auth/auth.interceptor.ts:6) automatically adds the Keycloak JWT Access Token to all HTTP requests:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const oidcSecurityService = inject(OidcSecurityService);
return oidcSecurityService.getAccessToken().pipe(
switchMap((token) => {
if (token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`, // Keycloak JWT Token
},
});
}
return next(req);
}),
);
};The token sent contains user and authentication information:
{
"exp": 1738675200,
"iat": 1738671600,
"iss": "http://localhost:8080/realms/angular-auth-realm",
"sub": "user-uuid",
"preferred_username": "testuser",
"email": "test@example.com",
"given_name": "Test",
"family_name": "User",
"scope": "openid profile email"
}Your backend API must validate this JWT token against Keycloak. Examples:
npm install express-jwt jwks-rsaconst jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');
// Automatic token validation
app.use(jwt({
secret: jwksRsa.expressJwtSecret({
jwksUri: 'http://localhost:8080/realms/angular-auth-realm/protocol/openid-connect/certs'
}),
audience: 'angular-client',
issuer: 'http://localhost:8080/realms/angular-auth-realm',
algorithms: ['RS256']
}));
// Protected route
app.get('/api/protected', (req, res) => {
// req.user contains token data
res.json({ message: 'Authenticated', user: req.user });
});The interceptor is configured in src/app/app.config.ts:
provideHttpClient(
withInterceptors([authInterceptor])
)IMPORTANT: The interceptor adds the token to all HTTP requests. If you make requests to external APIs that shouldn't have the token, modify the interceptor:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const oidcSecurityService = inject(OidcSecurityService);
// Only add token to your API requests
const isApiRequest = req.url.startsWith(environment.apiUrl);
if (!isApiRequest) {
return next(req); // Don't add token to external URLs
}
return oidcSecurityService.getAccessToken().pipe(
switchMap((token) => {
if (token) {
req = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
}
return next(req);
}),
);
};- Update in Keycloak
- Update in
src/environments/environment.development.ts:authority: 'http://localhost:8080/realms/YOUR-REALM'
- Update in Keycloak
- Update in
src/environments/environment.development.ts:clientId: 'your-client-id'
In src/app/auth/auth.config.ts:
scope: 'openid profile email offline_access roles'Verify that the callback URL is correctly configured in:
- Keycloak: Valid redirect URIs
environment.ts: redirectUrl
Verify that the clientId in environment.ts exactly matches the Client ID in Keycloak.
Verify that:
- The
offline_accessscope is included useRefreshToken: trueis inauth.config.ts- The client has "Standard flow" enabled
Verify that:
- The
/callbackroute is configured - The CallbackComponent is imported
- Angular 21.1.2: Main framework
- angular-auth-oidc-client 21.0.1: OIDC library
- Keycloak 26.5.2: Identity provider
- TailwindCSS 4.1.18: Styling framework
- Vitest 4.0.18: Testing
- ESLint + Prettier: Code quality
- Docker + Docker Compose v2: Containerization
- mise: Node and pnpm version management (
.mise.toml) - Dev Container: Docker-in-Docker for isolated, reproducible environments
- Configure roles and permissions in Keycloak
- Implement role-based authorization
- Add refresh token rotation
- Configure 2FA/MFA
- Implement protected backend API
- Configure HTTPS for production
- Add e2e tests
MIT
Contributions are welcome. Please open an issue or pull request.

