How to Authenticate Users with MetaMask using Angular
Introduction​
This tutorial demonstrates how to create an Angular application that allows users to log in using their Web3 wallets.
After Web3 wallet authentication, the server creates a session cookie with a signed JWT stored inside. It contains session info (such as an address, signed message) in the user's browser.
Once the user is logged in, they will be able to visit a page that displays all their user data.
Prerequisites​
- Follow the Your First Dapp - Angular tutorial to set up your Angular dapp and server
Install the Required Dependencies​
To implement authentication using a Web3 wallet (e.g., MetaMask), we will use a Web3 library. For the tutorial, we will use @wagmi/core.
- Install
@wagmi/core
,ethers
, andaxios
dependencies -@wagmi/core@0.5.8
is a stable version we can use with Angular 14:
- npm
- Yarn
- pnpm
npm install @wagmi/core@0.5.8 ethers@^5 axios
yarn add @wagmi/core@0.5.8 ethers@^5 axios
pnpm add @wagmi/core@0.5.8 ethers@^5 axios
- Open
src/environments/environment.ts
- add a variable ofSERVER_URL
for our server.
export const environment = {
 production: false,
 SERVER_URL: 'http://localhost:3000',
};
- We will generate two components (pages) -
/signin
(to authenticate) and/user
(to show the user profile):
ng generate component signin
ng generate component user
- Open
src/app/app-routing.module.ts
, add these two components as routes:
import { SigninComponent } from './signin/signin.component';
import { UserComponent } from './user/user.component';
const routes: Routes = [
 { path: 'signin', component: SigninComponent },
 { path: 'user', component: UserComponent },
];
Initial Setup​
We will do an initial setup of our /signin
and /user
pages to make sure they work before integrating with our server.
- Open
src/app/signin/signin.component.html
and replace the contents with:
<h3>Web3 Authentication</h3>
<button type="button" (click)="handleAuth()">Authenticate via MetaMask</button>
- Open
src/app/signin/signin.component.ts
and add an emptyhandleAuth
function belowngOnInit(): void {}
:
ngOnInit(): void {}
async handleAuth() {}
- Run
npm run start
and openhttp://localhost:4200/signin
in your browser. It should look like:
- Open
src/app/user/user.component.html
and replace the contents with:
<div *ngIf="session">
<h3>User session:</h3>
<pre>{{ session }}</pre>
<button type="button" (click)="signOut()">Sign out</button>
</div>
- Open
src/app/user/user.component.ts
and add the variable we used above and an emptysignOut()
function:
session = '';
ngOnInit(): void {}
async signOut() {}
- Open
http://localhost:4200/user
in your browser. It should look like:
Server Setup​
Now we will update our server's index.js
for the code we need for authentication. In this demo, cookies will be used for the user data.
- Install the required dependencies for our server:
npm install cookie-parser jsonwebtoken dotenv
- Create a file called
.env
in your server's root directory (wherepackage.json
is):
- APP_DOMAIN: RFC 4501 DNS authority that is requesting the signing.
- MORALIS_API_KEY: You can get it here.
- ANGULAR_URL: Your app address. By default Angular usesÂ
http://localhost:4200
. - AUTH_SECRET: Used for signing JWT tokens of users. You can put any value here or generate it onÂ
https://generate-secret.now.sh/32
. Here's anÂ.env
 example:
