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.
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:
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.