In this tutorial, you'll learn how to configure and use Google OAuth authentication in a Hilla project that uses Spring Boot and React.
This tutorial is based on Hilla 2.0 with React.
Source code
You can find the completed source code on GitHub: https://github.com/marcushellberg/hilla-google-auth/
Requirements
30 minutes
Java 17 or newer
Node 18 or newer
Create a new Hilla project with a React frontend
Generate a new Hilla project with:
npx @hilla/cli init --react --empty hilla-google-auth
Set up a Google OAuth 2.0 Client ID
To create a client ID, you will first need to create a Google Cloud project, and add an OAuth consent screen to that project. To do this, go to the OAuth consent screen on the Google Cloud Platform dashboard. There you can create a new project and create its OAuth consent screen (select “External” when prompted for the “User Type” of the consent screen).
Once the project with a consent screen is created, go to the credentials page and do the following:
Select “Create credentials”, followed by “OAuth client ID”.
When prompted for the application type, select “Web application”.
In the “Authorized redirect URIs” field, add a new redirect for localhost:8080/login/oauth2/code/google.
Once the OAuth client is created, take note of the generated client ID and client secret.
Note: Other OAuth OIDC providers
You can also use other OAuth social login providers like GitHub, Facebook, and Twitter using these same instructions, replacing "google" in the Spring Security configuration and login paths. You can also configure multiple providers.
Add Spring Security dependencies
Begin by adding the following dependencies to the project’s pom.xml file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Configure Spring Security
Next, configure and enable Spring Security. Start by creating a new package com.example.application.security
and within it a new file SecurityConfiguration.java
with the following content:
package com.example.application.security;
import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends VaadinWebSecurity {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.oauth2Login()
.loginPage("/login").permitAll().and()
.logout().logoutSuccessUrl("/").permitAll();
}
}
Then, add the client id and secret you created in the first step to a local application.properties
file. It is important that you don’t commit secrets to Git.
In the root of the project, create a new subdirectory config/local
and add it to the .gitignore
file.
mkdir -p config/local
touch config/local/application.properties
echo "
# Contains local config that shouldn't go into the repository
config/local/" >> .gitignore
Add the following two properties to the newly created application.properties
file:
spring.security.oauth2.client.registration.google.client-id=<your client id>
spring.security.oauth2.client.registration.google.client-secret=<your client secret>
Create a login page and a view for logged-in users
Now that you have Spring Security configured, you can create a login page. The actual login is handled by Google, so all you need is a page that links to the correct URL.
In the frontend/views folder, create a new file LoginView.tsx
with the following content:
export function LoginView() {
return (
<div className="m-m">
<a href="/oauth2/authorization/google">Log in with Google</a>
</div>
)
}
Next, create a view for logged-in users. In the frontend/views
folder, create a new file MainView.tsx
with the following content:
export function MainView() {
return (
<div className="m-m">
<p>You are logged in</p>
</div>
)
}
Finally, update the routing configuration in routes.tsx
. Replace the contents of the file with the following:
import { createBrowserRouter } from 'react-router-dom';
import {LoginView} from "Frontend/views/LoginView";
import {MainView} from "Frontend/views/MainView";
const router = createBrowserRouter([
{ path: '/login', element: <LoginView /> },
{ path: '', element: <MainView/>}
]);
export default router;
✅ Checkpoint: run the application and verify it works
Run the application and verify that you are able to log in.
Start the application by running
Application.java
in your IDE, or with the included Maven wrapper:./mvnw
Spring Security should redirect you to
/login
when the app starts.Once you authenticate using Google, you should see
MainView
.
Create an endpoint and configure access
Create a server endpoint for fetching information about the logged-in user. In the src/main/java/com/example/application/endpoints
folder, create a UserDetails.java
record to hold information about the user.
package com.example.application.endpoints;
public record UserDetails(
String email,
String name,
String profilePictureUrl
) {}
In the same package, create a Hilla endpoint, UserEndpoint.java
, with the following content:
package com.example.application.endpoints;
import com.vaadin.flow.spring.security.AuthenticationContext;
import dev.hilla.Endpoint;
import jakarta.annotation.security.PermitAll;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import java.util.Optional;
@Endpoint
@PermitAll
public class UserEndpoint {
private final AuthenticationContext authContext;
public UserEndpoint(AuthenticationContext authContext) {
this.authContext = authContext;
}
public Optional<UserDetails> getAuthenticatedUser() {
return authContext.getAuthenticatedUser(OidcUser.class)
.map(u -> new UserDetails(
u.getEmail(),
u.getFullName(),
u.getPicture()
));
}
}
Here are the most important parts of the endpoint code:
@Endpoint
makes public methods in the class callable from the client through TypeScript methods. It generates corresponding TypeScript types to method parameters and return values.@PermitAll
allows all authenticated users to access public methods on the endpoint.The constructor injects
AuthenticationContext
to access user details.getAuthenticatedUser()
gets the authenticatedOidcUser
and copies over only the needed info into aUserDetails
record.
Access the authentication state from React components
Create a custom React hook and provider for fetching the user details and making them available to components.
In the frontend folder, create a new file useAuth.tsx
with the following content:
import {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from 'react';
import { UserEndpoint } from 'Frontend/generated/endpoints.js';
import { logout as serverLogout } from '@hilla/frontend';
import UserDetails from 'Frontend/generated/com/example/application/endpoints/UserDetails.js';
export function authHook() {
const [authenticated, setAuthenticated] = useState(false);
const [user, setUser] = useState<UserDetails>();
const [authInitialized, setAuthInitialized] = useState(false);
async function login() {
try {
const authUser = await UserEndpoint.getAuthenticatedUser();
setUser(authUser);
setAuthenticated(!!authUser);
} finally {
setAuthInitialized(true);
}
}
async function logout(redirect: string = '/login') {
setAuthenticated(false);
setUser(undefined);
setAuthInitialized(false);
await serverLogout();
location.href = `${location.origin}${redirect}`;
}
useEffect(() => {
login();
}, []);
return {
authenticated,
authInitialized,
user,
login,
logout,
};
}
type AuthContextType = ReturnType<typeof authHook>;
const initialValue: AuthContextType = {
authenticated: false,
user: undefined,
authInitialized: false,
login: async () => {},
logout: async () => {},
};
const AuthContext = createContext<AuthContextType>(initialValue);
interface AuthProviderProps {
children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps) {
const auth = authHook();
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
}
export function useAuth() {
return useContext(AuthContext);
}
Here are the most important parts:
The
authHook
function contains the logic needed for checking the authentication state, accessing user details, and logging out.AuthContext
andAuthProvider
allow you to have a single, shared, authentication state accessible from any component.useAuth
is the hook that gives you access to the auth state and login/logout functionality in your components.
Restrict access to views
Create a component for protecting routes, ProtectedRoute.tsx
:
import { ReactElement, ReactNode } from 'react';
import { useAuth } from 'Frontend/useAuth.js';
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { REDIRECT_PATH_KEY } from 'Frontend/routes.js';
interface ProtectedRouteProps {
redirectPath?: string;
component: ReactElement;
}
export function ProtectedRoute({
redirectPath = '/login',
component,
}: ProtectedRouteProps): ReactElement {
const { authenticated, authInitialized } = useAuth();
const location = useLocation();
if (!authInitialized) {
return <div></div>;
}
if (!authenticated) {
// Store the requested path, so we can redirect to it after logging in (in App.tsx)
localStorage.setItem(REDIRECT_PATH_KEY, location.pathname);
return <Navigate to={redirectPath} replace />;
}
return component ? component : <Outlet />;
}
The component:
Uses the
useAuth()
hook to access information on the authenticationDisplays an empty
<div>
if auth status is not yet availableIf the user is not authenticated, it stores the current path to local storage and redirects to the login page.
If the user is authenticated, it shows the passed-in component or a router outlet
Update routes.tsx
to use the new functionality. Replace the contents with the following:
import { createBrowserRouter } from 'react-router-dom';
import { LoginView } from 'Frontend/views/LoginView';
import { MainView } from 'Frontend/views/MainView';
import { ProtectedRoute } from 'Frontend/ProtectedRoute.js';
import { SecretView } from 'Frontend/views/SecretView.js';
export const REDIRECT_PATH_KEY = 'redirectPath';
const router = createBrowserRouter([
{ path: '/login', element: <LoginView /> },
{ path: '', element: <ProtectedRoute component={<MainView />} /> },
]);
export default router;
Finally, update App.tsx
with the following content:
import { RouterProvider, useNavigate } from 'react-router-dom';
import router, { REDIRECT_PATH_KEY } from './routes';
import { AuthProvider } from 'Frontend/useAuth.js';
import { useEffect } from 'react';
export function App() {
/**
* Redirects a user back to the view they attempted to access before login
*/
useEffect(() => {
const redirectPath = localStorage.getItem(REDIRECT_PATH_KEY);
if (redirectPath) {
localStorage.removeItem(REDIRECT_PATH_KEY);
location.href = `${location.origin}${redirectPath}`;
}
}, []);
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
}
The new component:
Checks if there is a redirect path stored in local storage. If there is, it navigates there.
Wraps the entire application with the
AuthProvider
to give all components access to theuseAuth()
hook.
Run the application and verify that you are able to log in.
Display user information and add a logout button
Now that you have access to user information through the useAuth()
hook, update MainView.tsx
to display user information and a logout button.
import {useAuth} from "Frontend/useAuth.js";
import {Button} from "@hilla/react-components/Button.js";
export function MainView() {
const {user, logout} = useAuth();
return (
<div className="m-m">
{user && <div>
<p>You are logged in as {user.name} ({user.email})</p>
<img src={user.profilePictureUrl} alt={user.name} referrerPolicy="no-referrer"/>
</div>}
<p>
<Button onClick={() => logout()}>Log out</Button>
</p>
</div>
)
}
You should now see information about the logged-in user and be able to log out.
A note on logout
In this example, logging out invalidates the session and logs the user out from only this application. When using a social login like Google, it is not expected behavior to log the user out of all Google services, like Gmail, when logging out.
In some cases, you may want to log the user out of the SSO provider. In that case, you need to perform an RP-initiated logout. Read more about handling Open ID Connect logouts in this blog post from Okta.
Completed application source code
You can find the source code for the completed application on GitHub: