Keycloak: A real Scenario from Development to Production

Wei He
7 min readApr 24, 2019

--

Keycloak is a standalone authentication and authorization system based on Java and JBoss. Its powerful, comprehensive and amazing features can be found on its home page https://www.keycloak.org. I choose it as my web site UAA system also because it could be seamlessly integrated into my web architecture and it have had an active community as well as widely spread supports.

Development

Architecture

For my web applications I have a front end using Angular 7 and a back end using Spring Boot framework. There are resources whose access should be limited to the authenticated and authorized users. The following collaboration diagram shows the basic authentication and authorization workflow in the localhost development environment.

Development architecture

Keycloak configuration

First of all I created in Keycloak a new realm “myrealm”, a client “my-frontend” and a corresponding user with a role “web_user”.

Front end

For Angular I found a plugin to request token from Keycloak easily: https://github.com/mauriciovigolo/keycloak-angular. After installation of that module my configuration in environment.ts looked like this:

import { KeycloakConfig } from 'keycloak-angular';

const keycloakConfig: KeycloakConfig = {
url: 'http://127.0.0.1:8080/auth',
realm: 'myrealm',
clientId: 'my-frontend'
};

export const environment = {
production: false,
authServiceApiUrl: 'http://127.0.0.1:8080/auth',
backendServiceApiUrl: 'http://127.0.0.1:8088/backend/',
keycloak: keycloakConfig
};

I created a class app-init.ts to initialize the configuration

import { KeycloakService } from 'keycloak-angular';
import {environment} from '../../environments/environment';

export function initializer(keycloak: KeycloakService): () => Promise<any> {
return (): Promise<any> => {
return new Promise<any>(async (resolve, reject) => {
try {
await keycloak.init({
config: environment.keycloak,
enableBearerInterceptor: true,
bearerExcludedUrls: ['/', '/home', '/asset']
});
resolve();
} catch (e) {
reject(e);
}
});
};
}

and defined it as a provider in app.module.ts

import { KeycloakService, KeycloakAngularModule } from 'keycloak-angular';NgModule({
declarations: [
...
],
imports: [
...
KeycloakAngularModule
],
providers: [
...,
{
provide: APP_INITIALIZER,
useFactory: initializer,
multi: true,
deps: [KeycloakService]
}
],
bootstrap: [AppComponent]
})

Then I could use Keycloak in a service class to authenticate the user and get the token for his operations, for example in case a user would like to call an end point to upload something in a service class:

public backendServiceApiUrl = 
environment.backendServiceApiUrl;

constructor(private httpClient: HttpClient,
private keycloakService: KeycloakService) {}

async upload(formData: FormData): Promise<any> {
await this.getAccessToken2Header();
const postHttpOptions = {
headers: this.httpHeaderWithToken
};
return this.httpClient
.post(`${this. backendServiceApiUrl}/upload`,
formData, postHttpOptions)
.pipe(
catchError(this.handleError('ServiceName',
'upload',
[]))
).toPromise();
}

getAccessToken2Header(): Promise<any> {
const promise = new Promise((resolve, reject) => {
this.keycloakService.addTokenToHeader()
.toPromise().then(
httpHeaders => {
this.httpHeaderWithToken = httpHeaders;
resolve();
}, msg => {
reject(msg);
}
);
});
return promise;
}

handleError<T> (serviceName = '',
operation = 'operation',
result = {} as T) {

return (error: HttpErrorResponse): Observable<T> => {
console.error(error); // log to console instead

const message = (error.error instanceof ErrorEvent) ?
error.error.message :
`{error code: ${error.status},
body: "${error.message}"}`;

// -> Return a safe result.
return of( result );
};
}

Normally the HTTP-Client of Angular 7 returns an Observable. I had to convert it to a Promise in order to use “await” to make sure that the header have had been attained before the request was sent.

Back end

My Spring Boot application was called “backend” and it has been configured with the respective Keycloak information in the application.properties for the token validation:

keycloak.auth-server-url=http://127.0.0.1:8080/auth
keycloak.realm=myrealm
keycloak.resource=my-frontend
keycloak.public-client=true
keycloak.principal-attribute=preferred_username
spring.main.allow-bean-definition-overriding=true

The last line above was important to allow me configure the Spring Security with the Keycloak adapter for Spring Boot as following code snippet:

