Let’s Get Authenticated — Google Sign In

A Full Stack Solution to Google Sign In with React.js and Spring Boot

Ken Foss
Javarevisited
10 min readJun 7, 2024

--

As the web has integrated into every aspect of human existence, so have authentication and the all-too-familiar greeting of the sign-in screen. In recent years, a far more convenient sing-in method has surged in popularity for ease over storing or remembering a myriad of passwords. This would be OAuth and OpenID sign in. While OpenID sign was introduced as a simple way to get up and running with sign-in methods such as Google, OAuth provides much more functionality. OAuth will allow sharing of information across applications pending user agreement. Provided the extra functionality of OAuth, our focus will remain here.

A major obstacle to the start of any application is secure sign-on, with persistence across windows and even when browsers are closed and re-opened. This demo application uses a Spring Boot back-end, storing user information in a SQL database, to this effect. User “session” information is passed via JSON web tokens that will persist up to an hour in the client-side local storage.

Following Along

I highly recommend following along with the code, all can be found in the repository below. There will be a demo of the flow from the perspective of our React.js front-end provided, and some of the supporting code both front-end and back-end.

From the demo_backend folder in the git project below, run the following command to start the Spring Boot Application:

./mvnw clean install spring-boot:run

Then, from the demo_frontend directory, start the React.js appliction with the following command:

npm start

Note, you will need an instance of SQL running locally to connect to. If you do not have your own setup for this, I recommend referring to my post on starting a local testing instance with docker.

The Front-End

It is standard for an application to be divided into (at least) two entities. These are the front-end or client-side application, and the back-end. This post is meant to help produce an authentication server that will support this architecture. The front-end is a user facing JavaScript application (made with the React.js in our case), making it the obvious choice to direct users to the google sign-in screen.

So this is where we will start, please view the demo_frontend folder in the given git repository to follow along with this flow.

Configuring Google APIs & Services — The Consent Screen

When creating the project, we must also create a consent screen for our app. We can do this from the google development console, if you have not already you must create a new project via the highlighted button of the image below. When you navigate to the “OAuth consent screen” tab you will be met with a prompt to generate said consent screen. First, we will be opening the user type to External (open to all google users), though we will have to configure test users before the app is launched. You will be met with a page of App information. This will customize various aspects of the consent screen users will be met with. It is not important for this demo, but customize it to adjust what your users are met with.

Google App Registration — OAuth Consent screen

Google OAuth allows you to any of a users information stored with google. However, we want to avoid saturation of the response with irrelevant information. So, we will configure only the scopes we need. These are userinfo.email, openid, and userinfo.profile.

Google App Registration — Scope Configuration

Lastly, we are met with an option to specify test users. I configured a personal/test email address specifically for this purpose. I recommend you do the same.

Configuring Google APIs & Services — The Credentials

We will now be making an OAuth client ID to support the login functionality. Select the new project and Create the new client id from the “Credentials” tab of “APIs & Services”, you will be asked for an Application Type which is “Web application” in our case. Name the project as you please.

Google App Registration — Credentials

The application will request an id token from the JavaScript Origin, and receive it via the authorized redirect URI. In our configuration the “Dashboard” page will receive this token and facilitate passing it to the back-end for verification and create an authenticated session within the context of our application. So, in our case the JavaScript origin will be http://127.0.0.1:3000, and the redirect http://localhost:3000/dashboard, as you can see pictured in the next section.

Google OAuth Flow

The JavaScript Front-End

Now the fun part, no more configuration… for now. I recommend cloning the repository given in the following along section, if you have not already. Here you will notice the entry page is the Login component, which can be found in src/pages/Login.js. Here we route the user to the google consent screen, to do this we will need the client id which can be found here:

With this ID, the authorized redirect URI and scopes, we make the call to route our user to the google consent screen.

Route Users to the Google Consent Screen

you will notice there is also a response_type. There are a few options here, the id_token is a JSON Web Token (JWT) that contains claims about the authenticated user. We will use it to get google information to generate our users on the back-end.

Now if you refer back to the flow given in the credential configuration section, you will see the token is returned to the /dashboard page of this site which is found in our /src/pages/Dash.js file.

This useEffect() hook will run on page load. The id_token is actually returned to this page in the URL, you will see it parsed on lines 11–17 and stored in a JavaScript variable to later be passed to our Back-End sign-in/account creation endpoint.

Something of importance to note here is the use of the localStorage API to store the authentication JWT. This is a recommended practice from the OWASP session management cheatsheet which recommends localStorage for persisting sessions across windows or tabs. This will allow our users to login and be authenticated for the life of their JWT without needing to sign-in whenever they open our page (or re-open the browser).

There is also a isAuthenticated() endpoint that is not expanded upon. This is because this method reaches out to our back-end and passes along the DemoAuthToken to ensure it’s validity. Should this token be expired, authenticated will remain set to false and we will attempt to fetch a new token. To see exactly how it is implemented, I invite you to inspect the code yourself.

Now to the meat and potatoes, lets write some back-end code.

The Back-End