APP_DOMAIN=amazing.finance
MORALIS_API_KEY=xxxx
ANGULAR_URL=http://localhost:4200
AUTH_SECRET=1234
- Open
index.js
. We will create a/request-message
endpoint for making requests toMoralis.Auth
to generate a unique message (Angular will use this endpoint on the/signin
page):
// to use our .env variables
require('dotenv').config();
// for our server's method of setting a user session
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const config = {
domain: process.env.APP_DOMAIN,
statement: 'Please sign this message to confirm your identity.',
uri: process.env.ANGULAR_URL,
timeout: 60,
};
app.post('/request-message', async (req, res) => {
const { address, chain, network } = req.body;
try {
const message = await Moralis.Auth.requestMessage({
address,
chain,
network,
...config,
});
res.status(200).json(message);
} catch (error) {
res.status(400).json({ error: error.message });
console.error(error);
}
});
- We will create a
/verify
endpoint for verifying the signed message from the user. After the user successfully verifies, they will be redirected to the/user
page where their info will be displayed.
app.post('/verify', async (req, res) => {
try {
const { message, signature } = req.body;
const { address, profileId } = (
await Moralis.Auth.verify({
message,
signature,
networkType: 'evm',
})
).raw;
const user = { address, profileId, signature };
// create JWT token
const token = jwt.sign(user, process.env.AUTH_SECRET);
// set JWT cookie
res.cookie('jwt', token, {
httpOnly: true,
});
res.status(200).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
console.error(error);
}
});
- We will create an
/authenticate
endpoint for checking the JWT cookie we previously set to allow the user access to the/user
page:
app.get('/authenticate', async (req, res) => {
const token = req.cookies.jwt;
if (!token) return res.sendStatus(403); // if the user did not send a jwt token, they are unauthorized
try {
const data = jwt.verify(token, process.env.AUTH_SECRET);
res.json(data);
} catch {
return res.sendStatus(403);
}
});
- Lastly we will create a
/logout
endpoint for removing the cookie.
app.get('/logout', async (req, res) => {
try {
res.clearCookie('jwt');
return res.sendStatus(200);
} catch {
return res.sendStatus(403);
}
});
Your final index.js
should look like this:
const Moralis = require('moralis').default;
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const app = express();
const port = 3000;
app.use(express.json());
app.use(cookieParser());
// allow access to Angular app domain
app.use(
cors({
origin: process.env.ANGULAR_URL,
credentials: true,
})
);
const config = {
domain: process.env.APP_DOMAIN,
statement: 'Please sign this message to confirm your identity.',
uri: process.env.ANGULAR_URL,
timeout: 60,
};
// request message to be signed by client
app.post('/request-message', async (req, res) => {
const { address, chain, network } = req.body;
try {
const message = await Moralis.Auth.requestMessage({
address,
chain,
network,
...config,
});
res.status(200).json(message);
} catch (error) {
res.status(400).json({ error: error.message });
console.error(error);
}
});
// verify message signed by client
app.post('/verify', async (req, res) => {
try {
const { message, signature } = req.body;
const { address, profileId } = (
await Moralis.Auth.verify({
message,
signature,
networkType: 'evm',
})
).raw;
const user = { address, profileId, signature };
// create JWT token
const token = jwt.sign(user, process.env.AUTH_SECRET);
// set JWT cookie
res.cookie('jwt', token, {
httpOnly: true,
});
res.status(200).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
console.error(error);
}
});
// verify JWT cookie to allow access
app.get('/authenticate', async (req, res) => {
const token = req.cookies.jwt;
if (!token) return res.sendStatus(403); // if the user did not send a jwt token, they are unauthorized
try {
const data = jwt.verify(token, process.env.AUTH_SECRET);
res.json(data);
} catch {
return res.sendStatus(403);
}
});
// remove JWT cookie
app.get('/logout', async (req, res) => {
try {
res.clearCookie('jwt');
return res.sendStatus(200);
} catch {
return res.sendStatus(403);
}
});
const startServer = async () => {
await Moralis.start({
apiKey: process.env.MORALIS_API_KEY,
});
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
};
startServer();
- Run
npm run start
to make sure your server runs without immediate errors.
Bringing It All Together​
Now we will finish setting up our Angular pages to integrate with our server.
- Open
src/app/signin/signin.component.ts
. Add our required imports:
// for navigating to other routes
import { Router } from '@angular/router';
// for making HTTP requests
import axios from 'axios';
import { getDefaultProvider } from 'ethers';
import {
createClient,
connect,
disconnect,
getAccount,
signMessage,
InjectedConnector,
} from '@wagmi/core';
import { environment } from '../../environments/environment';
- Add this code to set up the Wagmi client:
const client = createClient({
autoConnect: true,
provider: getDefaultProvider(),
});
- Replace our empty
handleAuth()
function with the following:
async handleAuth() {
const { isConnected } = getAccount();
if (isConnected) await disconnect(); //disconnects the web3 provider if it's already active
const provider = await connect({ connector: new InjectedConnector() }); // enabling the web3 provider metamask
const userData = {
address: provider.account,
chain: provider.chain.id,
network: 'evm',
};
const { data } = await axios.post(
`${environment.SERVER_URL}/request-message`,
userData
);
const message = data.message;
const signature = await signMessage({ message });
await axios.post(
`${environment.SERVER_URL}/verify`,
{
message,
signature,
},
{ withCredentials: true } // set cookie from Express server
);
// redirect to /user
this.router.navigateByUrl('/user');
}
- Open
src/app/user/user.component.ts
. Add our required imports:
import { Router } from '@angular/router';
import axios from 'axios';
import { environment } from '../../environments/environment';
- Replace
ngOnInit(): void {}
with:
async ngOnInit() {
try {
const { data } = await axios.get(
`${environment.SERVER_URL}/authenticate`,
{
withCredentials: true,
}
);
const { iat, ...authData } = data; // remove unimportant iat value
this.session = JSON.stringify(authData, null, 2); // format to be displayed nicely
} catch (err) {
// if user does not have a "session" token, redirect to /signin
this.router.navigateByUrl('/signin');
}
}
- Replace our empty
signOut()
function with the following:
async signOut() {
await axios.get(`${environment.SERVER_URL}/logout`, {
withCredentials: true,
});
this.router.navigateByUrl('/signin');
}
If you get errors related to default imports, open your tsconfig.app.json
file and add "allowSyntheticDefaultImports": true
under compilerOptions
:
"compilerOptions": {
 "allowSyntheticDefaultImports": true,
 "outDir": "./out-tsc/app",
 "types": []
}
Testing the MetaMask Wallet Connector​
Visit http://localhost:4200/signin
to test the authentication.
- Click on the
Authenticate via MetaMask
button:
- Connect the MetaMask wallet and sign the message:
- After successful authentication, you will be redirected to the
/user
page:
- When a user authenticates, we show the user's info on the page.
- When a user is not authenticated, we redirect to the
/signin
page. - When a user is authenticated, we show the user's info on the page, even refreshing after the page.