Spring Boot and Flutter integration without passwords

Imagine you create a new mobile application, your users signup for a new account using just their phone PIN or fingerprint. What if your application becomes invulnerable to phishing attacks too in the process? Would you consider a simple alternative to passwords or do you still want to deal with all the passwords related issues?

Mihaita Tinta
ING Hubs Romania
13 min readOct 17, 2022

--

In this article we are going to write a Flutter mobile application consuming a Spring Boot Rest API. We are going to explore some challenges a developer might encounter when building an end to end solution running on a local machine. Our users have to signup for a new account and use their mobile devices to authenticate. Instead of passwords, we are going to use strong credentials using WebAuthn. At the end, I explain how the user can authenticate on the web interface using their existing mobile credentials. This way, the mobile device can act as an authenticator for both mobile and web environments.

I. Flutter application

Flutter is an open-source UI software development kit created by Google. It is used to develop cross platform applications for Android, iOS, Linux, macOS, Windows, Google Fuchsia, and the web from a single codebase.

To create our new Flutter application we need to run:

flutter create flutter_app

Add our dependencies are:

  • http — http client to call our REST API.
  • shared_preferences — to save user data on the device.
  • fluttertoast — to show some simple messages to the users.

We need to run:

flutter pub add http
flutter pub add shared_preferences
flutter pub add fluttertoast

To consume our WebAuthn endpoints exposed by our REST API, we need to use the Fido Google Play services library: com.google.android.gms:play-services-fido:19.0.0-beta . From Flutter, this is possible using a plugin. I tried this fido client, but others may be available. We also have to pass the — no-sound-null-safety parameter when running the app because an update is needed by the plugin to support null safety. You also need to get it from github locally and reference it in the pubspec.yaml file. It should look like this:

The next step is to create a trusted link between the mobile application and the domain exposing our API. From Android Studio, you can generate a file that needs to be exposed from your domain on this address: https://<domain>/.well-known/assetlinks.json Let’s save the content somewhere for now. We need to add it later in our Spring Boot application. If we skip this step, the fido client won’t be able to authenticate our users, because the domain we call doesn’t indicate it accepts WebAuthn requests from our mobile application. (You may notice a SHA fingerprint related to our apk file — the value is computed based on the keystore you are using when building the apk)

There are some screens we need to create to have a working example.

1. User registration

New user registration

Our application always shows the registration page when it starts. This contains two fields for the firstName/lastName and two buttons to either create a new account or to go directly to the login page. The Flutter application starts based on the main.dart file below:

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebAuthn Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const RegisterNewUserScreen(),
);
}
}

The RegisterNewUserScreen class exists in the register_new_user.dart file. To display a simple form we can use a Column to list some input fields — TextField and an ElevatedButton to trigger the signup action.

If the api calls from the registerNewUser method are successful, we can route the user to the Login screen.

if (mounted) { // we need to check this flag in an async method
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const LoginScreen(),
),
);
}

What should our mobile application do to register the new user?

I mentioned the http dependency at the beginning. We need to use this to call our Spring Boot application.

The DSL is pretty simple and we can chain async calls easily in dart. If the method we write is doing some async calls (i.e: waiting for the server to answer) we need to add the async keyword to the method signature and await for the result. You may notice the result is wrapped in a Future type, indicating we should wait for the result. The Future can also throw an error, if the server wasn’t able to process our request.

The response from this call contains some constraints, the fido client needs to use when creating a new set of strong credentials. This includes:

  • private key — never leaving the device, used to sign challenges received from the server
  • public key — to be stored on server side (our API) in the registration ceremony. It is used by our API in the authentication flow to validate signed challenges received from the mobile app.

The API gets only the first name/last name information in our setup and it provides an auto-generated username and an userHandle representing an internal API identifier for the userId. One of the username or userHandle needs to be present in the registration flow. If we want to support completely anonymous users in our system, this is the way to go. We can skip having even the username with WebAuthn!

Now that we have the registration response from the server, we need to create a new set of public key based credentials using the fido client. We have to pass the information identifying our backend application: rpId — relyingParty Id, rpName — relyingParty Name.

