Spring Boot Permissions/Routing/Packaging Fun with React.js and Okta

Ken Yee
9 min readJul 10, 2021

--

Introduction

This common setup isn’t as well documented as I expected given how prolifically the Okta folks blog, so I’m going to cover the issues I encountered recently when setting up a Spring Boot microservice API protected with Okta authentication and used by a React.js web app (sometimes referred to as an SPA or Single Page App) that is also served by the same microservice. I’ll also cover some of the issues I hit getting it packaged for use in production.

JWT vs. OIDC

First of all, it’s important to understand how you’re configuring Spring Boot with Okta authentication. You can either set it up for OIDC (basically OAuth2) authentication or JWT token (signed token with metadata) authentication.

OIDC is not used for Single Page Application web clients like React.js. The authentication forms and flows stay within Spring Boot with OIDC and session management is also done in Spring Boot. This flow requires an Okta ClientId/Secret keypair to be kept on the server so it can generate access tokens.

JWT authentication on the other hand is done with tokens that are validated by Okta servers. The authentication flow is done mostly on the web client (we’ll get to the “mostly” part when we configure Spring permissions). Both the server and the client only require the Issuing URL (used to validate tokens) and the ClientId. The React.js Okta library handles connection to the Okta server and refreshing the JWT token. The server just validates tokens it gets from the React.js client with Okta. All JWT scopes (e.g., email and groups) are managed on the Okta server.

CSRF

CSRF should be disabled for microservices that support only API calls and SPA. It’s only used for Form authentication based web sites. This is why CSRF tokens are needed to prevent malicious attacks from form submission:

CSRF diagram from https://phppot.com/php/cross-site-request-forgery-anti-csrf-protection-in-php/

CORS

During development of any React.js app, you’ll have to handle Cross Origin Resource Sharing. This means that your Javascript code is trying to contact a hostname/URL that isn’t the same URL as where your web app was loaded by the web browser. React.js apps typically are at http://localhost:3000 during development. At the network level, this is what actually happens so you can recognize it in Chrome devtools:

CORS Network Sequence from Wikipedia

Authorization Calls

Now that we have an understanding of JWT and CORS, we can see what the network traffic looks like for the login and logout process. We have to understand this a bit before we configure Spring Boot to handle everything.

As you can see in the diagram below, Okta login will redirect to /login/callback and include the JWT token. The logout will redirect to “/”. It’s important to note that these URLs go to your server, not back to the React.js client, so the server has to forward back to the React.js client in the browser.

Okta JWT Login/Logout

Okta Configuration

To support debugging of the React.js client, you’ll have to go to Security/API/TrustedOrigins in your Okta Admin Dashboard and add these values to the allowed CORS endpoints:

http://localhost:3000
http://localhost:8080

If you want to use only localhost:8080, you can also edit your package.json and add a proxy to get past CORS issues w/ React.js which Yarn starts on port 3000 (this is typically not worth it):

"proxy": "http://localhost:8080",

Login Redirect URLs

In the Okta Admin Dashboard, you’ll also have to configure your valid login redirect URLs as:

https://www.yourdomain.com/login/callback
http://localhost:3000/login/callback
http://localhost:8080/login/callback

Logout Redirect URLs

And finally, in the Okta Admin Dashboard, you need to configure your valid logout redirect URLs. All these are required to ensure your site is secure and the JWT authentication happens properly.

https://www.yourdomain.com
http://localhost:3000
http://localhost:8080

Spring Boot Permissions

Spring Boot permission configuration can be categorized into setup for CSRF, CORS (during local development), configuration for the React.js client, and permissions for your APIs that need JWT tokens.

CORS configuration and React.js resource handling is done via a class that provides a WebMvcConfigurer Bean:

