# Tutorial: Set up Google OAuth in a Hilla Spring Boot + React project

In this tutorial, you'll learn how to configure and use Google OAuth authentication in a [Hilla](https://hilla.dev) 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/](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:

```bash
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**](https://console.cloud.google.com/apis/credentials/consent) 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**](https://console.cloud.google.com/apis/credentials) and do the following:

1. Select “Create credentials”, followed by “OAuth client ID”.
    
2. When prompted for the application type, select “Web application”.
    
3. In the “Authorized redirect URIs” field, add a new redirect for [**http://localhost:8080/login/oauth2/code/google**](http://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:

```xml
<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:

```java
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.

```bash
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`](http://application.properties) file:

```plaintext
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:

```typescript
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:

```typescript
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:

```typescript
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`](http://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.

```java
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:

```java
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 authenticated `OidcUser` and copies over only the needed info into a `UserDetails` 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:

```typescript
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` and `AuthProvider` 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`:

```typescript
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 authentication
    
* Displays an empty `<div>` if auth status is not yet available
    
* If 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:

```typescript
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:

```typescript
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 the `useAuth()` hook.
    

Run the application and verify that you are able to log in.

![A browser window displaying a "Log in with Google" link](https://cdn.hashnode.com/res/hashnode/image/upload/v1677095141555/704b4849-591e-46a0-88e1-a3bc81f8305d.png align="center")

## **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.

```typescript
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 browser window displaying the name, email, and picture of the logged-in user.](https://cdn.hashnode.com/res/hashnode/image/upload/v1677095181070/8e70c84e-99b5-49ea-975b-542d3844d0ea.png align="center")

## **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](https://developer.okta.com/blog/2020/03/27/spring-oidc-logout-options).

## **Completed application source code**

You can find the source code for the completed application on GitHub:

[https://github.com/marcushellberg/hilla-google-auth/](https://github.com/marcushellberg/hilla-google-auth/)
