Magic Link Authentication in react SPA - reactjs

For a small app catering to a very small set of users, we are planning to implement magic link authentication. The user would come to the application, enter their email address, get a magic link on the email address, after the user clicks on the link, they are logged in to the app.
I am not able to find enough resources to figure out how to do it in a SPA. Here are some helpful links:
Magic Link with Node
https://medium.com/#aleksandrasays/sending-magic-links-with-nodejs-765a8686996
https://medium.com/one-more-thing-studio/how-to-make-magic-links-with-node-1d164c036e29
Magic Link with Auth0
https://auth0.com/passwordless
This is the SPA workflow that I have in mind:
User comes to the SPA
The SPA takes the user to the login page where they can provide their email address.
The SPA sends the email address to the backend api
The api decides whether or not the user is registered, and sends them an email with a short lived jwt.
Clicking on this link takes user to a SPA route with the jwt in query params.
The Frontend forwards this jwt to the api backend, and the api backend verifies the jwt and sets a cookie
This cookie can then be used to maintain the user session.
I want to verify this workflow, but I am not able to find enough resources.
Specifically, I want to clarify whether the magic link should send the user to the SPA and the SPA should be responsible for extracting the jwt and sending it to the API backend, or is there another way to do it?
Is this how this should be implemented? What are the security implications?
I am using react and react-router.

Cotter co-founder here.
We have a super easy magic link integration for React. Here's a guide to set up a Simple Magic Link Login for your React App.
You can integrate magic link login in 2 steps:
1. Add dependencies
yarn add cotter
2. Show the log in form
(step 2-4 in your flow)
import React, { useEffect } from "react";
import Cotter from "cotter"; // 1️⃣ Import Cotter
function App() {
useEffect(() => {
// 2️⃣ Initialize and show the form
var cotter = new Cotter(API_KEY_ID); // 👈 Specify your API KEY ID here
cotter
.signInWithLink() // use Magic link
.showEmailForm() // show email login form
.then(resp => console.log(resp))
.catch(err => console.log(err));
}, []);
return (
// 3️⃣ Put a <div> with id "cotter-form-container"
// that will contain the form
<div id="cotter-form-container" style={{ width: 300, height: 300 }} />
);
}
export default App;
You can create your API_KEY_ID here.
Done! Now you should have an email Login Form that sends a magic link to your users. Here's a working example.
The response
After the user click the link (step 5), you'll receive the following response in then((resp) => console.log(resp)):
{
"email": "youremail#gmail.com",
"oauth_token": {
"access_token": "eyJhbGciONiIsImtiJFUzI1pZCI6...",
// you'll also get a refresh token and an id token
},
"user": {
"ID": "abcdefgh-1234-5678-1234-f17786ed499e", // Cotter's User ID
// More user information
}
}
You can then send this response to your backend server and do the following steps: (step 6-7 in your flow)
Verify if the access_token (a JWT token) is valid.
If it's valid, you can register the user if the email is not recognized (you should also associate the email with Cotter's user id).
You can use the access_token for all your API endpoints, or you can generate your own session and set a cookie

Checkout this Reack Hook use-magic-link to integration Magic Link very quickly into a React app.
Read this article for more info: Simple Auth Setup for Your React App

