This repository contains a simple application that demonstrates how to secure a Spring/Angular application with Keycloak.
- Create a new realm called
app-realm
. - Create a new client called
ebank-client
with the following access settings: - Create roles
ADMIN
andUSER
for the clientebank-client
. - Create users and assign them some roles:
Assigning
ADMIN
andUSER
roles to a user: - Test Keycloak configuration with Postman:
Get access token with username and password: Authenticate with access token: Accessing with credentials:
- application.properties:
server.port=8084
spring.datasource.url=jdbc:h2:mem:currency-deposit
spring.h2.console.enabled=true
# Keycloak configuration
# Name of the realm
keycloak.realm = app-realm
# Name of the client
keycloak.resource = ebank-client
# Only accept bearer tokens
keycloak.bearer-only = true
# URL of the Keycloak server
keycloak.auth-server-url = http://localhost:8080
# Disable SSL certificate verification
keycloak.ssl-required = none
- Class
KeycloakAdapterConfig
:
@KeycloakConfiguration
public class KeycloakAdapterConfig {
// To retrieve Keycloak configuration from application.properties
@Bean
KeycloakSpringBootConfigResolver springBootConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
- Class
KeycloakSecurityConfig
:
// To enable Spring Security
@KeycloakConfiguration
// To enable method security with @PreAuthorize and @PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
// This approach is deprecated on spring boot 3.0.0
// todo: migrate to spring boot 3.0.0
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
// To register Keycloak authentication strategy for public or confidential applications
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// To configure KeycloakAuthenticationProvider with default spring security authentication manager
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
// We work with JWT tokens, so we don't need CSRF as we don't use cookies
http.csrf().disable();
// To enable H2 console access without authentication
http.authorizeRequests().antMatchers("/h2-console/**").permitAll();
// To enable H2 console access without X-Frame-Options header
http.headers().frameOptions().disable();
// To enable CORS
http.authorizeRequests().anyRequest().authenticated();
}
}
- Class EBankRestController:
Adding
@PreAuthorize("hasAuthority('ADMIN')")
to the endpoint, only users with ADMIN role can access this endpoint. Adding@PreAuthorize("hasAuthority('USER')")
to the endpoint, only users with USER role can access this endpoint.
@RestController
@CrossOrigin("*")
public class EBankRestController {
@Autowired
private EBankServiceImpl eBankService;
@PostMapping("/currencyTransfer")
// Only users with ADMIN role can access this endpoint
@PreAuthorize("hasAuthority('ADMIN')")
public CurrencyTransferResponse currencyTransfer(@RequestBody NewWalletTransferRequest request){
return this.eBankService.newWalletTransaction(request);
}
@GetMapping("/currencyDeposits")
// Only users with USER role can access this endpoint
@PreAuthorize("hasAuthority('USER')")
public List<CurrencyDeposit> currencyDepositList(){
return eBankService.currencyDeposits();
}
}
- application.properties:
server.port=8082
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:currenciesDb
spring.graphql.graphiql.enabled=true
# Keycloak configuration
# Name of the realm
keycloak.realm = app-realm
# Name of the client
keycloak.resource = wallet-client
# Only accept bearer tokens
keycloak.bearer-only = true
# URL of the Keycloak server
keycloak.auth-server-url = http://localhost:8080
# Disable SSL certificate verification
keycloak.ssl-required = none
- Class
KeycloakAdapterConfig
:
@Configuration
public class KeycloakAdapterConfig {
@Bean
KeycloakSpringBootConfigResolver springBootConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
}
- Class
KeycloakSecurityConfig
:
@KeycloakConfiguration
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(keycloakAuthenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.csrf().disable();
http.authorizeRequests().antMatchers("/h2-console/**").permitAll();
http.authorizeRequests().antMatchers("/graphql/**").permitAll();
http.authorizeRequests().antMatchers("/graphiql/**").permitAll();
http.headers().frameOptions().disable();
http.authorizeRequests().anyRequest().authenticated();
}
}
- app.module.ts:
import {KeycloakAngularModule, KeycloakService} from "keycloak-angular";
export function kcFactory(kcService : KeycloakService) {
return () => {
kcService.init({
config: {
realm: "app-realm",
clientId: "ebank-client",
url: "http://localhost:8080"
},
initOptions: {
onLoad: "login-required",
checkLoginIframe: true
},
})
}
}
@NgModule({
providers: [
{provide: APP_INITIALIZER, deps: [KeycloakService], useFactory: kcFactory, multi: true}
],
// ... rest of the code
})
- Configuring the routes:
const routes: Routes = [
{
path : "currencies", component : CurrenciesComponent
},
{
path : "continents", component : ContinentsComponent
},
{
path : "wallets", component : WalletsComponent, canActivate : [AuthGuard], data : {roles : ["USER"]}
},
{
path : "transactions/:walletId", component : WalletTransactionsComponent, canActivate : [AuthGuard], data : {roles : ['USER']}
},
{
path : "currencyDeposit", component : CurrencyDepositComponent,
}
];
- Create guard:
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
Router,
RouterStateSnapshot
} from '@angular/router';
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
@Injectable({
providedIn: 'root'
})
export class AuthGuard extends KeycloakAuthGuard {
constructor(
protected override readonly router: Router,
protected readonly keycloak: KeycloakService
) {
super(router, keycloak);
}
public async isAccessAllowed(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) {
// Force the user to log in if currently unauthenticated.
if (!this.authenticated) {
await this.keycloak.login({
redirectUri: window.location.origin
});
}
// Get the roles required from the route.
const requiredRoles = route.data['roles'];
// Allow the user to proceed if no additional roles are required to access the route.
if (!Array.isArray(requiredRoles) || requiredRoles.length === 0) {
return true;
}
// Allow the user to proceed if all the required roles are present.
return requiredRoles.every((role) => this.roles.includes(role));
}
}