@Configuration
class CorsConfig(val environment: Environment) {

val isLocalEnv: Boolean
get() = environment.activeProfiles.contains("local")

@Bean
fun corsConfigurer(): WebMvcConfigurer {
return object : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
if (isLocalEnv) {
// this disables CORS so we can debug the React client easily
registry.addMapping("/**")
.allowedHeaders("*")
.allowedOriginPatterns("*")
.allowedMethods("POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "DELETE", "GET")
.allowCredentials(true)
}
}

override fun addViewControllers(registry: ViewControllerRegistry) {
// this routes paths to the React.js client routes;
// excludes our service's read-only APIs, health endpoints, and Wwagger
registry.addViewController("/web/**")
.setViewName("forward:/")
registry.addViewController("{spring:^((?!/v?/|/health/|/swagger-ui/).)*$}")
.setViewName("forward:/")
}

override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
// this is used to handle the /login/callback from Okta from React.js
registry.addResourceHandler("/login/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(object : PathResourceResolver() {
override fun getResource(resourcePath: String, location: Resource): Resource {
val requestedResource: Resource = location.createRelative(resourcePath)
return if (requestedResource.exists() &&
requestedResource.isReadable
)
requestedResource else ClassPathResource(
"/static/index.html"
)
}
})
}
}
}
}

The WebMvcConfigurer’s addCorsMappings() only disables CORS if the currently active Spring profile is “local” which indicates it’s the local dev environment.

The React.js client’s routes live in the /web path and the overridden addViewControllers() adds all the special routing for it so that the browser gets routed properly by React.

Finally, addResourceHandlers() adds handling for any images in the /static folder and redirects for Okta’s /login callback.

A class that subclasses WebSecurityConfigurerAdapter is used to configure CRSF and API endpoint permissions:

@Configuration
class SecurityConfiguration(val environment: Environment) : WebSecurityConfigurerAdapter() {

val isLocalEnv: Boolean
get() = environment.activeProfiles.contains("local")

override fun configure(http: HttpSecurity) {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // web UI is SPA

// CSRF tokens not needed for SPA
http.csrf().disable()

http.authorizeRequests()
.requestMatchers(EndpointRequest.to(HealthEndpoint::class.java)).permitAll()
.antMatchers("/**/*.{js,html,css}", "/", "/static/**").permitAll()
.antMatchers("/web/**").permitAll()
.antMatchers("/built/**", "/images/**", "main.css", "/favicon*", "/site.webmanifest").permitAll()
.antMatchers("/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**").permitAll()
.antMatchers("/actuator", "/actuator/**").permitAll()
.antMatchers("/v1/query/**", "/v1/ui/**").permitAll()
.antMatchers("/login/**", "/logout").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt()

// Send a 401 message to the browser (w/o this, you'll see a blank page)
Okta.configureResourceServer401ResponseBody(http)
}

@Throws(Exception::class)
override fun configure(web: WebSecurity) {
web.ignoring().antMatchers(HttpMethod.OPTIONS)
}
}

First, since we don’t support server-side web pages, we can turn off sessions so they don’t waste memory. After that, CSRF is disabled because we don’t need it.

For URL permissions, we allow all the static resources needed by the React.js client. We also allow unauthenticated access to the Swagger UI and heath endpoints. Next we allow access to APIs that the React.js client uses as well as login/logout redirects. Anything not mentioned in this configuration is then configured to use JWT authentication by default.

The http OPTIONS method is also configured as allowed, because web browsers use this to check whether CORS is configured properly for the React.js call to the APIs.

MockMvc JWT Testing

Doing testing of authenticated APIs was also not documented that well; there is an Okta blog that describes how to do OIDC testing with Spring’s MockMvc that almost gives you enough hints to get it working.

Spring’s website doesn’t document this well either, so I had to get some help from folks on Gitter.im’s Spring Security forum (big thanks to Nicolas Fränkel pointing me in the right direction).

The core code is the following that sets up a valid security context with a JWT token:

private fun authenticationToken(jwtToken: Jwt): AbstractAuthenticationToken {
return JwtAuthenticationConverter().apply {
val claim = jwtToken.claims["sub"] as String
setPrincipalClaimName(claim)
}.convert(jwtToken)!!
}
private fun setupJwtMvcContext(email: String = "kyee@somewhere.com") {
val jwt = Jwt.withTokenValue(ID_TOKEN)
.header("alg", "none")
.claim("sub", email)
.build()
SecurityContextHolder.getContext().authentication = authenticationToken(jwt)
val authInjector = SecurityContextHolderAwareRequestFilter()
authInjector.afterPropertiesSet()
mvc = MockMvcBuilders.webAppContextSetup(this.context).build()
}
companion object {
// just a valid dummy JWT token
// see https://developer.okta.com/blog/2019/04/15/testing-spring-security-oauth-with-junit
private const val ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" +
".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsIm" +
"p0aSI6ImQzNWRmMTRkLTA5ZjYtNDhmZi04YTkzLTdjNmYwMzM5MzE1OSIsImlhdCI6MTU0M" +
"Tk3MTU4MywiZXhwIjoxNTQxOTc1MTgzfQ.QaQOarmV8xEUYV7yvWzX3cUE_4W1luMcWCwpr" +
"oqqUrg"
}

You can then call setupJwtMvcContext() at the beginning of a test to load the JWT info into the security context:

@Throws(Exception::class)
@Test
fun testAddAdminInvalid() {
setupJwtMvcContext()
val addRequest = AdminUser(
email = "invalidemail",
name = "Test User"
)

val postBody = addRequest.asJsonString()
mvc.perform(
MockMvcRequestBuilders.post("/v1/admin")
.content(postBody)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
)
.andExpect(MockMvcResultMatchers.status().isBadRequest)
}

You can then retrieve the email address from the JWT token by doing this in your controller:

@PostMapping("admin")
@PreAuthorize("hasAuthority('SCOPE_email')")
fun addRepo(
@RequestBody addAdminRequest: AddAdmin,
@AuthenticationPrincipal jwt: Jwt
) {
val email = validateEmail(jwt)
adminService.addAdmin(addAdminRequest)
logger.info("${addRepoRequest.repoName} added by $email")
}

Okta Javascript Library

Hopefully, Okta’s authentication javascript library is stable now, but be aware that the API can change a lot between versions. The blogs also can be using older versions of the API. You can find the latest here:

My project was done using the 5.0.x version of the API and it has gone to version 6.0 within the past 6 months, so that gives you an idea of how rapidly it changes.

Using it is fairly simple. Your Router has to be wrapped for it to handle authentication checking routes and you have to add a route to handle the login callback. Routes that require an authenticated user has to use the SecureRoute tag:

const oktaAuthConfig = new OktaAuth(config.oidc);