Our back-end will be configured in Spring Boot. To set up the project refer to the pom.xml in the demo_backend directory of the git repository found in the following along section. We will be receiving the google id token to the /oauth/google endpoint. From here a user will be registered to a sql table, and a JSON web token built to represent the user in the context of our application.

Creating Users in Application context

The endpoint in our spring application will be a POST endpoint, and it will facilitate creating user credentials for our users. As long as the sign-in is attempted with a valid google ID token, a user will be signed in. In our implementation, if a user has not yet been created in the application context their user will be automatically generated. These users will be generated in the “google_user” SQL table. See the com.auth.demo.jpa package of the git project “demo_backend” folder for the configuration files to use this table as a JPA repository. The google_user table will have only and “email” and “google_id” column for now, but adding columns to store user information from payment status to application settings should be a simple addition.

Past the creation of a simple GoogleUser model and GoogleUserDTO (Data Transfer Object), we will need a JpaRepository for our GoogleUser model. This allows us to query based on naming convention, and we can create a query to find the user by their google ID. This will look like so:

Off naming conventions, we can now use the findByGoogleId(String googleId) to find users matching a specific google id in the google_user table.

We will need to configure the application to connect to our SQL repository, in my case I am running it locally. Use this configuration in your application.yml, the endpoint in the url should match your Database name, in this case “GoogleOauth”.

Signing In

Users will sign in with google on the front-end, which will reach out to the /oauth2/google endpoint to register users in context of the application back-end. Here the GoogleIdTokenVerifier is used, which relies on access to the google Id we configured earlier. This will be configured in our application context with the GoogleConfig configuration property bean and the corresponding configuration in the application.yml.

With this, we can build a GoogleIdTokenVerifier and generate users in our google_user table when appropriate, or find pre-existing users. If the token is verified with the token verifier, it will return a GoogleIdToken and we can get the requested google account information from here (user id, email). With this we generate a JWT token to authenticate the user in the context of this application and send it back to the front-end to be used in future requests.

Security Configuration

The crux of our OAuth server revolves around the security filter settings. For our purposes we will disable CSRF for ease of testing, though this is not recommended in production. If you are going to implement CSRF with this setup I invite you to read my post on CSRF with Spring Security, for a comprehensive view of the topic.

We want to ensure accessibility to the login endpoint before a user is authenticated. To do so we can use the permitAll() method on our request matcher on the endpoint. We will be requiring authentication for all other endpoints.

Note the jwtAuthenticationFilter, this filter will set the authentication status so that a user can clear the .authenticated() endpoints based on the validity of an authorization header they will pass with their request. This header consists of the JWT created on login to persist the user’s authentication status with the application.

JSON Web Token Generation and Filter

The gears that make the JWT based authentication system of this application run can be founr din the JwtGenrationService class. All authentication starts with a login request generating a token with the generateToken(String googleId) method. Here we will sign the token with a secret key that should not be stored publicly in production, a likely solution to this is use of an environment variable. With this key someone can decode the token and access whatever data is contained within, even generate their own tokens meant to infiltrate the application. This key can also authenticate the user across multiple micro-services. To achieve this, implement the same token filter in each micro-service and verify that incoming tokens are signed with the shared key. We will simply store the google id on these tokens to be used in identifying which user is signed in. The JWT_EXPIRY will set a timeout for this token, such that after this period it will no longer register as valid (in milliseconds).

After this we need a method to get the google id from a JWT, this will first verify the JWT is valid both in expiry and signed with the secret key. The google id is stored in the googleId claim. We will also provide a method to validate a token in any context.

NOTE if an application uses google OAuth in a similar manner to this and gets a users permission in the google OAuth consent screen, they will have access to that users google id. Without the JWT secret key used to sign these tokens they will still be denied access to this application.

The JWT filter gets the token from the request header “Authorization”, and validates the token with the validateToken(String token) method in the generator service. Should the token be valid it will get the username and create a UsernamePasswordAuthenticationToken that will be attached to the authentication context. This allows the request to clear endpoints configured with authenticated() in the security configuration. Furthermore, we will see in the next section that this authentication context can be used to get the users information (google ID) later in the application flow.

Getting User Details Throughout the Application

We will be using the /hello endpoint to test authentication status and getting user information when a user is properly signed in. Recall the authentication token is set to the users authentication status. This can later be accessed by getting the authentication context with the SecurityContextHolder. The UserDetails are then ascertained, and we can get the “username” from here. Note we made our google id the username here.

We also need a way to simply return a boolean authentication status within the application. This is used to determine if the user already has a valid stored authentication JWT on the front-end before attempting a fresh sign in (and thusly limit load on the back-end authentication service). The user will not be able to access endpoints with the .authenticated() property assigned in the security configuration in this application without clearing the custom JWT filter. So if a user reaches this method at all we know they are authenticated.

Tying it Together — Demo

Phew, that was a mouthful. All that work for a quick couple seconds of user experience on the client-side. But with this method you can safely authenticate your users and start storing their information! Let’s walk through the login from the user perspective to close this all out…

Recorded with Loom

--

--