Docs

Documentation versions (currently viewingVaadin 24)

Add Login

In this guide, you’ll learn how to create a login view using Hilla and React. A hands-on mini-tutorial at the end will walk you through enabling Spring Security, setting up in-memory authentication, and integrating a login form into a real Vaadin application.

Warning
Never Use Hard-Coded Credentials In Production
In-memory authentication is convenient for development, but production applications must use a more secure approach, such as JDBC authentication, LDAP authentication, or OAuth 2.0. Refer to the Spring Security Reference Manual for more details.

Client Configuration

Vaadin includes a client-side security extension that integrates with Spring Security on the server. To configure it, follow these steps:

  1. Create a user information endpoint on the server.

  2. Set up a React context in the frontend.

  3. Enable the context in the App component.

Create a User Information Endpoint

Since authentication happens on the server, you need a way to pass user information to the client. To achieve this, create a @BrowserCallable service that returns user details:

@BrowserCallable
public class UserInfoService {

    @PermitAll 1
    public @NonNull UserInfo getUserInfo() {
        var auth = SecurityContextHolder.getContext().getAuthentication(); 2
        var authorities = auth.getAuthorities()
            .stream()
            .map(GrantedAuthority::getAuthority)
            .toList();
        return new UserInfo(auth.getName(), authorities); 3
    }
}
  1. Allows all authenticated users to access getUserInfo().

  2. Retrieves user details from Spring Security.

  3. Returns user information to the client.

Vaadin does not provide a built-in user information type, so you need to define your own:

public record UserInfo(
    @NonNull String name,
    @NonNull Collection<String> authorities
) {
}

You can include additional user details (e.g., email, avatar) in this type and access them in React.

Setup the Authentication Context

Now, configure a React context to store authentication details, using Vaadin’s helper configureAuth():

import { configureAuth } from '@vaadin/hilla-react-auth';
import { UserInfoService } from "Frontend/generated/endpoints";

const auth = configureAuth(UserInfoService.getUserInfo) 1
export const useAuth = auth.useAuth 2
export const AuthProvider = auth.AuthProvider 3
  1. Uses UserInfoService to fetch user details from the server.

  2. useAuth is a React hook that provides authentication state within views.

  3. AuthProvider is a React context provider.

Enable the Authentication Context

Next, wrap the root component with <AuthProvider>. First, move index.tsx from src/main/frontend/generated to src/main/frontend. Then, modify it as follows:

import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router';
import { router } from 'Frontend/generated/routes.js';
import { AuthProvider } from "Frontend/security/auth";

function App() {
    return (
        <AuthProvider>
            <RouterProvider router={router} />
        </AuthProvider>
    );
}
...

The Login View

The login view is a standard Hilla view. The easiest way to implement one is by using the LoginForm component:

import { LoginForm } from "@vaadin/react-components";
import { ViewConfig } from "@vaadin/hilla-file-router/types.js";
import { useSearchParams } from "react-router";

export const config: ViewConfig = {
    skipLayouts: true, 1
    menu: {
        exclude: true 2
    }
}

export default function LoginView() {
    const [searchParams] = useSearchParams()
    const hasError = searchParams.has("error"); 3

    return (
        <main className="flex justify-center items-center w-full h-full">
            <LoginForm error={hasError} action="login"/> 4
        </main>
    )
}
  1. Disables auto layout to prevent the login view from being embedded in a router layout.

  2. Excludes the login view from the navigation menu.

  3. Detects if the ?error query parameter is present.

  4. Instructs the login form to send a POST request to /login for authentication.

Spring Security’s form login mechanism automatically processes authentication requests sent to /login. When authentication fails, the user is redirected back to the login page with ?error, which the login view handles.

Server Configuration

To instruct Spring Security to use your login view, modify your security configuration:

@EnableWebSecurity
@Configuration
class SecurityConfig extends VaadinWebSecurity {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        setLoginView(http, "/login");
    }
    ...
}

Now, when a user tires to access a protected view, they’ll be redirected to the login page.

Important
By default, Vaadin permits access to Hilla views and router layouts, and denies access to Flow views and router layouts. This is covered in more detail in the Protect Views guide.

Try It