@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses=KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
/**
* Registers the KeycloakAuthenticationProvider
* with the authentication manager.
*/
@Autowired
public void configureGlobal(
AuthenticationManagerBuilder auth)
throws Exception {
KeycloakAuthenticationProvider
keycloakAuthenticationProvider
= keycloakAuthenticationProvider();
keycloakAuthenticationProvider
.setGrantedAuthoritiesMapper(
new SimpleAuthorityMapper());
auth
.authenticationProvider(
keycloakAuthenticationProvider);
}

@Bean
public KeycloakConfigResolver
KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}

/**
* Defines the session authentication strategy.
*/
@Bean
@Override
protected SessionAuthenticationStrategy
sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(
new SessionRegistryImpl());
}

@Override
protected void configure(HttpSecurity http)
throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/*")
.permitAll()
.antMatchers("/*")
.hasRole("web_user")
.anyRequest()
.permitAll();
http.csrf().disable(); }
}

In the configuration above I had to allow the HTTP method OPTION because the front end used it firstly to make a “try” for the protected resource automatically. The CRSF had to be disabled to make the front end call on more than one back end services possible.

Production

Architecture

In the production environment the architecture has been a little more complex.

First of all I had to use HTTPS instead of HTTP to secure the client/server communication, otherwise the username and password would be transferred in plain text, which would be suffered by eavesdropping.

Secondly, the Ng server running on 4200 is not recommended to be used in production, I had to distribute the transpiled front end code to a web server. I chose using Apache2 as the HTTP Server to host the web pages and the JavaScripts.

Thirdly, I didn’t like to open any ports except HTTP/HTTPS of my server to the Internet, therefore I had to use the Apache2 as a Reverse Proxy.

Finally, my web server deployment layout had to be like the following figure:

Production architecture

Front end

Before I transpiled the Angular 7 code to JavaScript, I had to create an environment configuration file environment.prod.ts

import { KeycloakConfig } from 'keycloak-angular';


const keycloakConfig: KeycloakConfig = {

url: 'https://auth.example.com/auth',

realm: 'myrealm',

clientId: 'my-frontend'

};


export const environment = {

production: true,

authServiceApiUrl: 'https://auth.example.com/auth',

backendServiceApiUrl: 'https://www.example.com/backend,

keycloak: keycloakConfig

};

Why did I not use https://www.example.com/auth for the URL to Keycloak? I will explain it later.

Then I transpiled the Angular code to JavaScript via the command:

ng build --prod  --base-href /my-frontend/

After then I uploaded the front end code onto my server www.example.com under the directory /srv/www/htdocs/my-frontend/.

Back end

The application properties was respectively modified:

keycloak.auth-server-url=https://auth.example.com/auth
keycloak.realm=myrealm
keycloak.resource=my-frontend
keycloak.public-client=true
keycloak.principal-attribute=preferred_username
spring.main.allow-bean-definition-overriding=true

Apache configuration

Firstly, of course, I must install the SSL certificate on my web server — Apache. I chose the Letsencrypt because it was free of charge and the usage of certbot was simple and straightforward.

Secondly I configured the /etc/apache2/vhosts/vhost-ssl.conf in my OpenSUSE:

<VirtualHost *:80>

ServerAdmin admin@example.com

ServerName www.example.c0m

Redirect permanent / https://www.example.com/

RewriteEngine on

RewriteCond %{SERVER_NAME} =www.example.com

RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]

</VirtualHost>

<VirtualHost *:443>

DocumentRoot "/srv/www/htdocs"

ServerName www.example.com:443

ServerAdmin admin@example.com

Include /etc/letsencrypt/options-ssl-apache.conf

ErrorLog /var/log/apache2/error_log

TransferLog /var/log/apache2/access_log

ProxyPreserveHost On

SSLProxyEngine On

SSLProxyCheckPeerCN On

SSLProxyCheckPeerExpire On

RequestHeader set X-Forwarded-Proto "https"

RequestHeader set X-Forwarded-Port "443"

ProxyPass /backend/ http://www.example.com:8088/

ProxyPassReverse /backend/ http://www.example.com:8088/

ProxyVia On

SSLEngine on

SSLProtocol all -SSLv2 -SSLv3

SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5

<FilesMatch "\.(cgi|shtml|phtml|php)$">

SSLOptions +StdEnvVars

</FilesMatch>

<Directory "/srv/www/cgi-bin">

SSLOptions +StdEnvVars

</Directory>

<Directory "/srv/www/htdocs">

Options Indexes FollowSymLinks

AllowOverride None

Require all granted

FallbackResource /my-frontend/index.html

</Directory>
BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0

CustomLog /var/log/apache2/ssl_request_log ssl_combined

ServerAlias example.com

Include /etc/letsencrypt/options-ssl-apache.conf

SSLCertificateFile /etc/letsencrypt/live/www.example.com/fullchain.pem

SSLCertificateKeyFile /etc/letsencrypt/live/www.example.com/privkey.pem

</VirtualHost>

Let me now explain why I have not used the www.example.com to proxy the Keycloak URL. I configured the proxy to the Keycloak like this:

ProxyPass /auth/ http://www.example.com:8080/

ProxyPassReverse /auth/ http://www.example.com:8080/

The Keycloak but did not know the /auth suffix and could not generate a correct admin URL through which one could initialize the Keycloak.

What if I configured the proxy like so?

ProxyPass / http://www.example.com:8080/

Don’t forget that the root path have already been assigned to the front end code by setting

FallbackResource /my-frontend/index.html

Therefore my work around was to add another root server URL within subdomain auth.example.com:

<VirtualHost *:80>

ServerAdmin admin@example.com

ServerName auth.example.c0m

Redirect permanent / https://auth.example.com/

RewriteEngine on

RewriteCond %{SERVER_NAME} =auth.example.com

RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]

</VirtualHost>

<VirtualHost *:443>

DocumentRoot "/srv/www/htdocs"

ServerName auth.example.com:443

ServerAdmin admin@example.com

Include /etc/letsencrypt/options-ssl-apache.conf

ErrorLog /var/log/apache2/error_log

TransferLog /var/log/apache2/access_log

ProxyPreserveHost On

SSLProxyEngine On

SSLProxyCheckPeerCN On

SSLProxyCheckPeerExpire On

RequestHeader set X-Forwarded-Proto "https"

RequestHeader set X-Forwarded-Port "443"

ProxyPass / http://auth.example.com:8080/

ProxyPassReverse / http://auth.example.com:8080/

ProxyVia On

SSLEngine on

SSLProtocol all -SSLv2 -SSLv3

SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5

<FilesMatch "\.(cgi|shtml|phtml|php)$">

SSLOptions +StdEnvVars

</FilesMatch>

<Directory "/srv/www/cgi-bin">

SSLOptions +StdEnvVars

</Directory>

<Directory "/srv/www/htdocs">

Options Indexes FollowSymLinks

AllowOverride None

Require all granted

</Directory>

BrowserMatch "MSIE [2-5]" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0

CustomLog /var/log/apache2/ssl_request_log ssl_combined

ServerAlias example.com

Include /etc/letsencrypt/options-ssl-apache.conf

SSLCertificateFile /etc/letsencrypt/live/auth.example.com/fullchain.pem

SSLCertificateKeyFile /etc/letsencrypt/live/auth.example.com/privkey.pem

</VirtualHost>

Certainly I had to add the subdomain hostname into the /etc/hosts as alias of the 127.0.0.1 and also generated a certificate for the subdomain.

One last thing

However, after the URL problem was resolved I have still got the error by forwarding: WWW-Authenticate: Bearer realm=”myrealm”, error=”invalid_token”, error_description=”Didn’t find publicKey for specified kid”. I checked the Keycloak and found the public key was there. Why the kid could not be found? It was because that the new certificate of the subdomain could not be recognized by the Java. I had to add the certificate manually:

# openssl x509 -in <(openssl s_client -connect auth.example.com:443 -prexit 2>/dev/null) -out ~/auth.example.com.crt# keytool -importcert -file ~/auth.example.com.crt -alias keycloak -keystore ${JDK_HOME}/jre/lib/security/cacerts -storepass changeit

Lessons learnt

After all of these efforts the authentication has worked as expected. Though the services were deployed on the same server just on different ports, they have been well extendable to be deployed to other servers with different URLs.

I have learnt mainly from the practice that the completion of the development is far away from the deployment to the production running. Alone for this small architecture there have been many and will still be lots of coming considerations for security, which I can hardly find any easy and direct answers in Internet, because the production architectures are very framework and platform specific, other than the localhost.

--

--

Wei He
Wei He

Written by Wei He

10 years software architect who likes designing and programming with Java and Angular, who can lead, participate and follow, who is always listening and open

Responses (5)