Login page as SPA in Spring Authorization Server

Dawid Kałuża
5 min readFeb 29, 2024

--

In this article I’d like to describe how you can setup login page in SPA, and how you can setup Spring Authorization Server to both work well.

Introduction

Use-case I’m talking about is OAuth2 authorization code flow, where an end user needs to login to give external application access to their resources. I’m not going to go into details about OAuth2 protocol, Spring Auth Server relies on it so I assume that you already know what OAuth is. Nevertheless, to make sure we are on the same page, I will quickly remind here how authorization code flow works.

Let’s say, we want to give our web app access to user’s messages provided by message service and we are using Oauth2 and Spring Authorization Server. So in our case, the actors are:

  • User (resource owner),
  • Web app (public client),
  • Message service (resource server),
  • Spring Authorization server (authorization server).

In that case, it’s advised to get access tokens through authorization code grant type, which looks like in the image below:

Here, I want to focus on steps 4 and 5, where users interact with Authorization Server. By default, login page is static, which means that every interaction with UI ends up with reloading the whole page and this is how it works when server is responsible for views on client side. Changing those pages to SPA pages can make the user experience more friendly and that’s why I want to show how we can implement it.

Why default implementation is not enough?

Spring Authorization Server, by default, relies on session stored on the server. Hence, it’s actually a good idea to use default formLogin implementation provided by Spring Security to take care of end user authentication process. However, as already mentioned, it’s all based on static pages and making those SPA pages is not really straightforward. To understand it, let’s take a look at formLogin implementation:

The main obstacles here are responses to login request — we always receive redirections there (HTTP 3xx), which is fine for static pages, but in SPAs it’s easier to deal with data here as it gives our client apps more control over what to do with the response, how present it to the users. Redirections is a decision itself, already made by the server, and browsers often take those responses and instantly redirect users to indicated page, leaving no control to web app there.

So, for SPA pages, we’d expect to have something like this:

So let’s do this!

Customizing formLogin.

We still are going to use formLogin, since it’s already well protected against any exploits. Let’s start our implementation from How-to: Authenticate using a Single Page Application with PKCE :: Spring Authorization Server. There, we define two security chains: first for authorization server, second for the rest. But I’d like to add another chain between them, for login/logout endpoints and, of course, formLogin customizations:

@Bean
@Order(2)
SecurityFilterChain authenticationSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/login", "/logout") //limit filter chain to those endpoints only
.cors(Customizer.withDefaults()) //apply default cors policy
.csrf((csrf) -> csrf.disable()) //disable csrf to just keep it simple, not safe for prod though
.formLogin(form -> form
.loginPage("http://localhost:9090/login") //url to login page
.loginProcessingUrl("/login") //endpoint where POST login requests will be sent
.successHandler((req, res, auth) -> { //actions when authentication succeeds
res.resetBuffer();
res.setStatus(HttpStatus.OK.value());
res.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

// fetch saved url (visited before being redirected to login page) and return it in response body.
var savedReq = new HttpSessionRequestCache().getRequest(req, res);
res.getWriter()
.append("{\"redirectUrl\": \"")
.append(savedReq == null ? "" : savedReq.getRedirectUrl())
.append("\"}");
res.flushBuffer();
})
.failureHandler( //and when it fails
(req, res, ex) -> res.setStatus(HttpStatus.UNAUTHORIZED.value())
)
)
.logout(logout -> logout
.logoutSuccessUrl("http://localhost:9090/login?logout") //target page after logout
)
// actions when any exception arises
.exceptionHandling(handler -> handler
.authenticationEntryPoint(
new HttpStatusEntryPoint(HttpStatus.FORBIDDEN)
)
)
// secure other potential endpoints
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
);

return http.build();
}

TIP: How to implement CSRF for SPA apps, if you are interested: here.

Hope that the snippet with its comments explains what implementation introduces. Now, authentication can be easily performed in your SPA.

Consuming endpoint on client-side.

My client interface is created by React, http requests are sent by axios. Basing on that, that’s how consuming login endpoint looks like in my case.

axiosInstance
.post(
"http://localhost:8080/login",
`username=${username}&password=${password}`, //body with credentials in given content type format
{
withCredentials: true, //allows sending session cookie
headers: {
"Accept": "application/json",
'Content-Type': 'application/x-www-form-urlencoded',
}
}
)
.then(response => {
switch (response.status) {
case 200: { //when authentication succeeds
setSuccess(true);
setError(false);

const redirectUrl = response.data?.redirectUrl;
if (redirectUrl) {
window.location = redirectUrl;
}
break;
}

default: { //unexpected error
setSuccess(false);
setError(true);
}
}
})
.catch(() => { //when authentication fails
setSuccess(false);
setError(true);
});

How it works? Basically, when authentication succeeds, we inform our UI about it, which then shows information describing user that everything went fine — if it fails, we inform UI about it too, the message will just be different. Besides that, in case of success, web app decides to redirect user to the page they visited before being redirected to /login page, which in this case is authorization endpoint.

Summary

And this is it. The whole implementation of both client and server can be found here: dawidkaluza/spa-in-spring-auth-server

In this article, I only described how to make login page in SPA, however, there is also consent page, but I skipped it on purpose because frist: KISS, second: it’s more tricky there, which is actually very well explained here: Stateless OAuth2 Social Logins with Spring Boot.

Nevertheless, I still hope that you will find this tutorial helpful. In case of any questions, feel free to comment — we will plan accordingly 😁

--

--