How to login your users without a password (but with a magic.link)
Johannes Hinkov
Posted on August 30, 2023
How do you deal with your passwords?
Do you use the same "secure" password on every website?
Or are you generating a random password each time and store it "securely" in a password manager?
However you do it, i'm pretty sure you don't have really secure and different passwords for all your accounts (and remember them in mind at the same time..).
If you put all your randomly generated passwords into a password manager, you will need to be worried every time they get hacked (spoiler alert: they get hacked pretty often).
If you don't use a password manager, you inevitably end up using the same password over and over again (because no one loves to remember thousands of passwords..)
So this is a pretty unpleasant situation if you ask me, right?
What can you do as a builder to make your users lifes a little better when it comes to dealing with passwords?
Thanks to magic links there is a nice solution!
What are magic links?
If you are like me, you often times find yourself forgetting your passwords to your accounts.
What i'm doing then (and you probably too) is clicking the 'Forgot your password' link and then you get a link to reset your password.
You click the link, which has superpowers (through a generated token which acts like an One Time Password (OTP) that is attached to the link), enter a new password and boom - You have a new password and are able to login again.
Now you could say a magic link is nearly the same workflow - with the difference that there is no password and you don't have to click a 'Forgot password' link.
When you want to login to a service, you just enter your email and that's it. You get a magic link emailed, you click the link and you are in.
Now you might think this is quite uncommon and maybe you are right - Most of the websites are using conventional login workflows with username and password - but .. the magic.link authentication has a lot of advantages!
Advantages of magic links
1. Great User Experience
No one likes to remember passwords and everyone has an email account.
You will never face the problem that a user doesn't login to your service or website only because he forgot his password and is too lazy to reset it.
2. You don't need to deal with email infrastructure or verifying emails
With the solution that i will show you here, you don't need to setup a smtp service or mailing server.
Further, because the email is being validated implicitly (by your users logging in through a mail they get into their mailbox) you don't need to verify / check the provided email adress explicitly.
You know your user has a working email account that is not full every time he logs in.
3. You don't need your users to explicitly confirm their email on registration
In the traditional user/password auth workflow a user will have to confirm his email adress upon registering.
With the magic.link auth workflow, the confirmation is being done on every login implicitly. So again a better user experience for your customers / users.
4. You can register users "on the fly"
If you don't need your users to enter additional data like name, adress etc. and the email is all you need, you can even further improve the user experience and create an account on the fly when a user is not already existing on login.
So you don't have to differ between signup and signin.
Also if you really need additional data, you can ask later in the process for your user to specify what you need - in a separate formular.
Implementing your own authentication workflow with magic links
So let's finally implement the magic.link authentication in your app.
For this example here i'm going to use Spring Boot 3, React 18 and the service from https://magic.link.
You will need an account which is free in the basic version (up to 1000 users).
I have extracted the basic functionality into a library which you can integrate as a maven dependency into your Spring Boot project.
I will also show you a reference implementation that illustrates how exactly the library is being used.
You can use the provided reference implementations for Spring Boot and React as boilerplate / base to bootstrap your own projects!
The architecture
Let's first have a look at the login flow.
The solution here integrates with a backend that uses JWT for authentication.
There's a high chance you are also already using an implementation in your backend that relies on JWT.
If that's the case, you can simply combine the magic.link auth and integrate the service into your app.
It's also conceivable to only use the magic.link auth and solely rely on the DID token in your backend, but that's another way and a different story.
So here is how it works:
We will integrate magic.link components into our frontend (how exactly is shown down below).
These components will then make a call to magic.link and initiate a login request, as soon as our user enters his email and hits the login button.
The user then will get a popup with an OTP which he will need in a second to login.
At the same time, an email gets send to the provided email adress with a magic link.
The user enters the provided OTP, if it is correct, he will be logged in in the same window, that initiated the login request.
The other window can be closed.
Once the user entered the correct OTP, we receive the DID Token in the frontend and can call our backend.
The backend is going to validate the DID token (through calling magic.link API) and then, only if it is valid, create a new JWT and give it back to the caller, our frontend.
From there, we can simply save our JWT as usual in a cookie and provide it in the header in subsequent calls to the backend.
user-service (library)
Let's have a look at the implementations and start with the core, the user service i've created as a small library.
You can simply put it into your project as a maven dependency:
<dependency>
<groupId>de.munichdeveloper.user</groupId>
<artifactId>user-service</artifactId>
<version>1.2.3</version>
</dependency>
When you add that library, it will enrich your application by the following components:
- A rest controller with a method signinByMagicToken
This controller will handle the request from our frontend. It expects the DID Token (decentralized Identifier Token), which we will get from the magic.link service and that will be requested by our frontend.
We'll have a look at that later.
Here is the code that does the magic:
public JwtAuthenticationResponse signinByMagicLink(String didToken) throws IOException, InterruptedException {
DIDToken parsedDIDToken = DIDTokenHelper.parseAndValidateToken(didToken);
String issuer = parsedDIDToken.getIssuer();
UserMetadata metadataByIssuer = getMetadataByIssuer(issuer);
String email = metadataByIssuer.getData().getEmail();
String jwt = jwtService.generateToken(email);
return JwtAuthenticationResponse.builder().token(jwt).build();
}
We get the DID token handed over, that we got from the magic.link service.
We then parse the token, which is done by some helper methods.
From the parsed token, we read out the issuer, which is the user that needs to be logged in.
We call the magic.link API to get some metadata for the issuer to get the email of the user.
Now we are almost done, at this point we know everything is okay and the user has provided a valid DID token.
Finally, we create a JWT for the email and return it back.
Implementing the Spring Boot 3 backend
As already mentioned, you will need to include the user-service lib as maven dependency here in your backend implementation.
Note that the maven dependency is not published and thus you will need to build it on the same machine, where you are going to build the backend service, so that it resides in your local maven repository.
The next step is to adapt your SpringBoot Main Class and add a few annotations.
Here's an example:
@SpringBootApplication
@EnableJpaRepositories({"de.munichdeveloper.user"})
@EntityScan(basePackages = {"de.munichdeveloper.user"})
@ComponentScan(basePackages = {"de.munichdeveloper.user", "de.munichdeveloper.magic"})
public class SpringBootMagicLinkApp {
public static void main(String[] args) {
SpringApplication.run(SpringBootMagicLinkApp.class, args);
}
}
We need to tell our Spring Application where it needs to look for further components, entities and repositories.
Otherwise, these components from the lib wouldn't get scanned and thus wouldn't work.
For the sake of completeness, here are two further configs that are needed in certain cases.
The first is the CorsConfig, which i am using because in my example application, my frontend and my backend are not hosted on the same server.
The second is the SecurityConfig, which should be pretty straight forward.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults())
.csrf(CsrfConfigurer::disable)
.authorizeHttpRequests(request -> request.requestMatchers("/magic/**")
.permitAll().anyRequest().authenticated())
.sessionManagement(manager -> manager.sessionCreationPolicy(STATELESS))
.authenticationProvider(authenticationProvider()).addFilterBefore(
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
I will not go too deep into these, as they are, as already said, pretty straight forward. If you are not familiar with SecurityConf and CorsConfig in SpringBoot, i recommend you google them or just C&P them (and adapt where needed) ;-)
Implementing the frontend with React 18
Time to have a look at the react frontend.
In order to use the magic lib, you have to add it to your package.json with:
yarn add magic-sdk
To be able to call the magic.auth API, we need to get our public key from our magic account and put it into the .env file under the key REACT_APP_PK_KEY
To do so, get into your dashboard of your magic account and create a new dedicated app.
It should look similar to this:
As the name says, it is a public key, so it's ok that it is exposed to the users browser (and you are allowed to see my PK also .. but you should use your own :-P )
Now, the interesting part is in the code located in lib\magic.js
The method loginUser is being called in the login form (located in components\Authenticate.js)
export const loginUser = async (email, cb) => {
let did = await magic.auth.loginWithMagicLink({ email });
let authPromise = api.authenticateWithDid(did);
let response = await authPromise;
let { data } = response;
let { token } = data;
const parsedJWT = parseJwt(token);
const user = { data: parsedJWT, token };
return cb({ isLoggedIn: true, user });
};
First, we call the method loginWithMagicLink from the magic auth library.
The method blocks and returns the desired did token when finished.
We then pass the did token to our backend by calling our API.
As already described earlier, here we receive a jwt token and can finally hand it over to a callback method, which will then save the jwt into the user context, where it will be picked up for subsequent calls.
Demo
Demo Time!! 🎥
You can have a look at my current project to see the auth login live in action: docuMAN
docuMAN is an AI journaling App which i developed primarily for myself :-)
It helps me to efficiently document things from work (and also other areas of my live) AND also find them later.
This last point is crucial, because i was documenting so much stuff (in traditional apps like evernote) only to later struggle with finding the desired informations again.
docuMAN helps here with a mix of AI and elasticsearch, so i can really find what i need.
If you think this is a cool idea and it could be of use for yourself, i would be happy if you try docuMAN out for free!
Here's the link again: https://documan.onrender.com
Any feedback is highly appreciated and helps a lot!
Final words
Thank you guys for reading my article!
If you found it useful i would be happy if you star the repos on github and drop a like on this post here! ⭐⭐⭐
Also feel free to ask me any questions or send any problems you might have!
All the code is available on github in these repositories here, feel free to use it for your next projects (MIT license):
However, keep in mind this is not production ready code! You will have to finish the code if you are really going to use it in production ;-)
Posted on August 30, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.