This is the workflow for magic link:
When a user enters the email address, Magic sends verification link to the email address to verify that email. When you click on "Confirm your email", a modal will show up to log in to the app. When the user click on the "Log in to app", Public+Private keys are generated and stored on the browser. That means users own their own identity. This key pair is in embedded iframe and inaccessible to the developer in order to protect the user's private key.
Since those keys are generated on the user's client instead of magic servers, Magic will not be able to see those secrets.
Magic sdk will use that private key to generate auth token. This auth token is called DID token (Decentralized Identifier). When you decode this token, you can see the user's email address and when it was issued. Essentially, DID token is your digital signature. If we store this token in our database and if our database gets hacked, malicious users will not be able to access our private key. Then we pass this DID token to the server to check the user
Magic stores the user's email and id in indexedDb. It also stores some cookies for itself, to function properly
to work with magic, you use magic-sdk. You set the magic client ✨
import { Magic } from "magic-sdk";
const createMagic = () => {
return (
new Magic(process.env.API_KEY)
);
};
export const magic = createMagic();
then using this client:
// you have input and captured the email
if (email) {
try {
// this store public/private key on browser and returns the DID token
const didToken = await magic.auth.loginWithMagicLink({
email,
});
if (didToken) {
// once you have the token, using metadata, you can add another propertis
// iat, exp, roles etc
// then sign this with jwt
// store the token in browser
}

Related

Misunderstanding the process of JWT authentication

I create project using React + Redux + Apollo Client + Graphql
When we need to log in in our app we need to use token (saved in localStorage for example) which is put in the headers parameter like in the code below:
const client = new ApolloClient ({
uri: 'http://localhost:4000/api',
headers: {
authorization: `Bearer ${localStorage.token}`,
},
});
After request server verifies token and becomes aware who is the user.
My question: from where do we need to get token and put it to the headers parameter for log on (sign up) process? A new customer comes to our log on page, he has no token (in localStorage or somewhere else) at the beginning but server requires it in the requests. And if we remove headers parameter from our client, the log on process will proceed but server won't understand who is the current user.
Typically the server would be the one issuing the JWT token, and this would happen during user login or maybe during account creation. For these particular calls, you should not be expecting the JWT in the header. Instead, the user would be passing credentials, such as username and password. For most other calls, it is appropriate to pass the JWT in the header of the request.
Keep in mind that the main purpose of the JWT is free the user from having to provide personal credentials during most requests. Instead, the user can just present a JWT, much as one would present a passport, to get access to your web services.
In response to your comments below, I would suggest that you keep the signup/registration process separate from the user-only area of your application. Here is a typical workflow:
Prospective user visits your site, and creates an account, by choosing a username and password, and possibly by providing certain other personal information
Your application creates an account, and then sends an email verification link to the user's email address. The server lands the user on a page which mentions all of this
The user opens the email, which contains a verification link, which when clicked will activate the account. Your application returns a web page which then asks the user to login.
Finally, the user logs in from the normal login page.
Note carefully here, that JWT were not at all involved in the signup process, nor do they need to be. The user JWT only needs to come into existence after the user actually logs in for the first time.
Decision:
you need to check for token in localStorage and update the request if token exists
const client = new ApolloClient({
uri: 'http://localhost:4000/api',
request (operation) {
const headers = {};
const token = localStorage.getItem('token');
if (token) headers.authorization = 'Bearer ' + token;
operation.setContext({ headers });
}
})

Blank page after login using bookmarked authorization URL in IdentityServer4

We have discovered that our users very often for the first time visits our web application by browsing the direct URL of the OIDC client (https://oidcclienturl.com/), The ASP.NET Core OIDC authentication middleware kicks in and the user gets redirected back to Identityserver 4 login page.
Everything works fine but then they decide to add the (temporary? state, nonce, cookies...) authorization URL as a bookmark in their browser before entering their credentials and continuing back to the web application.
This causes an issue when the user later uses the bookmark in a new session. The login seem to actually work after entering valid user credentials even if the user uses an old authorization URL, but when the user gets redirected back to the web application they end up on a blank page (https://oidcclienturl.com/signin-oidc).
After the blank page have been loaded the user is able to browse the direct URL (https://oidcclienturl.com/) sucessfully and appear as an authentcated user in the web application.
Any ideas whats causing the blank page?
That blank page shouldnt exist, if I understand it correctly its the default callback path of the oidc authentication middleware in ASP.NET Core.
Unfortunately, the real-world problem of users bookmarking the login page isn't handled cleanly by OIDC, which requires the client app to initiate the login flow.
I've addressed this by adding a RegistrationClientId column to my user data table, which is the Identity Server ClientId corresponding to the client app that called IDS when the user account was created. In the client app configuration, we use the custom Properties dictionary to add a URI fragment:
new Client
{
ClientId = "some_client",
ClientName = "Some Client",
ClientUri = "https://localhost:5000",
Properties = new Dictionary<string, string>
{
{ "StartLoginFragment", "/Auth/StartLogin" }
}
// other config omitted
};
When a user logs in, an empty return URL indicates IDS wasn't called by a client app, so we use RegistrationClientId to query IClientStore, then we combine the ClientUri and StartLoginFragment URIs and use the resulting URI to redirect the user back to the client application.
Over in the client application, that endpoint kicks off the OIDC sign-in flow, and since the user is already signed-in on IDS, it comes right back to the correct location in the client app. The controller action looks like this:
[HttpGet]
public async Task StartLogin()
{
await acctsvc.SignOutAsync();
await HttpContext.ChallengeAsync("oidc",
new AuthenticationProperties()
{
RedirectUri = "/"
});
}
The call to SignOutAsync just ensures any client-app signin cookies are cleaned up. It's in our custom account service, but it just runs HttpContext.SignOutAsync on the usual "Cookies" and "oidc" schemes. Normally that would also result in a signout call to IDS, but the redirection by the subsequent ChallengeAsync replaces the pending signout call.
The downside is that the action is an HTTP GET meaning pretty much anyone could theoretically trigger this action. At most it would be an annoyance.
In the special case where your IDS is only handling auth for a single client, you can skip a lot of that -- if they land on the page with no return URL, just send them to your client app start-login endpoint straightaway, before they login.

firebase admin SDK create user and send verification email

How can I send a verification email after I create a user with firebase admin SDK?
I am trying to combine createUser function and sendEmailVerification function
could somebody indicate a hint or an answer? thanks
update:
the user creation is being done by an admin user who is already signed in in the app, so the admin user is just creating users on the dashboad. This is completely different from the registeration methods.
update 2:
I tried to follow bojeil's answer, I am still stuck with the step that user signs in with the custom token. It gets into conflict with my current admin user session, the admin users gets kicked out and instead the new user is signed in and even when I sign out the new user, the admin user is still out and needs to sign in to get back into the app.
here is my code inside the app after I get the custom token:
$http.post('/.custom-token', {uid: $scope.data.data.uid})
.then(function (response) {
console.log("custom token here:", response.data.token);
firebase.auth().signInWithCustomToken(response.data.token)
.then(function (firebaseUser) {
firebaseUser.sendEmailVerification();
firebase.auth().signOut().then(function() {
// Sign-out successful.
console.log("signed out success");
}, function(error) {
// An error happened.
});
})
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
});
so, I get the token, sign in the new user, send the email verification link, and then, sign out the new user.
But my admin user who is doing all of this gets signed out as well.
what am I missing here?
OK this is what you can do but you may hit quota limitations:
Include the firebase-admin module.
Include the firebase client module.
using admin sdk, create the new user via createUser
when that promise resolves, get the uid of the user created.
using admin sdk, create custom token for that uid.
using client sdk, signInWithCustom token using that custom token.
A user is returned in the process, call user.sendEmailVerification()
signOut that user from the client SDK.
According to firebase, the admin-sdk does not currently support this feature. See their response here:
https://stackoverflow.com/a/44557980/8177355
Whenever an email / password authentication user is logged in, and attempts to use a feature that requires authentication, I call onAuthStateChanged() and then check the user's record for email verification.
If the email is not verified, and I have not sent a verification email before, I send it automatically. I return an error, asking the user to verify their email. (I store a variable in a profile setup for the user in firestore, to indicate whether it has been sent previously).
On future attempts to use the app, if the email is still not verified, I return the same error, and also include a button in the error labeled "re-send verification email" that triggers sending of the verification email when pressed. (This way I am not automatically sending tons of verification emails every time the user tries to do something.)
A rather clean solution would be to actually use the REST APIs.
curl 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode?key=[API_KEY]' \
-H 'Content-Type: application/json' \
--data-binary '{"requestType":"PASSWORD_RESET","email":"[user#example.com]"}'
The [API KEY] is a web client api key that can be retrieved from Project Settings > add an app >> click web and you will get the configuration with a JSON, within the JSON there is an APIKey that is the one that you need to use.
You don't even need to use the Firebase Admin SDK for this. You can just use the regular Firebase client-side SDK:
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(function(user) {
console.log("User successfully created:", user.uid);
return user.sendEmailVerification();
})
.then(function() {
console.log("Email verification email successfully sent!");
})
.catch(function(error) {
console.log("Error:", error);
});
Here's bojeil's steps in code, which work in a NodeJS Firebase Cloud Function. Assuming that 'admin' is your Firebase Admin SDK instance, and 'firebase' is your Firebase Client SDK instance.
var userId = "YOURUSERIDHERE";
admin.auth()
.createCustomToken(userId)
.then((customToken) => {
return firebase.auth().signInWithCustomToken(customToken)
})
.then((data) => {
return data.user.sendEmailVerification({url: "http://YOURREDIRECTURL"});
}).then((result) => {
console.log("success");
}).catch((error) => {
console.log("faillure");
});
Make sure that you've setup the Admin SDK properly, by creating a service account and initialising it in the Admin SDK configuration snippet.
I think we can try the following combinations:
createUser()
generateEmailVerificationLink
Send the email verification link using Firebase Trigger Email Extension.
What I did is very simple:
Create a user in the server
In the client await the server creation and after that sign-in using the client SDK,
in the background listening to firebase auth user change event and than use the user and call sendEmailVerification
This is my React code:
await Api('signupUser', data) // server call
await firebase
.auth()
.signInWithEmailAndPassword(email, password); //client login
// send email on login
useEffect(() => {
return firebase.auth().onIdTokenChanged((user) =>
user?.sendEmailVerification());
}, []);

How do you change what gets displayed on a front-end (Angular) view based on properties in your JWT token?

I'm pretty new to working with tokens. Just started learning yesterday.
I have an Express backend API. I understand that the token prevents anyone from getting access to data on any given API endpoint/json data...But how can I READ/decrypt the JWT when it's on the angular side?
I.E., Okay, I know that this user is logged in and can therefore view this page, however, this particular user is the CREATOR of this event. Therefore, on this event's show page, users who have been invited are allowed to view it, and so is the event creator, but only the event creator will see a button that when clicked, does a delete request to the event. The other users will not see this button.
The only way I see this being possible is that the JWT containing the user object can be decoded on the front/end, then I have access to variables with the decoded JWT properties. I.E., username and userID. That way, on the view page being rendered in Angular, I can code logic such as:
```
if (decodedJWT.user.username === event.creator.username) {
DO SOMETHING HERE LIKE DISPLAY A CERTAIN BUTTON
}
```
Thanks.
You need to create an API point in your nodeJS backend that will return the user details if he is logged in:
app.use('/userinfo', expressJwt({secret: secret}));
Here you make sure /userinfo is protected by JWT and your secret phrase.
Then the route corresponding to this API returns user info:
router.get('/get', function (request, response) {
response.json({
id: request.user.id,
name: request.user.nom,
email: request.user.email,
role: request.user.role
});
});
The full API is located at /userinfo/get and is secured by the JWT token.
In angularJS you just need to make a request to that API using simple $http.get('/userinfo/get') which will return a user object or an error from the server if not logged in.
Of course you need to pass the token to all your $http requests.

What is the correct way to use Laravel basic auth with angularjs?

Here is the approach that I follow.
I secure the routes for my API like this:
Route::group(array('prefix' => '/api/v1'), function () {
Route::get('dashboard', array('before' => 'basic.once', function () {
return 'Dashboard';
}));
});
I am planning to use basic auth over an SSL connection.
I have to send username and password to the with every request.
I understand that I need to store some user details on the client side (angular/browser) so that the user logs in once and is allowed to access protected routes until his session is valid.
What I don't understand is what user information do I store at the client end and how?
The API will be used for building mobile apps in future.
What is the simplest thing I can do to achieve this?
The simplest way is that when a user register (or you create a user), you add an extra field not just the standard credentials (username, password), this extra field can be token what you'll generate when you create the user, after that when your user logs in you send back to him this unique token, and at client side (angularJS) you can store this token for example in session storage, and after this you need to send this token with every API call. To simplify the back-end you can make a filter to test the token and log in the user. This will be important if you want to use your application on mobile devices where aren't cookies, so you will log in the users with every call.

Resources