In this mini-tutorial, you’ll enable security and add a login form to a real Vaadin application. This serves as a foundation for future security-related mini-tutorials.

Set Up the Project

First, generate a walking skeleton with a Hilla UI, open it in your IDE, and run it with hotswap enabled.

Note
Security configuration changes may require a manual restart for them to take effect. Hotswap may not be enough.
Add the Spring Security Dependency

Add the following Spring Security dependency to pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
Create the Security Configuration Class

Create a new package: [application package].security

Inside this package, create a SecurityConfig class:

import com.vaadin.flow.spring.security.VaadinWebSecurity;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;

@EnableWebSecurity
@Configuration
class SecurityConfig extends VaadinWebSecurity {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

    @Bean
    public UserDetailsManager userDetailsManager() {
        LoggerFactory.getLogger(SecurityConfig.class)
            .warn("Using in-memory user details manager!");
        var user = User.withUsername("user")
                .password("{noop}user")
                .roles("USER")
                .build();
        var admin = User.withUsername("admin")
                .password("{noop}admin")
                .roles("ADMIN")
                .build();
        return new InMemoryUserDetailsManager(user, admin);
    }
}
Create the User Information Endpoint

Create a new package: com.example.application.security.service

Inside this package, create a UserInfo record:

import org.jspecify.annotations.NonNull;
import java.util.Collection;

public record UserInfo(@NonNull String name,
                       @NonNull Collection<String> authorities) {
}

Then create a UserInfoService class:

import com.vaadin.hilla.BrowserCallable;
import jakarta.annotation.security.PermitAll;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

@BrowserCallable
public class UserInfoService {

    @PermitAll
    public UserInfo getUserInfo() {
        var auth = SecurityContextHolder.getContext().getAuthentication();
        var authorities = auth.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority).toList();
        return new UserInfo(auth.getName(), authorities);
    }
}
Setup the Authentication Context

Create a new directory: src/main/frontend/security

Inside this directory, create an auth.ts file:

import { configureAuth } from '@vaadin/hilla-react-auth';
import { UserInfoService } from "Frontend/generated/endpoints";

const auth = configureAuth(UserInfoService.getUserInfo)
export const useAuth = auth.useAuth
export const AuthProvider = auth.AuthProvider
Enable the Authentication Context

Locate the src/main/frontend/generated/index.tsx file and move it to src/main/frontend. Then add the AuthProvider:

import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router';
import { router } from 'Frontend/generated/routes.js';
import { AuthProvider } from "Frontend/security/auth";

function App() {
    return <AuthProvider><RouterProvider router={router} /></AuthProvider>;
}
...
Create the Login View

In the src/main/frontend/views directory, create a new file login.tsx:

import { LoginForm } from "@vaadin/react-components";
import { ViewConfig } from "@vaadin/hilla-file-router/types.js";
import { useSearchParams } from "react-router";

export const config: ViewConfig = {
    skipLayouts: true,
    menu: {
        exclude: true
    }
}

export default function LoginView() {
    const [searchParams] = useSearchParams()
    const hasError = searchParams.has("error");

    return (
        <main className="flex justify-center items-center w-full h-full">
            <LoginForm error={hasError}
                       action="login"/>
        </main>
    )
}
Update the Spring Security Configuration

Modify SecurityConfig to reference the new login view:

@EnableWebSecurity
@Configuration
class SecurityConfig extends VaadinWebSecurity {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        setLoginView(http, "/login");
    }
    ...
}
Require Authentication

By default, Vaadin grants access to all Hilla views. Require authentication by adding the following to src/main/frontend/views/@layout.tsx:

import { ViewConfig } from "@vaadin/hilla-file-router/types.js";
...
export const config: ViewConfig = {
    loginRequired: true
}

export default function MainLayout() {
    // ...
}
Test the Application

Restart your application to make sure all your changes have been applied. Navigate to: http://localhost:8080

You should now see the login screen. Login with one of the following credentials:

  • User: user / Password: password

  • Admin: admin / Password: admin

After logging in, you should be able to access the task list view.

Final Thoughts

You have now successfully added authentication to your Vaadin application. Next, learn how to log out users by reading the Add Logout guide.