Keycloak - uwierzytelnianie i autoryzacja użytkownika w aplikacji Angular/Spring Boot
Jak wykorzystać serwer Keycloak do logowania w aplikacji? Omówimy to na przykładzie gotowego projektu, który umożliwia użytkownikowi zalogowanie się do aplikacji z poziomu przeglądarki internetowej.
Czym jest Keycloak?
Keycloak to serwer uwierzytelniania i autoryzacji na licencji open-source. Może zostać podłączony do LDAP/AD lub uwierzytelniać użytkowników przy użyciu Google, Facebooka itd. Posiada również konsolę administracyjną, w której możemy łatwo skonfigurować chociażby uprawnienia użytkowników.
Więcej informacji na stronie www.keycloak.org.
Projekt demo
Projekt aplikacji znajduję się na GitHubie - link do repozytorium. Składa się on z aplikacji backendowej napisanej w Spring Boot (Kotlin) oraz frontendowej napisanej w Angular 8.
Aby przedstawić sposób zaimplementowania uwierzytelniania użytkownika za pomocą serwera Keycloak, zabezpieczymy endpointy na backendzie oraz osobno część frontendową.
Serwer Keycloak
Wykorzystamy serwer Keycloak uruchomiony na Dockerze. Wykonując w głównym folderze repozytorium polecenie:
docker run --rm --name keycloak-server -p 8180:8080 \
-e KEYCLOAK_USER=admin \
-e KEYCLOAK_PASSWORD=admin \
-e KEYCLOAK_IMPORT=/realm/realm-export.json \
-v $(pwd)/realm/:/realm/ \
jboss/keycloak
uruchomimy serwer Keycloak na porcie 8180
.
Konsola administracyjna będzie dostępna pod adresem: 127.0.0.1:8180/auth/admin
Zalogujemy się do niej przy użyciu loginu i hasła zdefiniowanego w KEYCLOAK_USER
oraz KEYCLOAK_PASSWORD
, w naszym przypadku admin:admin
.
Konfiguracja serwera
Sama konfiguracja serwera Keycloak, to temat na osobny wpis, dlatego wykorzystamy przygotowaną wcześniej konfigurację.
Eksport konfiguracji serwera został umieszczony w repozytorium projektu demo (realm/realm-export.json
) i zostanie załadowany podczas uruchomienia serwera.
Skonfigurowane zostały:
- realm
SpringBootAngular
- client
SpringBootAngularClient
- Access Type:
confidential
- podczas logowania musimy przekazać secret skonfigurowany wSpringBootAngularClient
- dostępnę są jeszcze opcje
bearer-only
orazpublic
- więcej na ten temat w dokumentacji (link)
- dostępnę są jeszcze opcje
- Valid Redirect URIs:
*
- dla ułatwienia bez ograniczeń - Web Origins:
*
- dla ułatwienia bez ograniczeń
- Access Type:
- rola
user_role
- tylko użytkownik z tą rolą będzie mógł się autoryzować w aplikacjach - użytkownik
user
z roląuser_role
(user:password
) - użytkownik
user2
bez roliuser_role
(user2:password2
)
Aplikacja backendowa
Aplikacja backendowa składa się z dwóch kontrolerów:
MainController
- wystawia zabezpieczone endpointy/api/hello
- zwraca stringHello from the Backend!
/api/logout
- wylogowuje nas po stronie aplikacji backendowej
KeycloakController
- wystawia publiczny endpointapi/keycloak/config
- udostępnia wspólną konfigurację dla frontendu
Aplikacja frontendowa
Aplikacja frontendowa składa się z trzech komponentów:
PublicComponent
- dostępny dla wszystkich bez logowaniaProtectedComponent
- zabezpieczony przed nieautoryzowanym użytkownikiemToolbarComponent
- menu z przyciskami dla wygody
Uwierzytelnianie na backendzie
Zależności
Na backendzie wykorzystamy zależności:
org.keycloak:keycloak-adapter-core
org.keycloak:keycloak-spring-boot-2-adapter
org.keycloak:keycloak-tomcat-adapter
Moglibyśmy wykorzystać jedną zależność, zawierającą powyższe adaptery:
org.keycloak:keycloak-spring-boot-2-starter
jednak w najnowszej dostępnej wersji 4.0.0.Final
używa starszych wersji adapterów. Jedną ze zmian w nowszych wersjach adapterów, jest poprawiona walidacja tokenów. Jeśli w przyszłości pojawi się nowa wersja zależności, z poprawionymi adapterami, to nic nie będzie stało na przeszkodzie żeby z niej skorzystać.
Konfiguracja
Oprócz konfiguracji portu aplikacji oraz poziomu logowania adapterów (dzięki czemu zobaczymy w logach co się dokładnie dzieje), musimy skonfigurować keycloak-adapter-core
.
Dzięki keycloak-spring-boot-2-adapter
możemy wszystko skonfigurować w application.yml
:
keycloakRequiredUserRole: user_role
keycloak:
enabled: true
auth-server-url: http://localhost:8180/auth
realm: SpringBootAngular
resource: SpringBootAngularClient
security-constraints:
- authRoles:
- ${keycloakRequiredUserRole}
securityCollections:
- name: protected resource
patterns:
- /api/*
- securityCollections:
- name: public resource
patterns:
- /api/keycloak/config
credentials:
secret: "<SECRET>"
realm-key: "<PUBLIC_KEY>"
keycloakRequiredUserRole
- dla ułatwienia, ponieważ wykorzystamy tylko jedną rolę, będzie nam łatwiej udostępnić ją przez api (jeśli chcemy wykorzystać więcej ról, to musimy je wyciągąć z listyauthRoles
keycloak.security-constraints
- tutaj definiujemy ograniczenia endpointów
Możemy zdefiniować ścieżki dostępne publicznie:
np. /api/keycloak/config
oraz ścieżki które będą wymagały uprawnień
np. /api/*
które będzie wymagać od użytkownika roli ${keycloakRequiredUserRole}
(czyli user_role
).
Konfiguracja Keycloak:
keycloak.enabled
- umożliwi nam łatwe wyłączenie uwierzytelniania;keycloak.auth-server-url
- adres serwera Keycloak;keycloak.realm
- nazwa realmu;keycloak.resource
- nazwa klienta skonfigurowanego dla podanego realmu;keycloak.credentials.secret
- secret wygenerowany wSpringBootAngularClient
, możemy go znaleźć w konsoli administracyjnej (Clients > SpringBootAngularClient > Credentials > Secret
);keycloak.realm-key
- klucz publiczny realmu, możemy go znaleźć w konsoli administracyjnej (Realm Settings > Keys > Active > RSA > Public Key
).
Wystawienie konfiguracji dla frontendu
Ponieważ zabezpieczamy osobno aplikację backendową jak i frontendową, a konfiguracja Keycloak jest taka sama, to możemy wystawić konfigurację z application.yml
przez api. Dzięki temu unikniemy duplikowania konfiguracji w aplikacji frontendowej.
Konfigurację wystawimy w KeycloakController
pod adresem api/keycloak/config
. Będzie ona miała postać:
{
"enabled": true,
"authServerUrl": "http://localhost:8180/auth",
"realm": "SpringBootAngular",
"resource": "SpringBootAngularClient",
"requiredUserRole": "user_role",
"credentials": {
"secret": "<SECRET>"
}
}
Uwierzytelnianie na frontendzie
Zależności
Po stronie aplikacji frontendowej wykorzystamy bibliotekę keycloak-angular
(link).
W package.json
dodamy zależności:
"keycloak-angular": "^7.0.1",
"keycloak-js": "^6.0.1"
Pobranie konfiguracji z backendu
Konfigurację pobierzemy uderzając na endpoint backendu.
Przy pierwszym pobraniu konfiguracji z KeycloakConfigService
zostanie wykonany request, a wynik zostanie zapisany. Kolejne pobrania konfiguracji będą już zwracać zapisaną konfigurację.
@Injectable({providedIn: 'root'})
export class KeycloakConfigService {
private config: KeycloakConfig;
constructor(private http: HttpClient) {
}
getConfig(): Observable<KeycloakConfig> {
if (this.config) {
return of(this.config);
} else {
const configObservable = this.http.get<KeycloakConfig>('/api/keycloak/config');
configObservable.subscribe(config => this.config = config);
return configObservable;
}
}
}
Konfiguracja
Pobraną konfigurację wykorzystamy w injection tokenie, definiując w app.module.ts
provider dla tokenu APP_INITIALIZER
:
{
provide: APP_INITIALIZER,
useFactory: initializer,
multi: true,
deps: [KeycloakService, KeycloakConfigService]
}
Jako initializer
utworzymy funkcję wykorzystującą nasz KeycloakConfigService
oraz inicjalizującą KeycloakService
z biblioteki keycloak-angular
:
export function initializer(keycloakService: KeycloakService, keycloakConfigService: KeycloakConfigService): () => Promise<boolean> {
return (): Promise<boolean> => keycloakConfigService.getConfig()
.pipe(
filter(config => config.enabled),
flatMap(config => {
return keycloakService.init({
config: {
url: config.authServerUrl,
realm: config.realm,
clientId: config.resource,
credentials: {
secret: config.credentials.secret
}
},
initOptions: {
onLoad: 'check-sso',
checkLoginIframe: false
}
});
})).toPromise();
}
Zabezpieczenie route’ów
Aby zabezpieczyć routy, wykorzystamy Angular Route Guard
.
Stworzymy AppAuthGuard
rozszerzając KeycloakAuthGuard
oraz implementując canActivate.
W konstruktorze pobierzemy konfigurację z KeycloakConfigService
, aby zweryfikować czy uwierzytelnianie jest włączone (parametr keycloak.enabled
w application.yml
), oraz czy użytkownik posiada wymaganą rolę:
@Injectable({
providedIn: 'root'
})
export class AppAuthGuard extends KeycloakAuthGuard {
isAuthEnabled: boolean = true;
requiredUserRole: string;
constructor(protected router: Router, protected keycloakService: KeycloakService, private keycloakConfigService: KeycloakConfigService) {
super(router, keycloakService);
this.keycloakConfigService.getConfig().subscribe(config => {
this.isAuthEnabled = config.enabled;
this.requiredUserRole = config.requiredUserRole;
});
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
if (!this.isAuthEnabled) {
return Promise.resolve(true);
} else {
return super.canActivate(route, state) as Promise<boolean>;
}
}
isAccessAllowed(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return new Promise(async (resolve) => {
if (!this.authenticated) {
return this.keycloakService.login();
}
resolve(this.roles.includes(this.requiredUserRole));
});
}
}
a następnie wykorzystamy naszego guarda w app-routing.module.ts
do zabezpieczenia routa protected
:
const routes: Routes = [
{
path: '',
component: PublicComponent
},
{
path: 'protected',
component: ProtectedComponent,
canActivate: [AppAuthGuard]
}
];
Testy
Uruchomienie
Jeśli mamy już uruchomiony serwer Keycloak, możemy zbudować aplikacje i obraz dockerowy, a następnie uruchomić kontener.
W tym celu w głównym folderze repozytorium najpierw budujemy aplikacje:
./gradlew build
po zbudowaniu tworzymy obraz dockerowy:
docker build -t michuu93/spring-angular-keycloak-demo .
a na koniec uruchamiamy kontener:
docker run --rm --name spring-angular-keycloak-demo --network host michuu93/spring-angular-keycloak-demo
Ponieważ zarówno aplikacje z wnętrza kontenera, jak i my z przeglądarki internetowej musimy mieć dostęp do serwera Keycloak pod tym samym adresem, nie wnikając w zasadę działania sieci w dockerze uruchomiliśmy kontener z opcją --network host
.
Dzięki temu nie musimy wystawiać dodatkowo żadnego proxy ani podmieniać hostów w naszym systemie. Jest to jednak ułatwienie na potrzeby demo i nie powinniśmy go wykorzystywać na produkcji.
Weryfikacja działania uwierzytelniania
Po uruchomieniu możemy sprawdzić aplikacje w działaniu!
Przechodzimy na stronę localhost:9082 i powinniśmy zobaczyć aplikację frontendową, a dokładniej ToolbarComponent
oraz PublicComponent
:
Do dyspozycji mamy menu, z którego możemy przejść do:
Public
- niezabezpieczonego route zPublicComponent
;Protected
- zabezpieczonego route zProtectedComponent
;Keycloak Configuration
- niezabezpieczonego endpointu udostępniającego konfiguację Keycloak z backendu;Backend Hello
- zabezpieczonego endpointu zwracającego tekstHello from the Backend!
;Logout (backend)
- wylogowania z backendu;Logout (frontend)
- przycisk widoczny tylko po zalogowaniu na frontendzie, który wylogowuje nas z frontendu.
Sprawdźmy więc czy mamy dostęp do Keycloak Configuration
bez zalogowania:
Wygląda na to, że publicznie dostępny route i endpoint api, działają poprawnie.
Jeśli teraz przejdziemy do Protected
, zostaniemy przekierowani na stronę logowania Keycloak:
http://localhost:8180/auth/realms/SpringBootAngular/protocol/openid-connect/auth?client_id=SpringBootAngularClient&redirect_uri=http%3A%2F%2Flocalhost%3A9082%2F%23%2F&state=3e007783-4772-48dd-8b31-4bfe3cc9c42c&response_mode=fragment&response_type=code&scope=openid&nonce=023ab545-32dc-4bdc-9cbf-12cad1d0c944
Jak widzimy, adres zawiera informacje, takie jak nazwa protokołu, realm, client czy adres, na który mamy zostać przekierowani po zalogowaniu.
Jeśli zalogujemy się użytkownikiem posiadającym rolę user_role
(user:password
), to zostaniemy przekierowani z powrotem do naszej aplikacji:
Dodatkowo mamy jeszcze tylko przycisk Logout (frontend)
. Jesteśmy teraz zalogowani, więc możemy przejść do Protected
:
Zadziała również Backend Hello
:
Jeśli wylogujemy się teraz z frontendu - Logout (frontend)
, to zniknie przycisk Logout (frontend)
i nie będziemy mieli już dostępu do Protected
.
Cały czas jednak będziemy mieli dostęp do Backend Hello
. Dopiero kiedy wylogujemy się z backendu - Logout (backend)
, to stracimy dostęp do Backend Hello
.
Jednak jeśli będąc zalogowanym użytkownikiem, wylogujemy się z backendu a nie frontendu, to automatycznie zostaniemy również wylogowani z frontendu.
Dlaczego?
O tym jak działa Keycloak i tokeny, którymi się posługujemy (a dokładniej standard OpenID Connect) nie jest tematem tego wpisu. Co do kwestii wylogowywania, to temat (ze względu na złożoność implementacji) nadaje się na osobny wpis, szczególnie gdy mówimy o rozproszonych systemach z wieloma instancjami aplikacji, stojącymi za loadbalancerem.
Projekt demo posiada bardzo prosty mechanizm wylogowywania.
Wylogowanie z frontendu wywoła metodę logout
na serwisie biblioteki keycloak-angular
:
logout = async (): Promise<void> => await this.keycloakService.logout();
Natomiast wylogowanie z backendu uderzy na /api/logout
, co spowoduje wykonanie na backendzie:
fun logout(request: HttpServletRequest, response: HttpServletResponse) {
request.logout()
response.sendRedirect("/")
}
Na koniec sprawdźmy jeszcze użytkownika bez roli user_role
(user2:password2
).
Po zalogowaniu, w aplikacji powinniśmy zobaczyć te same przyciski w menu, co na użytkowniku posiadającym wymaganą rolę. Jest tak, ponieważ ukrywanie przycisków uzależniliśmy tylko od tego, czy użytkownik jest zalogowany, a nie czy ma uprawnienia do routa:
ngOnInit(): void {
this.keycloakService.isLoggedIn().then(isLogged => this.isLogged = isLogged);
}
Uderzenie na Backend Hello
również nie zadziała.
Uwierzytelnianie możemy bardzo szybko wyłączyć w obu aplikacjach, ustawiając keycloak.enabled
w application.yml
na false
.
Może być to przydatne np. na środowiskach testowych.
Na zakończenie
Mam nadzieję, że udało mi się w przystępny sposób przedstawić integrację aplikacji z serwerem Keycloak.
Nie jest to jedyny sposób implementacji, jeśli nasza aplikacja ma być bardziej elastyczna, możemy wykorzystać implementację standardu OAuth2 przy użyciu Spring Security
. Dzięki temu nie uzależnimy się od serwera Keycloak i w przyszłości będziemy mogli łatwiej zamienić go na inny serwer uwierzytelniania/autoryzacji.
Keycloak wystawia endpointy pod którymi udostępnia konfigurację (link).
Znajdziemy je w Realm Settings > General > Endpoints
, np. dla standardu OpenID Connect będzie to w naszym przypadku:
http://127.0.0.1:8180/auth/realms/SpringBootAngular/.well-known/openid-configuration
Mogą być one przydatne, jeśli aplikacja nie wykorzystuje adapterów Keycloak do połączenia z serwerem.
Nie musimy też wykorzystywać logowania do aplikacji przez przeglądarkę - możemy wykorzystać Keycloaka do uwierzytelniania aplikacji między sobą, np. kiedy integrujemy ze sobą różne moduły. Wtedy przydatna będzie opcja bearer-only
w konfiguracji clienta.
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA
technical
newsletter
-
SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
B2B 19 680 - 27 220 PLN netto -
REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
B2B 14 950 - 21 000 PLN netto -
ZOBACZ WSZYSTKIE OGŁOSZENIA