Authentication with Spring Security
Authentication may be configured to use Spring Security. Since the downloaded application is a Spring Boot project, the easiest way to enable authentication is by adding Spring Security.
Dependencies
Add the following dependency to the project Maven file;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
After doing this, the application is protected with a default Spring login view, by adding the Spring Security dependency. By default, it has a single user — with username, 'user' — and a random password. When you add logging.level.org.springframework.security = DEBUG
to the application.properties
file, the username and password are shown in the console when the application starts up.
Server Configuration
To implement your own security configuration, create a new configuration class that extends the VaadinWebSecurity
class, and annotate it to enable security.
VaadinWebSecurity
is a helper which provides default bean implementations for SecurityFilterChain and WebSecurityCustomizer. It takes care of the basic configuration for requests, so that you can concentrate on your application-specific configuration.
@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurity {
@Override
protected void configure(HttpSecurity http) throws Exception {
// Set default security policy that permits Hilla internal requests and
// denies all other
super.configure(http);
// use a form based login
http.formLogin(Customizer.withDefaults());
}
@Bean
public UserDetailsManager userDetailsService() {
// Configure users and roles in memory
return new InMemoryUserDetailsManager(
// the {noop} prefix tells Spring that the password is not encoded
User.withUsername("user").password("{noop}user").roles("USER").build(),
User.withUsername("admin").password("{noop}admin").roles("ADMIN", "USER").build()
);
}
}
Warning
|
Never use hard-coded credentials.
You should never use hard-coded credentials in a real application.
The Security documentation has examples of setting up LDAP or SQL-based user management.
|
Adding Public Views & Resources
Public views need to be added to the configuration before calling super.configure()
. Here’s an example of this:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(registry -> {
registry.requestMatchers(new AntPathRequestMatcher("/public-view")).permitAll(); // custom matcher
});
super.configure(http);
}
Public resources can be added by overriding configure(WebSecurity web)
like so:
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring().requestMatchers(new AntPathRequestMatcher("/images/**"));
}
Implement Client-Side Security
The implementation of client-side security in React is greatly simplified by the react-auth
package. It can use any @BrowserCallable
service which provides user authentication information, like the one in this example:
@BrowserCallable
public class UserInfoService {
@PermitAll
@Nonnull
public UserInfo getUserInfo() {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
final List<String> authorities = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return new UserInfo(auth.getName(), authorities);
}
}
You can instruct Hilla to use this service as the source for authentication information. The service is expected to return no user if not authenticated, or an object providing information about the current user.
The easiest way to configure authentication to use this service is to create an auth.ts
file:
import { configureAuth } from '@hilla/react-auth';
import { UserInfoService } from 'Frontend/generated/endpoints';
// Configure auth to use `UserInfoService.getUserInfo`
const auth = configureAuth(UserInfoService.getUserInfo);
// Export auth provider and useAuth hook, which are automatically
// typed to the result of `UserInfoService.getUserInfo`
export const useAuth = auth.useAuth;
export const AuthProvider = auth.AuthProvider;
The exported AuthProvider
must wrap the root component of your application to be able to apply security to it:
frontend/App.tsx
export default function App() {
return (
<AuthProvider>
<RouterProvider router={router} />
</AuthProvider>
);
}
The useAuth
hook provides four items:
-
state
, which contains the authentication state that allows access to the user object and other information like authentication status (i.e., loading, errors); -
login
andlogout
, which are functions used to perform the corresponding actions; and -
hasAccess
, another function which is used to verify if the current user has access to a path.
Create a Login View
Use the <LoginOverlay>
component to create the following login view, so that the autocomplete and password features of the browser are used:
export default function LoginView() {
const { state, login } = useAuth();
const [hasError, setError] = useState<boolean>();
const [url, setUrl] = useState<string>();
if (state.user && url) {
const path = new URL(url, document.baseURI).pathname;
return <Navigate to={path} replace />;
}
return (
<LoginOverlay
opened
error={hasError}
noForgotPassword
onLogin={async ({ detail: { username, password } }) => {
const { defaultUrl, error, redirectUrl } = await login(username, password);
if (error) {
setError(true);
} else {
setUrl(redirectUrl ?? defaultUrl ?? '/');
}
}}
/>
);
}
Protecting a Hilla View
After the login view is defined, you should define a route for it in the routes.tsx
file. You can wrap the routes definition with the protectRoutes
function that will filter out views having authentication requirements not fulfilled by the current authentication state.
export const routes: RouteObject[] = protectRoutes([
{
element: <MainLayout />,
handle: { title: 'Main' },
children: [
{ path: '/', element: <AboutView />, handle: { title: 'About', requiresLogin: true } },
],
},
{ path: '/login', element: <LoginView /> },
]);
export default createBrowserRouter(routes);
Role-based Protection
Hilla supports role-based security out-of-the-box, provided that the user object returned by your service has a roles
property which returns an array or collection of strings. In that case, you can configure a route to allow access to one or more specific roles:
{
path: '/',
element: <AdminView />,
handle: { title: 'Administration Page', rolesAllowed: ['ADMIN'] },
}
If roles are exposed differently in your user object, you can still tell Hilla how to find them by amending your authentication configuration on the client. In the UserInfo
example provided before, the roles are returned by getAuthorities
, so the auth.ts
file should be modified as such:
// Configure auth to use `UserInfoService.getUserInfo` and map to custom roles
const auth = configureAuth(UserInfoService.getUserInfo, {
getRoles: (userInfo) => userInfo.authorities,
});
The getRoles
function is still expected to return an array of strings. As a result, you might need to map
your roles to strings.
Configuration Helper Alternatives
VaadinWebSecurity.configure(http)
configures HTTP security to bypass framework internal resources. If you prefer to roll your own configuration, instead of using the helper, the matcher for these resources can be retrieved with VaadinWebSecurity.getDefaultHttpSecurityPermitMatcher()
.
For example, VaadinWebSecurity.configure(http)
requires all requests to be authenticated, except the Hilla internal ones. If you want to allow public access to certain views, you can configure it as follows:
public static void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.requestMatchers(
VaadinWebSecurity.getDefaultHttpSecurityPermitMatcher()
).permitAll()
.requestMatchers("/public-view").permitAll() // custom matcher
.anyRequest().authenticated();
...
}
Similarly, the matcher for static resources to be ignored is available as VaadinWebSecurity.getDefaultWebSecurityIgnoreMatcher()
:
public static void configure(WebSecurity web) throws Exception {
web.ignoring()
.requestMatchers(
VaadinWebSecurity.getDefaultWebSecurityIgnoreMatcher())
.requestMatchers(antMatcher("static/**")) // custom matcher
...
}
Appendix: Production Data Sources
The example given here of managing users in memory is valid for test applications. However, Spring Security offers other implementations for production scenarios.
SQL Authentication
The following example demonstrates how to access a SQL database with tables for users and authorities.
@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurity {
...
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Configure users and roles in a JDBC database
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery(
"SELECT username, password, enabled FROM users WHERE username=?")
.authoritiesByUsernameQuery(
"SELECT username, authority FROM from authorities WHERE username=?")
.passwordEncoder(new BCryptPasswordEncoder());
}
}
LDAP Authentication
This next example shows how to configure authentication by using an LDAP repository:
@EnableWebSecurity
@Configuration
public class SecurityConfig extends VaadinWebSecurity {
...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// Obtain users and roles from an LDAP service
auth.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.userSearchBase("ou=people")
.groupSearchBase("ou=groups")
.contextSource()
.url("ldap://localhost:8389/dc=example,dc=com")
.and()
.passwordCompare()
.passwordAttribute("userPassword");
}
}
Remember to add the corresponding LDAP client dependency to the project:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>