How to Package Angular Application with Spring REST API

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
Resource Filters

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.

Leave a Reply