Our API is called a Relying Party in the WebAuthn terminology and the fido client associates the new credentials with our domain. This is a very important step preventing phishing attacks. For example if another system (evil application) is trying to authenticate our users from a different domain, the fido client won’t let our users authenticate against it even if they click some phishing link.

When the user proves his presence and identity to the platform authenticator (mobile device), we get a registration result from the fido client. This contains the public key our API needs to store to finish the registration flow.

Now that we have a new user with a set of strong credentials associated, we can move to the authentication part. Note that we are using the userHandle to identify the user and the keyHandle to reference his public key based credentials.

2. User authentication

Authenticate registered user

In the login_account.dart file we define our login page. We can customise our login button with a fingerprint icon to tell ours what to expect. If the authentication flow is successful, we can route the user to the welcome page.

With passwords, you usually call one endpoint with the credentials (username/password pair) and get back an authentication token if they are valid.

With WebAuthn, the login flow contains two steps instead:

  • Get an authentication challenge from the server associated to a specific user. We use the userHandle stored in the previous step (signup flow)

The server provides the login challenge in the /api/assertion/start endpoint.

  • Use the fido client to sign the authentication challenge from the server using the local private key (keyHandle) after the user proves his presence and identity. The SigningResult contains the signed challenge and we pass it to the backend.

If the validation is successful on the server side, we get a sessionId cookie we can use in other calls.

I used a stateful approach for simplicity. Spring Security can associate the authenticated user from the session in other http requests. For example, we are calling the /api/whoami endpoint to get more details about the authenticated user with the same sessionId cookie value.

Now that we have an authenticated user, we can show the welcome page and potentially add other protected pages. Don’t forget to store the session Id to be able to call the API on behalf of our authenticated user.

Let’s now move to the server part.

II. Spring Boot application

In a previous article I explained how to add WebAuthn on top of Spring Security in a Spring Boot application with a WebAuthn starter library I wrote. We are going to use it to expose a couple of endpoints to support the registration and authentication flows for completely anonymous users.

If you want to create a new Spring Boot application from scratch you probably already know about this starter. I will also put a link with the entire code at the end of the article if you want to check it out. We are going to use web, data-jpa, h2 and spring security dependencies:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Remember the assetlinks.json file from earlier? We need to add it in our project. By default, Spring Boot is exposing files from the classpath:/static location. We can save it there:

In this simple API we are going to activate WebAuthn and save every new registered user with a spring data jpa repository. In a real application, probably an User contains more details.

Any request is also authenticated, except the assetlinks.json file and the h2 console we are going to use later.

