In the previous article, we have implemented Two Factor Authentication with Spring Security and a Soft Token. The Angular client and Spring Boot application will be running on different ports in localhost. In production, It is recommended to deploy these applications separately in their own dedicated servers.
However, in some cases like infrastructure limitations, we may need to package them together in a single deployable unit. In this article, we are going to package both Spring REST API and Angular together in a single deployable JAR / WAR file.
Spring Boot Changes
Add Plugins
pom.xml
We are gonna use frontend-maven-plugin
to install node
& npm
and build Angular project
maven-resources-plugin
is used to copy the angular resources generated in angular-11-social-login/dist/Angular11SocialLogin/
directory to resources
directory in the Spring Boot Application.
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.11.0</version>
<configuration>
<workingDirectory>./</workingDirectory>
<nodeVersion>v12.18.4</nodeVersion>
<npmVersion>6.14.8</npmVersion>
<workingDirectory>../angular-11-social-login/</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/resources/</outputDirectory>
<resources>
<resource>
<directory>../angular-11-social-login/dist/Angular11SocialLogin/</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
Update Spring Security Configuration
WebSecurityConfig.java
Permit the Angular resources ("/index.html", "/.js", "/.js.map", "/.css", "/assets/img/.png", "/favicon.ico"
) to be accessed without authentication
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().csrf().disable().formLogin().disable().httpBasic().disable()
.exceptionHandling().authenticationEntryPoint(new RestAuthenticationEntryPoint()).and().authorizeRequests()
.antMatchers("/", "/error", "/api/all", "/api/auth/**", "/oauth2/**", "/index.html", "/*.js", "/*.js.map", "/*.css", "/assets/img/*.png", "/favicon.ico")
.permitAll().anyRequest().authenticated().and().oauth2Login().authorizationEndpoint().authorizationRequestRepository(cookieAuthorizationRequestRepository()).and()
.redirectionEndpoint().and().userInfoEndpoint().oidcUserService(customOidcUserService).userService(customOAuth2UserService).and().tokenEndpoint()
.accessTokenResponseClient(authorizationCodeTokenResponseClient()).and().successHandler(oAuth2AuthenticationSuccessHandler)
.failureHandler(oAuth2AuthenticationFailureHandler);
// Add our custom Token based authentication filter
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
OAuth2AuthenticationSuccessHandler.java
Earlier, we were using UriComponentsBuilder
to append the token as a query parameter in the client redirect URI. Now we are gonna configure Angular client to use hash in the URL (I’ll explain why it is required in the following sections). Since UriComponentsBuilder
treats hash as a fragment, we are not going to use that. Instead, we can simply append the token in the URL directly.
@Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Optional<String> redirectUri = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue);
if (redirectUri.isPresent() && !isAuthorizedRedirectUri(redirectUri.get())) {
throw new BadRequestException("Sorry! We've got an Unauthorized Redirect URI and can't proceed with the authentication");
}
String targetUrl = redirectUri.orElse(getDefaultTargetUrl());
LocalUser user = (LocalUser) authentication.getPrincipal();
String token = tokenProvider.createToken(user, true);
return targetUrl.concat("?token=" + token);
}
OAuth2AuthenticationFailureHandler.java
Remove the usage of UriComponentsBuilder
and directly append the error in the URL.
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String targetUrl = CookieUtils.getCookie(request, REDIRECT_URI_PARAM_COOKIE_NAME).map(Cookie::getValue).orElse(("/"));
targetUrl = targetUrl.concat("?error=" + exception.getLocalizedMessage());
httpCookieOAuth2AuthorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
Update Application Properties
application.properties
We need to change the port number of angular Since both front end and back end will be running on the same port number
app.oauth2.authorizedRedirectUris=http://localhost:8080/oauth2/redirect,myandroidapp://oauth2/redirect,myiosapp://oauth2/redirect
Angular Changes
Spring Tool Suite Resource Filters
Angular project will have node_modules
, .design
& node
(after maven build) folders which can be excluded in STS by adding Resource Filters. So that we can avoid some unnecessary processing time in the IDE
You can add resource filters from the Project properties as shown below
Project -> Properties -> Resource -> Resource Filters -> Add Filter
Routing
Since both Angular and Spring runs on the same server, Spring will try to find mappings for angular paths as well which will result in a white label error page. There are many solutions for this. To fix this issue, we are gonna configure Angular to use hash in the routings.
app-routing.module.ts
@NgModule({
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy', useHash: true })],
exports: [RouterModule]
})
export class AppRoutingModule { }
auth.interceptor.ts
Update the login path to include hash
import { HTTP_INTERCEPTORS, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { TokenStorageService } from '../_services/token-storage.service';
import {tap} from 'rxjs/operators';
import { Observable } from 'rxjs';
const TOKEN_HEADER_KEY = 'Authorization';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private token: TokenStorageService, private router: Router) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
let authReq = req;
const loginPath = '/#/login';
const token = this.token.getToken();
if (token != null) {
authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
}
return next.handle(authReq).pipe( tap(() => {},
(err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status !== 401 || window.location.pathname + location.hash === loginPath) {
return;
}
this.token.signOut();
window.location.href = loginPath;
}
}
));
}
}
export const authInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];
Update Client Redirect URI
app.constants.ts
Change the port number and encode the redirect URL in order to preserve the hash symbol.
export class AppConstants {
private static API_BASE_URL = "http://localhost:8080/";
private static OAUTH2_URL = AppConstants.API_BASE_URL + "oauth2/authorization/";
private static REDIRECT_URL = "?redirect_uri=" + encodeURIComponent("http://localhost:8080/#/login");
public static API_URL = AppConstants.API_BASE_URL + "api/";
public static AUTH_API = AppConstants.API_URL + "auth/";
public static GOOGLE_AUTH_URL = AppConstants.OAUTH2_URL + "google" + AppConstants.REDIRECT_URL;
public static FACEBOOK_AUTH_URL = AppConstants.OAUTH2_URL + "facebook" + AppConstants.REDIRECT_URL;
public static GITHUB_AUTH_URL = AppConstants.OAUTH2_URL + "github" + AppConstants.REDIRECT_URL;
public static LINKEDIN_AUTH_URL = AppConstants.OAUTH2_URL + "linkedin" + AppConstants.REDIRECT_URL;
}
Run with Maven
Build and Run the application with the maven command mvn spring-boot:run
This command will build both Angular and Spring application and run it.
WAR Packaging
If you wanna package the Angular and Spring Boot application into a war
file, then specify the packaging
type as war
and add maven-war-plugin
as shown below.
If you wanna exclude the embedded tomcat from war file
, then uncomment packagingExcludes
configuration. So that war
file can be deployed in an external tomcat server.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.javachinna</groupId>
<artifactId>demo</artifactId>
<version>1.1.0</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- mysql driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>dev.samstevens.totp</groupId>
<artifactId>totp-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.11.0</version>
<configuration>
<workingDirectory>./</workingDirectory>
<nodeVersion>v12.18.4</nodeVersion>
<npmVersion>6.14.8</npmVersion>
<workingDirectory>../angular-11-social-login/</workingDirectory>
</configuration>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/classes/resources/</outputDirectory>
<resources>
<resource>
<directory>../angular-11-social-login/dist/Angular11SocialLogin/</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<!-- <packagingExcludes>WEB-INF/lib/tomcat-*.jar</packagingExcludes> -->
</configuration>
</plugin>
</plugins>
</build>
</project>
Source Code
https://github.com/JavaChinna/spring-boot-angular-integration-demo
Conclusion
That’s all folks. In this article, we have integrated Spring Boot with Angular application.
Thank you for reading.
I was looking for integrated angular, and see the chnages are not working – String token = tokenProvider.createToken(user, true);
Can you push that piece of code
Are you looking for the code in the below repository? This repo should have everything. It is a working version only. I’m not sure how some pieces are missing for you. Let me know if you still face issues with this code. I’ll clone this and see if it works.
https://github.com/JavaChinna/spring-boot-angular-integration-demo