Building a web application involves a lot of moving pieces. You have a backend server handling API calls, a frontend application running business logic, and you need to somehow make sure both are in sync and secure.
In this blog post, you’ll learn how to use Vaadin Fusion, Spring Boot, and Okta to create a full-stack web application with authentication. Specifically, you’ll learn how to:
I collaborated on this post with Marcus Hellberg, Head of Community at Vaadin. Marcus and I met years ago on the conference circuit, and we’re both from Finland. My great grandparents were Finnish and built the sauna and cabin I grew up in in Montana’s remote woods. Marcus and I share a passion for using Java and web technologies to build fast, efficient applications.
You might notice we keep saying “Vaadin Fusion” instead of “Vaadin.” I’ve always thought Vaadin was a web framework based on Google’s GWT (Google Web Toolkit). I asked Marcus about this, and he explained to me there are now two products—Vaadin Flow and Vaadin Fusion.
The classic Vaadin server-side Java API is now called Vaadin Flow. Vaadin Fusion is a new TypeScript frontend framework designed for Java backends. Both offer full-stack type safety and use the same set of UI components. Vaadin is no longer based on GWT. Instead, its components use web component standards.
Prerequisites
You can find the full completed source code for this tutorial on GitHub, in the oktadeveloper/okta-vaadin-fusion-spring-boot-example repository.
Vaadin Fusion is an open-source, front-end framework designed specifically for Java backends. Fusion gives you type-safe access to your backend from your client app by auto-generating TypeScript interfaces for your server Java objects. It wraps REST calls in async TypeScript methods, so you can access your backend as easily as calling any TypeScript function.
End-to-end type checking means you catch any breaking changes at build time, not in production. Oh, and there’s auto-complete everywhere, so you can focus on coding, not reading API docs.
Views are written in TypeScript with LitElement, a lightweight, reactive component library.
Begin by creating a new Vaadin Fusion app with the Vaadin starter wizard. It allows you to configure views, tech stack, and theme before downloading an app starter.
Rename the About view to “People” and change its URL to “people”:
Go into the application settings and change the name to Vaadin Okta
. Then, select TypeScript + HTML for the UI stack to get a Fusion project.
The two important folders in the project are:
/frontend
- This folder contains all the frontend code/src/main/java
- This folder includes all the backend code, which is a Spring Boot appStart the application with the following command:
mvn
The launcher should open up the app in your default browser. If not, navigate to http://localhost:8080.
Vaadin Fusion uses type-safe endpoints for server access. You create an endpoint by annotating a class with @Endpoint
. This will export all the methods in the class and make them callable from TypeScript. Vaadin will also generate TypeScript interfaces for any data types the methods use.
Vaadin endpoints require authentication by default. You can explicitly make an endpoint class or a single method accessible to unauthenticated users by adding an @AnonymousAllowed
annotation.
In this app, you want to restrict access to only authenticated users. You’ll use OpenID Connect (OIDC) and Okta to make this possible.
Add the Okta Spring Boot starter and Lombok dependencies to the <dependencies>
section of your pom.xml
file.
<dependency>
<groupId>com.okta.spring</groupId>
<artifactId>okta-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Only for convenience, not required for using Vaadin or Okta -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
Make sure your IDE imports the dependencies, or re-run mvn
.
Create a free Okta developer account on developer.okta.com if you don’t already have one.
Once logged in, go to Applications > Add Application and select Single-Page App.
Configure the app settings and click Done to create the app:
Store the issuer in src/main/resources/application.properties
by adding the following property:
okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
Vaadin integrates with Spring Security to handle authorization. Instead of restricting access to specific routes as you would with Spring REST controllers, you need permit all traffic to /**
so Vaadin can handle security.
Vaadin is configured to:
index.html
for the root path and any unmatched server routeBy default, all server endpoints require an authenticated user. You can allow anonymous access to an endpoint or a method by adding an @AnonymousAllowed
annotation. You can further restrict access by adding @RolesAllowed
to an endpoint or a method.
The security configuration below assumes you are only serving a Vaadin Fusion application. Suppose you are also serving Spring REST controllers or other non-Vaadin resources. In that case, you need to configure their access control separately, for instance, adding antMatchers("/api/**").authenticated()
if you serve REST APIs under /api
.
Create a new class SecurityConfiguration.java
in the same package as Application.java
with the following contents:
package com.example.application;
import com.okta.spring.boot.oauth.Okta;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
// @formatter:off
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/**/*.{js,html,css,webmanifest}");
// @formatter:on
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
// Vaadin handles CSRF for its endpoints
http.csrf().ignoringAntMatchers("/connect/**")
.and()
.authorizeRequests()
// allow access to everything, Vaadin will handle security
.antMatchers("/**").permitAll()
.and()
.oauth2ResourceServer().jwt();
// @formatter:on
Okta.configureResourceServer401ResponseBody(http);
}
}
Now that you have the server set up for authenticating requests add a service you can call from the client app.
First, create a Person.java
class to use as the data model in the com.example.application.views.people
package.
package com.example.application.views.people;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Person {
private String firstName;
private String lastName;
}
If you aren’t using Lombok, omit the annotations and add a constructor that takes in firstName
and lastName
, and create getters and setters for both.
If you’re doing this tutorial in an IDE, you may need to enable annotation processing so Lombok can generate code for you. See Lombok’s instructions { Eclipse, IntelliJ IDEA } for more information.
Open PeopleEndpoint.java
and replace the contents with the following:
package com.example.application.views.people;
import com.vaadin.flow.server.connect.Endpoint;
import java.util.ArrayList;
import java.util.List;
@Endpoint
public class PeopleEndpoint {
// We'll use a simple list to hold data
private List<Person> people = new ArrayList<>();
public PeopleEndpoint() {
// Add one person so we can see that everything works
people.add(new Person("Jane", "Doe"));
}
public List<Person> getPeople() {
return people;
}
public Person adEclipsedPerson(Person person) {
people.add(person);
return person;
}
}
Vaadin will make the getPeople()
and addPerson()
methods available as asynchronous TypeScript methods. It will also generate a TypeScript interface for Person
, so you can access the same type-information of both on the server and in the client.
Create a view that uses the server API. Open frontend/views/people/people-view.ts
and replace its code with the following:
import {
LitElement,
html,
css,
customElement,
internalProperty,
} from 'lit-element';
import Person from '../../generated/com/example/application/views/people/Person';
import '@vaadin/vaadin-text-field';
import '@vaadin/vaadin-button';
import { Binder, field } from '@vaadin/form';
import PersonModel from '../../generated/com/example/application/views/people/PersonModel';
import { addPerson, getPeople } from '../../generated/PeopleEndpoint';
@customElement('people-view')
export class PeopleView extends LitElement {
@internalProperty()
private people: Person[] = [];
@internalProperty()
private message = '';
// Manages form state, binds inputs to the model
private binder = new Binder(this, PersonModel);
render() {
const { model } = this.binder;
return html`
<h1>People</h1>
<div class="message">${this.message}</div>
<ul>
${this.people.map(
(person) => html`<li>${person.firstName} ${person.lastName}</li>`
)}
</ul>
<h2>Add new person</h2>
<div class="form">
<vaadin-text-field
label="First Name"
...=${field(model.firstName)}
></vaadin-text-field>
<vaadin-text-field
label="Last Name"
...=${field(model.lastName)}
></vaadin-text-field>
<vaadin-button @click=${this.add}>Add</vaadin-button>
</div>
`;
}
async connectedCallback() {
super.connectedCallback();
try {
this.people = await getPeople();
} catch (e) {
this.message = `Failed to get people: ${e.message}.`;
}
}
async add() {
try {
const saved = await this.binder.submitTo(addPerson);
if (saved) {
this.people = [...this.people, saved];
this.binder.clear();
}
} catch (e) {
this.message = `Failed to save: ${e.message}.`;
}
}
static styles = css`
:host {
display: block;
padding: var(--lumo-space-m) var(--lumo-space-l);
}
`;
}
Here’s what this code does:
people
and message
to hold the component’s state. Any time a property changes, the template will get re-rendered efficiently.Binder
for handling the new-person form. It keeps track of the model value, handles validations, and submits the value to the endpoint.<ul>
)vaadin-text-field
and vaadin-button
. The fields are bound to the Binder with the help of a spread operator (…=${field(…)}
). You can read more about forms in the Vaadin documentationadd()
method, which submits the form to the backend and adds the saved Person
to the people array.message
gets populated to inform the user.#spring-boot #security #programming #developer