Last week, we added OAuth with Google for client-side Authentication. We managed to get the frontend application authenticated with Google and retrieving the ticket containing the information about the logged-in user. Previously, we have already done the same on the backend-side. However, simply sending the ticket to the backend will not be enough, as we do not know whether the application can trust the token. Also, Spring does not offer this verification out-of-the-box, if the token is retrieved by a third party (in this case, the frontend application).
In this article, we will make changes to be able to consumer the token from the client, so that a separate login is not required. We will dedicate a new shared endpoint for registration and login, where the new token is verified.
Technically, this is not really required. The backend application is essentially stateless, at least as far as users are concerned. So any token will need verification upon every single request. However, the endpoint serves multiple purposes. For one, any new registration will immediately create a user. Secondly, the frontend application could be notified immediately if something is wrong server-side.
OAuth token verification
As stated, the token that is received on the server must be verified for its authenticity. JWT tokens can fairly simply be modified, so the data from them should never be trusted, unless the signature is confirmed as correct!
For this, we need to make multiple changes in our application. First, we need to add the call to Google for the verification. Fortunately, there is a library provided by Google for precisely that purpose. We can import this in our pom:
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-oauth2</artifactId>
<version>v2-rev20200213-1.32.1</version>
</dependency>
This jar will deliver us the functionality to verify the token with very little additions. We do require the client-id that is
given upon the application registration for Google OAuth. So, let’s construct that bean in a SecurityConfig
:
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.JsonFactory
import com.google.api.client.json.gson.GsonFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class SecurityConfig(@Value("\${spring.security.oauth2.client.registration.google.client-id}") private val clientId: String) {
@Bean
fun googleTokenVerifier(): GoogleIdTokenVerifier {
val transport = NetHttpTransport()
val jsonFactory: JsonFactory = GsonFactory()
return GoogleIdTokenVerifier.Builder(
transport,
jsonFactory
)
.setAudience(listOf(clientId))
.build()
}
}
If we were to have multiple clients that were to call us, we could append the other IDs in the audience
, and the application
would already be able to consume that.
As mentioned, the first place we’d like to integrate the token verifier would be in a dedicated endpoint. We can do this in
our UserController
with the /users/login
endpoint:
@RestController
@RequestMapping("/users")
@CrossOrigin
class UserController @Inject constructor(
private val userService: UserService,
private val googleIdTokenVerifier: GoogleIdTokenVerifier
) {
...
@GetMapping("/login")
fun getLoginInfo(
@RequestHeader(value = "Authorization") idTokenString: String): ResponseEntity<Nothing> {
val idToken: GoogleIdToken = googleIdTokenVerifier.verify(idTokenString.substring(7))
userService.loginGoogleUser(idToken)
return ResponseEntity.noContent().build()
}
}
The reason that the verified token is only starting at substring 7 is to remove the Bearer
from the verification.
The Controller would call our UserService
where we could check some values and determine that everything is working as expected:
fun loginGoogleUser(idToken: GoogleIdToken) {
val payload: GoogleIdToken.Payload = idToken.payload
val userId: String = payload.subject
// Get profile information from payload
val email: String = payload.email
val emailVerified: Boolean = payload.emailVerified
val name = payload["name"]
val pictureUrl = payload["picture"]
val locale = payload["locale"]
val familyName = payload["family_name"]
val givenName = payload["given_name"]
}
This is just to verify some information that we retrieve from Google. Later on, we will only take those values which are of interest to us.
Since our frontend is not yet calling the backend with the token, we need to add this in the React application. If you already have a way of getting your token, you may skip this part.
So, we add the AuthService
:
import {AxiosResponse} from "axios";
import axiosInstance from "../shared/axios";
class AuthService {
static loginUser = async (token: any) => {
return await axiosInstance.get("/users/login", {
headers: {
"Authorization": "Bearer " + token.id_token
}
})
.then((response: AxiosResponse<Array<any>>) => {
return response.data
})
.catch(() => {
alert("Unable to finalize the login.");
return null;
});
}
}
export default AuthService;
which we then call (if you read the React OAuth article, you may have seen that the AuthService call was commented out)
function GoogleLoginHook() {
const onSuccess = (res: any) => {
console.log('Login Success: currentUser:', res.profileObj);
console.log("Now registering with backend...")
AuthService.loginUser(res.tokenObj);
refreshTokenSetup(res);
};
...
}
Now, if we run the applications and make a renewed login from the frontend, we would see the following:
Token caching
Okay, that seems to work nicely! We now have a way of checking the token in general. Before we add it in other endpoints though, we need to check something. In fact, we do not want to call Google upon every single request for every single user to verify the token. This would not be scalable, and while Google will definitely have the capacity to handle those requests, they will throttle the requests of any application, just to be safe.
So, we need to make sure that we do not make too many calls. Let’s add -Djavax.net.debug=all
as an environment variable
for now, and see what happens if we make multiple requests with the same token to the backend.
Okay, that gives us quite some output. Way too much, one might say. Let’s dial the logs down just a bit. So, first, let’s
remove that environment variable, and instead add the following in our application.yaml
:
logging:
level:
root: DEBUG
If we run the application again, we will of course see another huge amount of logs. It’s easier to navigate in them already though. So, after running the login attempt once more, we will see that a request has been made to Google to verify the token:
This was of course expected. Now though, the bigger question: will it request a verification every time, or is it smart enough to recognise the same token by using some internal caching?
Since the logs are still bloated, we will change the yaml specs to the following first:
logging:
level:
com.google.api.client: DEBUG
If we now run the application again, obviously, we’ll have a connection on the first try. But, as we can see, if we make another request with the same token, no new verification is request! This is wonderful, as it means that our verifier will cache the data from the token, so that we now still have our authenticated user!
If you feel like playing around to see what happens when the token refreshes, you can divide the time again by 1000 in the UI, so that a refreshed token is sent to the backend. I’ve done this experiment for you already - it does not make a new request!
Even with another user however, it does not seem to make a new request. So, clearly, there is some internal magic going on
in the verifier, as the new data is absolutely retrieved from the ticket. In fact, the GoogleIdTokenVerifier
caches the
public keys, their expiration periods, etc. in-memory, and only retrieves the new ones when it’s necessary. This way, it can
handle all incoming requests easily without overloading the Google servers!
Now, for the next step, feel free to remove the additional logs again. :)
OAuth Filter
Okay, after now having the endpoint where we will later on store the user, we will now need to make sure that the authentication is also working on any requests where we need the user context. In order to do so, we will add a filter into the WebSecurityConfigurerAdapter.
The filter itself will first check whether the Authentication
header is present, try to convert it to a Google Id token (it
should not fail if it’s not possible, as a different authentication option may be chosen), and if successful, then populate
Spring’s SecurityContextHolder
. The SecurityContextHolder
is extremely practical, as it essentially allows us to do operations
on the current user. We don’t need to have the user’s ID in the REST path, since we have that information from the context.
This is a lot safer, as it’s much harder to provide a fake OAuth token that passes all validations than to simply get some
user’s ID. There’s a lot more that can be done in the SecurityContextHolder
, such as granting authorities to users,
protecting endpoints with them, etc. Let’s start simply by putting the user in the holder though.
Next, the filter can be added in the selection of filters in the method we’re already overriding:
override fun configure(http: HttpSecurity) {
http.addFilterBefore(oAuthGoogleSecurityFilter, BasicAuthenticationFilter::class.java)
...
}
With all the other changes we’ve done now, this function can be cleaned up some, and now simply look as follows:
override fun configure(http: HttpSecurity) {
http.addFilterBefore(oAuthGoogleSecurityFilter, BasicAuthenticationFilter::class.java)
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
}
Storing the user
The final step is to add the storage of the user in the DB. This only needs to happen if he does not yet exist, so we can
slightly change the functionality that we’ve made earlier for this in the OAuthAuthenticationSuccessHandler
. (Now that we
have the new flow, we can also delete that class). This means that the loginGoogleUser is changed to the following:
fun loginGoogleUser(idToken: GoogleIdToken) {
val payload: GoogleIdToken.Payload = idToken.payload
userRepository.findUserByEmail(payload.email) ?: userRepository.save(User(email = payload.email,
username = payload["name"]?.toString(),
firstName = payload["given_name"]?.toString(),
lastName = payload["family_name"]?.toString()
))
}
That’s already everything that’s required. If we store the user as such, with his main identifier, he can technically also press
the Continue with Facebook
button, once that’s implemented, as this would simply be another source that we’d trust for
the user’s authentication.
We can check that this works by putting a breakpoint in the /login
endpoint, and call the endpoint twice. On that breakpoint,
you should evaluate SecurityContextHolder.getContext().authentication
. The first try, this will simply be null. On the second
request however, you will now get your shiny, authenticated user!
Okay, this is now really everything that’s required on the backend for OAuth usage. In the next article, we’ll ensure that the UI will send the token upon every request, and also to store the token in its context. The entire login process is almost completed! Stay tuned!