If you build a default monolithic application JHipster with Angular and OAuth2 as your type of authentication using Keycloak, everything works apparently well. However, if you want to separate back-end from front-end (for that, you need to set SERVER_API_URL variable defined in the webpack.commons.js, as it is mentioned here), you will get several errors in the console of your web browser, such as,
Access to XMLHttpRequest at 'http://localhost:8080/oauth2/authorization/oidc' (redirected from 'http://localhost:8080/api/account') from origin 'http://localhost:9000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Some images of these errors are displayed in the following pictures
Errors in console
Error in network
In this post we are to going to describe how to solve these errors.
Prerequisites
We are supposed to have created a monolithic JHipster (6.4.1) application with OAuth2 as type of authentication.
We are also supposed to have a Keycloak docker container running. We have started this container with docker-compose, following what is indicated here. The command is:
docker-compose -f src/main/docker/keycloak.yml up
The content of keycloak.yml is:
version: '2'
services:
keycloak:
image: jboss/keycloak:7.0.0
command:
[
'-b',
'0.0.0.0',
'-Dkeycloak.migration.action=import',
'-Dkeycloak.migration.provider=dir',
'-Dkeycloak.migration.dir=/opt/jboss/keycloak/realm-config',
'-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
'-Djboss.socket.binding.port-offset=1000',
]
volumes:
- ./realm-config:/opt/jboss/keycloak/realm-config
environment:
- KEYCLOAK_USER=admin
- KEYCLOAK_PASSWORD=admin
- DB_VENDOR=h2
ports:
- 9080:9080
- 9443:9443
- 10990:10990
After starting Keycloak container with a JHispter realm, in our case at localhost, you should get a picture similar to this one.
Jhipster realm in Keycloak
Keycloak service
When you face the problem described at the beginning of this post, the first thing you think is that you have a CORS problem and, in consequence, you decide to configure the CorsFilter in WebConfigurer or change SecurityConfiguration class. However, the real problem is that JHipster doesn't add anything in the front-end, Angular in our case, about OAuth2. In fact, the code looks very similar to session authentication. Client has to send token to server when the authentication is established. So we need basically add Keycloak node module and a token interceptor to send the authentication token from front-end to back-end.
We add Keycloak module by entering this into the terminal:
npm install keycloak-js@7.0.0 --save
Note that we are installing version 7.0.0 because we installed version 7.0.0 of Keycloak. Because this module is really a plain javascript library, we need to reference it in several files:
- angular.js
"prefix": "jhi",
"architect": {
"build": {
"options": {
"scripts": ["./node_modules/keycloak-js/dist/keycloak.min.js"]
}
}
}
- webpack.commons.js
new CopyWebpackPlugin([
{ from: './node_modules/swagger-ui/dist/css', to: 'swagger-ui/dist/css' },
{ from: './node_modules/swagger-ui/dist/lib', to: 'swagger-ui/dist/lib' },
{ from: './node_modules/swagger-ui/dist/swagger-ui.min.js', to: 'swagger-ui/dist/swagger-ui.min.js' },
{ from: './src/main/webapp/swagger-ui/', to: 'swagger-ui' },
{ from: './src/main/webapp/content/', to: 'content' },
{ from: './src/main/webapp/favicon.ico', to: 'favicon.ico' },
{ from: './src/main/webapp/manifest.webapp', to: 'manifest.webapp' },
{ from: './node_modules/keycloak-js/dist/keycloak.min.js', to: 'keycloak.min.js' },
// jhipster-needle-add-assets-to-webpack - JHipster will add/remove third-party resources in this array
- index.html
<link rel="stylesheet" href="content/css/loading.css">
<script src="keycloak.min.js"></script>
<!-- jhipster-needle-add-resources-to-root - JHipster will add new resources here -->
Now we add keycloak.service.ts under app/core/auth
import { Injectable } from '@angular/core';
declare const Keycloak: any;
@Injectable({
providedIn: 'root'
})
export class KeycloakService {
public keycloakAuth: any;
constructor() { }
init(): Promise {
return new Promise((resolve, reject) => {
const config = {
url: 'http://localhost:9080/auth',
realm: 'jhipster',
clientId: 'web\_app',
credentials: 'web\_app'
};
this.keycloakAuth = new Keycloak(config);
this.keycloakAuth
.init({ onLoad: 'login-required' })
.success(() => {
resolve();
})
.error(() => {
reject();
});
});
}
getToken(): string {
if (this.keycloakAuth === undefined) return undefined;
return this.keycloakAuth.token;
}
}
As you can see in the code above, the init function receives an onLoad with 'login-required'. This code is because we want to be logged in when it gets called, and if we are not authenticated, it will redirect to login page.
Notice the config object passed into the Keycloak constructor, in which we directly set the Keycloak url, realm, clientId and credentials. This code could be improved calling to http://localhost:8080/api/auth-info
. If you are really interested in how to call this url, you will want to generate an Ionic for JHipster project and review auth.service.ts under app/auth.
Next, we tweak app.module.ts to initialize Keycloak service. Firstly, you must import HTTP_INITIALIZER and KeycloakService and create a factory provider in order to run our init function we wrote earlier when the app gets initialized.
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { KeycloakService } from './core/auth/keycloak.service';
export function kcFactory(keycloakService: KeycloakService): () => void {
return () => keycloakService.init();
}
We also add a providers section after the declarations one.
declarations: [JhiMainComponent, NavbarComponent, ErrorComponent, PageRibbonComponent, ActiveMenuDirective, FooterComponent],
providers:[
KeycloakService,
{
provide: APP_INITIALIZER,
useFactory: kcFactory,
deps: [KeycloakService],
multi: true
}
],
bootstrap: [JhiMainComponent]
Interceptor
We need to send the authentication token every http request, adding it in the header. So we create an interceptor, token.interceptor.ts, under app/blocks/interceptor.
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';
import { KeycloakService } from 'app/core/auth/keycloak.service';
@Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private kcService: KeycloakService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const authToken = this.kcService.getToken() || '';
request = request.clone({
setHeaders: {
Authorization: 'Bearer ' + authToken
}
});
return next.handle(request);
}
}
We have to tweak core.module.ts to activate the above interceptor, adding the following code in providers section:
.
.
.
},
{
provide: HTTP_INTERCEPTORS,
useClass: TokenInterceptor,
multi: true
}
],
Logout
Finally, we need to make some minor changes in LogoutResource.java to prevent program from an error when we hit on logout link.
import org.springframework.security.oauth2.jwt.Jwt;
// import org.springframework.security.oauth2.core.oidc.OidcIdToken;
.
.
.
public ResponseEntity logout(HttpServletRequest request,
@AuthenticationPrincipal Jwt idToken) {
// @AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) {
String logoutUrl = this.registration.getProviderDetails()
.getConfigurationMetadata().get("end\_session\_endpoint").toString();
.
.
.
If you want to get the code of this post, check out this Github repo.