@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/h2-console/**", "/.well-known/assetlinks.json");
}

We expose the currently authenticated user using the AuthenticationPrincipal annotation from Spring Security and query the user repository for more details.

@RestController
public class UserController {

@Autowired
MyUserRepo userRepo;

@GetMapping("/api/whoami")
public MyUser whoami(@AuthenticationPrincipal UserDetails user) {
return userRepo.findByUsername(user.getUsername())
.orElseThrow();
}
}

When the fido client is storing the credentials, they are linked to the relyingPartyId and relyingPartyName We have to add the values in the application.yaml file. Let’s try to use localhost for now:

webauthn:
relying-party-id: localhost
relying-party-name: WebAuthn Application
relying-party-origins: http://localhost:8080

Believe it or not, we are ready to run our first test.

III. Running the project

Let’s start our API locally: mvn spring-boot:run. From the logs we can see it started on the 8080 port.

2022-10-04 10:14:45.604  INFO 64360 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-10-04 10:14:45.613 INFO 64360 --- [ main] com.mih.webauthn.demo.DemoApplication : Started DemoApplication in 4.126 seconds (JVM running for 5.012)

From the Flutter application, we need to tell the http client where the API is located:

class ApiClient {
static const String baseUrl = 'http://localhost:8080/api';

We allow our emulator to call our localhost machine with the command below. (Inside the emulator, localhost means the mobile device loopback, therefore we need to forward the requests from the mobile app to the API)

adb reverse tcp:8080 tcp:8080

If we tap the Create a new account button, we see the first API call is successful, we reached the API on the /api/registration/start endpoint

registerRequest - status: 200, body: {"status":"OK","registrationId":"fkYNod+hTFQivdirUCzdrg==","publicKeyCr...

Unfortunatelly the fido client initiateRegistration gives a timeout. To understand why the pin screen is not there, we can see from the Logcat tab, some errors.

The error we faced is caused by the incorrect configuration of the relyingParty origin field. Here you can see how it should be configured. The closest configuration to a real production environment is to have a real (temporary) domain. We can use local tunnel to expose our local API instance:

lt --port 8080 --host http://loca.lt -o
your url is: https://spotty-rings-remain-86-126-30-191.loca.lt/

We need to update our application.yaml . The origins field tells the API it should accept authentication requests from that remote location: https://salty-islands-reply-86-126-30-191.loca.lt

webauthn:
relying-party-id: spotty-rings-remain-86-126-30-191.loca.lt
relying-party-name: WebAuthn Application
relying-party-origins: https://salty-islands-reply-86-126-30-191.loca.lt

In the Flutter application, we need to update our http client /lib/client/api_client.dart:

class ApiClient {    static const String baseUrl =
'https://salty-islands-reply-86-126-30-191.loca.lt/api';

If we try again we can see another error:

Caused by: java.lang.IllegalArgumentException: Incorrect origin: android:apk-key-hash:t5OwAx-0r2saAq-IRdO8SuK_hkZWOglhkDSwg7OSpFE

This is because we need to tell our API is should accept requests coming from our Flutter application. The one we used it’s actually valid for a web application. The application.yaml file looks like this now:

When we try again, we can finally see both the registration and authentication are successful:

New account
Authentication

Having an end to end example on our local machine makes development very easy. If we want to troubleshoot some issues, we can activate breakpoints on both Spring Boot side and Flutter side

You can inspect request payloads or responses at any step of the flows.

Let’s now move to the last part. Can we reuse the credentials from another environment?

IV. Authenticate users on web

Now that we have users registered in the mobile app, let’s see how we can authenticate them in our website.

We first need to create a login page: login.html in the src/resources/static location.

We allow unauthenticated users to load the login page, including the javascript files (containing the WebAuthn web components).

Anonymous users don’t rely on the username to get the authentication challenge. The web component, however is requesting the username field. The webauthn spring boot starter is storing user details in the H2 database using spring data in this setup.

To find the autogenerated username, we use the h2 console interface. You can read more about resident keys and how we could skip this manual step. This part is only a proof of concept and it works if the user is required to enter his username. If you want to make use of the discoverable keys on the client side, the authenticator can tell the relying party what userHandles are available and the authentication can progress.

After we complete the value, we see the browser is asking us to prove our identity using either the platform authenticator (my machine) or some roaming authenticator (something we can connect to via USB, bluetooth etc).

To be able to complete the login flow using my real mobile device, I am running the Flutter application on my device to register a new user. Don’t forget to switch from the emulator to your device:

Since I have the fingerprint added on my phone, I won’t see the PIN being requested.

New user on a real device

Now that I have a registered user on my phone, I am adding the auto generated username from H2 in the input field from the login page. I choose my phone from the authenticators list:

I am getting a notification from Chrome and the mobile device is connecting to my laptop using bluetooth. You can notice the relying party (with my temporary domain) wants to verify my identity.

After I prove my presence and identity on my phone, I am redirected in the web to the /api/whoami endpoint as the authenticated user. (Using the JSessionId cookie, helps the browser to keep a valid session in the browser and I am seeing the JSON representation of MyUser).

I hope you enjoyed this article and try WebAuthn in your future applications. The entire code is here.

Thanks for reading and happy coding!

--

--

Mihaita Tinta
ING Hubs Romania

A new kind of plumber working with Java, Spring, Kubernetes. Follow me to receive practical coding examples.