Firstly, I inform you that i'm a junior developer. So I beg for your tolerance regarding to my question :)
What I'm trying to do is a simple prod POC with Keycloak for an Angular 8 frontend with a Java backend and a Postgres db. I'm using an OVH VPS, with Nginx / Certbot for the ssl and https management.
What is not working: - access to a private url (protected by keycloak, using [AppAuthGuard] in my Angular app : Keycloak correctly redirect on the Login page, but the login doesn't work. I get an error from Keycloak : "We are sorry... An error occurred, please login again through your application.".
My browser says : "Status Code: 400 Bad Request" And when I check my Keycloak container logs, I have a warning:
WARN [org.keycloak.events] (default task-1) type=LOGIN_ERROR, realmId=TheLibrary, clientId=null, userId=null, ipAddress=192.168.80.1, error=invalid_code
That being said, my urls does have realm and clientId where it must be...
What is working fine: - url redirection using subdomain I created on my VPS, - access to my Keycloak admin console, - access to a public url of my Frontend (I mean a url without [AppAuthGuard]) - everything, if I run my backend / frontend / Keycloak code in local environment, without ssl or https.
I'm struggling on that login error since 4 days. It's driving me crazy.. and I have pretty much no clue to investigate.
I did't find any similar issue online.
The only thing I noticed, as a difference between my local poc which is working fine (with a local standalone Keycloak), is a cookie that appears to be loaded after a successful Login, and it is not loaded when I get the error in my prod poc (with everything online on my vps). I can see it with developer mode in Chrome > Network > all > ...
Here a my code and configuration :
FRONTEND
app-routing.module.ts
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {BooksComponent} from './shared/books/books.component';
import {AppAuthGuard} from './app-auth.guard';
const routes: Routes = [
{
path: 'books', component: BooksComponent
},
{
path: 'search',
loadChildren: () => import('./search/search.module').then(mod => mod.SearchModule),
// canActivate: [AppAuthGuard]
},
{
path: 'last-release',
loadChildren: () => import('./last-release/last-release.module').then(mod => mod.LastReleaseModule),
// canActivate: [AppAuthGuard]
},
{
path: 'loan',
loadChildren: () => import('./loan/loan.module').then(mod => mod.LoanModule),
canActivate: [AppAuthGuard],
data: { roles: ['user'] }
},
{
path: '', redirectTo: '/books',
pathMatch: 'full'
},
{
path: '**',
redirectTo: '/'
}
];
@NgModule({
declarations: [],
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
app-auth.guard.ts
import {Injectable} from '@angular/core';
import {ActivatedRouteSnapshot, Router} from '@angular/router';
import {KeycloakAuthGuard, KeycloakService} from 'keycloak-angular';
@Injectable({
providedIn: 'root'
})
export class AppAuthGuard extends KeycloakAuthGuard {
constructor(protected router: Router,
protected keycloakAngular: KeycloakService) {
super(router, keycloakAngular);
}
isAccessAllowed(route: ActivatedRouteSnapshot): Promise<boolean> {
return new Promise(async (resolve, reject) => {
const requiredRoles = route.data.roles;
console.log('Class: AppAuthGuard, Function: , Line 19 (): '
, route);
if (route.url[0].path === '/books') {
return resolve(true);
} else if (!this.authenticated ) {
this.keycloakAngular.login();
return resolve(true);
}
if (!requiredRoles || requiredRoles.length === 0) {
return resolve(true);
} else {
if (!this.roles || this.roles.length === 0) {
resolve(false);
}
let granted = false;
for (const requiredRole of requiredRoles) {
if (this.roles.indexOf(requiredRole) > -1) {
granted = true;
break;
}
}
resolve(granted);
}
});
}
}
app.module.ts
import {BrowserModule} from '@angular/platform-browser';
import {APP_INITIALIZER, ApplicationRef, DoBootstrap, NgModule} from '@angular/core';
import {AppComponent} from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AppRoutingModule} from './app-routing.module';
import {BooksComponent} from './shared/books/books.component';
import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http';
import {ReactiveFormsModule} from '@angular/forms';
import {KeycloakAngularModule, KeycloakService} from 'keycloak-angular';
import {environment} from '../environments/environment';
import {HttpErrorInterceptor} from './shared/http-error.interceptor';
import {HeaderComponent} from './shared/header/header.component';
import {SideNavComponent} from './shared/side-nav/side-nav.component';
import {initializer} from './app.init';
const keycloakService = new KeycloakService();
@NgModule({
declarations: [
AppComponent,
BooksComponent,
HeaderComponent,
SideNavComponent
],
imports: [
BrowserModule,
BrowserAnimationsModule,
HttpClientModule,
AppRoutingModule,
ReactiveFormsModule,
KeycloakAngularModule
],
providers: [
{
provide: KeycloakService,
useValue: keycloakService
}
],
entryComponents: [AppComponent]
})
export class AppModule implements DoBootstrap {
ngDoBootstrap(appRef: ApplicationRef) {
keycloakService
.init({
config: environment.keycloak,
initOptions: {
checkLoginIframe: false
},
})
.then(() => {
console.log('[ngDoBootstrap] bootstrap app');
appRef.bootstrap(AppComponent);
})
.catch(error => console.error('[ngDoBootstrap] init Keycloak failed', error));
}
}
environment.prod.ts
export const baseUrls = {
catalog: 'https://thelibrary.ms.catalog.mypoc.online/api'
};
export const environment = {
production: true,
authServiceApiUrl: 'https://auth.thelibrary.mypoc.online/auth',
keycloak: {
url: 'https://auth.thelibrary.mypoc.online/auth',
realm: 'TheLibrary',
clientId: 'thelibrary-app',
'ssl-required': 'all',
'public-client': true
},
baseUrl: {
catalog: {
getBooks: baseUrls.catalog + '/books'
}
}
};
Dockerfile-front
# base image
FROM node:latest
# install chrome for protractor tests
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
RUN apt-get update && apt-get install -yq google-chrome-stable
# set working directory
WORKDIR /app
# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH
# install and cache app dependencies
COPY build/front/package.json /app/package.json
RUN npm install
RUN npm install -g @angular/cli@8.3.24
# add app
COPY build/front/. /app
# start app
RUN pwd
RUN ls
CMD ng serve --disable-host-check --configuration=production --proxy-config proxy.conf.json --host 0.0.0.0 --public-host https://www.thelibrary.mypoc.online
Bytheway, is my "ng serve" line ok?
Docker-compose.yml
version: '3.7'
services:
db-thelibrary:
build:
context: ./db
dockerfile: Dockerfile-db
container_name: cont-db-thelibrary
restart: unless-stopped
ports:
- 5432:5432
environment:
POSTGRES_DB: ########
POSTGRES_USER: ########
POSTGRES_PASSWORD: ########
volumes:
- db_data:/var/lib/postgres/data
networks:
- network-thelibrary
back-thelibrary:
depends_on:
- db-thelibrary
build:
context: ./build/back
dockerfile: Dockerfile
container_name: cont-back-thelibrary
ports:
- '127.0.0.1:8090:8090'
environment:
SPRING_PROFILES_ACTIVE: prod
SPRING_DATASOURCE_USERNAME: ########
SPRING_DATASOURCE_PASSWORD: ########
SPRING_DATASOURCE_URL: jdbc:postgresql://db-thelibrary:5432/db_thelibrary?currentSchema=dev
networks:
- network-thelibrary
front-thelibrary:
build:
context: build/front
dockerfile: Dockerfile-front
container_name: cont-front-thelibrary
ports:
- '127.0.0.1:4200:4200'
networks:
- network-thelibrary
networks:
network-thelibrary:
driver: bridge
volumes:
front:
db_data:
KEYCLOAK
Docker-compose.yml
version: '3.7'
services:
db-keycloak:
image: postgres
container_name: cont-db-keycloak
restart: always
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: #######
POSTGRES_USER: ########
POSTGRES_PASSWORD: #########
networks:
- network-keycloak
keycloak:
depends_on:
- db-keycloak
image: jboss/keycloak
container_name: cont-keycloak
environment:
KEYCLOAK_HOSTNAME: www.auth.thelibrary.mypoc.online
KEYCLOAK_ALWAYS_HTTPS: 'true'
PROXY_ADDRESS_FORWARDING: 'true'
KEYCLOAK_LOGLEVEL: DEBUG
KEYCLOAK_USER: ######
KEYCLOAK_PASSWORD: ##########
DB_VENDOR: POSTGRES
DB_ADDR: db-keycloak
DB_PORT: 5432
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: ###
# Uncomment the line below if you want to specify JDBC parameters. The parameter below is just an example, and it shouldn't be used in production without knowledge. It is highly recommended that you read the PostgreSQL JDBC driver documentation in order to use it.
# JDBC_PARAMS: "ssl=true"
command:
-Djboss.socket.binding.port-offset=001
ports:
- '127.0.0.1:8081:8081'
- '127.0.0.1:8443:8443'
volumes:
- config:/config/
networks:
- network-keycloak
networks:
network-keycloak:
driver: bridge
volumes:
config:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/srv/keycloak/config'
postgres_data:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/srv/keycloak/postgres_data'
Keycloak client settings :
- Realm : TheLibrary
- Client ID : thelibrary-app
- Root URL : https://www.thelibrary.mypoc.online
- Valid Redirect URIs : https://www.thelibrary.mypoc.online/*
- Base URL : https://www.thelibrary.mypoc.online
- Web Origins : https://www.thelibrary.mypoc.online
Of course I correctly created two role, admin (composite role, including user) and user, linked to my client.
NGINX
My VPS run with Debian 9 Stretch. I installed Nginx and Certbot on it, with very basic settings, to handle that https://my.sub.url are well redirected on https://ipAdress:port. I followed this link to do so: https://certbot.eff.org/lets-encrypt/debianstretch-nginx I created the following .conf files in /etc/nginx/sites-available/
nginx.conf
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
gzip_disable "msie6";
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
thelibrary.conf
server {
server_name www.thelibrary.mypoc.online thelibrary.mypoc.online;
location / {
proxy_pass http://127.0.0.1:4200;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_http_version 1.1;
add_header Set-Cookie cip=$remote_addr;
add_header Set-Cookie chost=$Host;
}
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/thelibrary.mypoc.online/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/thelibrary.mypoc.online/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.thelibrary.mypoc.online) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = thelibrary.mypoc.online) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name www.thelibrary.mypoc.online thelibrary.mypoc.online;
return 404; # managed by Certbot
}
keycloak.conf
server {
server_name www.auth.thelibrary.mypoc.online auth.thelibrary.mypoc.online;
location / {
proxy_pass http://127.0.0.1:8081;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_http_version 1.1;
add_header Set-Cookie cip=$remote_addr;
add_header Set-Cookie chost=$Host;
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/thelibrary.mypoc.online/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/thelibrary.mypoc.online/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.auth.thelibrary.mypoc.online) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = auth.thelibrary.mypoc.online) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name www.auth.thelibrary.mypoc.online auth.thelibrary.mypoc.online;
return 404; # managed by Certbot
}
Each of my components runs with Docker compose.
For more details, here is my github : https://github.com/TheLibraryGroup. It contains most of my config files.
It would be very nice if someone could help me. I'm pretty sure it's a very little thing to do, but I can't figure it out since I'm really new to devops and networks topic.
Thank you very much for you time!