const App = () => {
const restoreOriginalUri = async (_oktaAuth, originalUri) => {
window.location.href = toRelativeUrl(originalUri, window.location.origin)
}
;

return (
<
Router>
<
Security oktaAuth={oktaAuthConfig} restoreOriginalUri={restoreOriginalUri}>
<
Container text style={{marginTop: 'none'}}>
...
<
Switch>
<
Route path={config.oidc.callbackPath} component={LoginCallback}/>
<
Route
path='/'
exact
render={props =>
<RepoList {...props} mineOnly={false}/>}
/>
<
SecureRoute
path='/web/edit/:id'
render={props =>
<Edit {...props} />}
/>
</
Switch>
/>

You can then have a navigation bar component wrapped with withOktaAuth that checks for a logged in user and put up Login/Logout buttons as appropriate:

export default withOktaAuth(class NavigationBar extends Component {

constructor(props) {
super(props);
this.state = {isOpen: false};
this.toggle = this.toggle.bind(this);
this.login = this.login.bind(this);
this.logout = this.logout.bind(this);
}

async login() {
await this.props.oktaAuth.signInWithRedirect();
}

async logout() {
await this.props.oktaAuth.signOut();
}

toggle() {
this.setState({
isOpen: !this.state.isOpen
});
}

render() {
if ( this.props.authState.isPending ) {
return (
<
div>Loading authentication...</div>
)
;
} else
return <Navbar color="light" light expand="md">
<
NavbarBrand>App Directory</NavbarBrand>
<
NavbarToggler onClick={this.toggle}/>
<
Collapse isOpen={this.state.isOpen} navbar>
<
Nav className="ml-auto" navbar>

{
!this.props.authState.isAuthenticated ?
<NavItem>
<
Button color="secondary" outline onClick={this.login}>Login</Button>
</
NavItem> :
<NavItem>
<
Button color="secondary" outline onClick={this.logout}>Logout</Button>
</
NavItem>
}

</
Nav>
</
Collapse>
</
Navbar>;
}
})
;

Packaging React.js Web App with Spring Boot JAR

Typically, a microservice is packaged up as a single JAR file that is run in a container via a java command line. However, this isn’t as simple as it sounds because the files are not served from a static directory, but as resources in the JAR file.

This is done using the maven resources plugin via a pom.xml config:

<plugin>
<
artifactId>maven-resources-plugin</artifactId>
<
version>3.2.0</version>
<
executions>
<
execution>
<
id>position-react-build</id>
<
goals>
<
goal>copy-resources</goal>
</
goals>
<
phase>prepare-package</phase>
<
configuration>
<
outputDirectory>${project.build.outputDirectory}/static</outputDirectory>
<
resources>
<
resource>
<
directory>${frontend-src-dir}/../build</directory>
<
filtering>false</filtering>
</
resource>
</
resources>
</
configuration>
</
execution>
</
executions>
</
plugin>

The front-end-src dir is defined at the top of the pom.xml file like so if you created your React.js app in the webclient directory:

<frontend-src-dir>webclient/src</frontend-src-dir>

The pulls all the files in webclient/build directory into your microservice’s JAR file.

Yarn Bundling and Private Artifactory

The webclient is built using the frontend-maven-plugin which is configured like this:

<plugin>
<
groupId>com.github.eirslett</groupId>
<
artifactId>frontend-maven-plugin</artifactId>
<
version>${frontend-maven-plugin.version}</version>
<configuration>
<
nodeVersion>${node.version}</nodeVersion>
<
yarnVersion>${yarn.version}</yarnVersion>
<
workingDirectory>${frontend-src-dir}</workingDirectory>
<
installDirectory>${project.build.directory}</installDirectory>
</
configuration>
<executions>
<
execution>
<
id>install-frontend-tools</id>
<
goals>
<
goal>install-node-and-yarn</goal>
</
goals>
</
execution>
<execution>
<
id>yarn-rpm-registry</id>
<
goals>
<
goal>yarn</goal>
</
goals>
<
configuration>
<
arguments>config set registry ${npm.registry}</arguments>
</
configuration>
</
execution>
<execution>
<
id>yarn-install</id>
<
goals>
<
goal>yarn</goal>
</
goals>
<
configuration>
<
arguments>install</arguments>
<
yarnInheritsProxyConfigFromMaven>false</yarnInheritsProxyConfigFromMaven>
</
configuration>
</
execution>
<execution>
<
id>build-frontend</id>
<
goals>
<
goal>yarn</goal>
</
goals>
<
phase>prepare-package</phase>
<
configuration>
<
arguments>build</arguments>
<
yarnInheritsProxyConfigFromMaven>false</yarnInheritsProxyConfigFromMaven>
</
configuration>
</
execution>
</
executions>
</
plugin>

We mirror our NPM artifacts as well as Node.js versions in our artifactory and that’s what the “config set registry” takes care of.

Also be sure to set this on your local machine because Yarn’s package.json includes hardcoded URLs to the NPM artifacts. If you don’t reference them from your artifactory, they’ll use the public npmjs artifactory which may not be accessible from your CI pipeline.

Conclusion

I hope that gives you enough tips to get around speed bumps you might encounter getting JWT logins to work with Spring Boot and an SPA app. I sure wish I had seen these tips while working on my project :-)

p.s. this was the first production Kotlin microservice at our company and I’m happy to report I didn’t encounter any issues writing it in Kotlin. Big thanks to Sébastien Deleuze’s hard work for helping make it so seamless.

References

--

--

Ken Yee

Mobile (Native Android), Backend (Spring Boot, Quarkus), Devops (Jenkins, Docker, K8s) software engineer. Currently Kotlin All The Things!