Introduction
Auth0 is a cloud service that provides a turn-key solution for authentication, authorization and user management. It is a feature-rich service that is highly customizable and can be used in a variety of ways. Auth0 is a great choice for a wide range of applications, from simple web apps to enterprise applications. It provides a great way to add authentication and authorization to your application without having to build it yourself, and has various integrations with services such as Google, Facebook, Twitter, and more. This along with its passwordless authentication and multi-factor authentication makes it a great choice for a wide range of applications.
How to use Auth0 authentication in your Amplication application
Setting up Auth0 authentication in your Amplication application is easy. You can use the Auth0 plugin to add the required dependencies and configuration files to your application. The steps are as follows:
Create a service in amplication
Start by creating a service within the Amplication platform. Once your service is set up, click on the Commit changes & build
button to initiate the build process. Merge the generated Pull Request to move ahead.
Add the NestJS Auth Module
Next, add the NestJS Auth Module to your service. You can do this by navigating to the Plugins section within your service topbar menu.
Also create and set an authentication entity if you have not done so yet. For more information on how to do this, see the Authentication section of the Amplication documentation.
Add the Auth0 plugin
Next, add the Auth0 plugin to your service. You can do this by navigating to the 'Plugins' section within your service sidebar menu, where you'll see a list of available plugins and installed plugins(see screenshot below for reference).
Note: You have to remove other auth plugins already there in the service by inspecting the installed plugins tab. ( Look out for the default JWT Auth Provider added automatically 😉 )
After installing the plugin you have to provide settings for the plugin. You can do this by clicking the settings
button next to the plugin name.
After this you can follow the instructions in the Plugin to configure your Auth0 account
To provide a summary of the steps:
- Create an Auth0 account
- Create an Auth0 application and API
- Configure the Auth0 application
- Configure the Auth0 plugin
The settings will look something like the following picture:
Then click the Save
button to save the settings and commit the changes.
Some things to note:
- The
domain
,clientId
,issuerURL
, andaudience
are required fields. These are the values you get from your Auth0 account.- The
emailFieldName
provided must be present in the authentication entity.defaultUser
will be used to create a default and new users.
Alternative: Automate setup of Auth0 account
If you hate having to setup everything manually, or just don't have access to the auth0 account, then you don't have to worry as I have got you covered. Introducing Auth0 Management API. Using this all the nifty work will be done for you. All you have to do is provide is a access token with necessary permisssions. You can also customise the names 🤖🚀.
For how to get the access token and the permissions required, see the Plugin Docs
After getting these you can add them to the plugin settings as shown below:
Then click the Save
button to save the settings and commit the changes. This will trigger a build and the plugin will do the rest for you.
Some things to note:
- If there are already actions and api with that name, the plugin will not create them again.
As you can see on this PR from our example repo, the plugin has created the actions and api for us. 🎉🎉🎉
How things work
Manual method :-
The plugin will create the following files for you as seen in this PR.
- Adds the required dependencies to the
package.json
file.
The @auth0/auth0-spa-js
and jwks-rsa
help in adding the authentication and authorization to the frontend and backend respectively. While react-router-dom
is used to add the routes to the frontend.
- Adds the required .env variables to the
.env
file used in the frontend and backend.
- Adds
ra-auth0-provider
to theAdmin
. This is used to setup Auth0 in the frontend. It provides theauthProvider
prop to theAdmin
component, and has requisitelogin
,logout
functions.
import { Auth0Client } from "@auth0/auth0-spa-js";
import { AuthProvider, UserIdentity } from "react-admin";
export const PreviousLocationStorageKey = "@react-admin/nextPathname";
export const client = new Auth0Client({
domain: process.env.REACT_APP_AUTH0_DOMAIN || "",
clientId: process.env.REACT_APP_AUTH0_CLIENT_ID || "",
cacheLocation: "localstorage",
authorizationParams: {
audience: process.env.REACT_APP_AUTH0_AUDIENCE,
scope: "openid profile email",
},
useRefreshTokens: true,
});
export const auth0AuthProvider: AuthProvider = {
login: async () => {
await client.loginWithPopup({
authorizationParams: {
redirect_uri: process.env.REACT_APP_AUTH0_REDIRECT_URI,
},
});
return Promise.resolve();
},
logout: async () => {
await client.logout({
logoutParams: {
returnTo: process.env.REACT_APP_AUTH0_LOGOUT_REDIRECT_URI,
},
});
return Promise.resolve();
},
async checkAuth() {
const isAuthenticated = await client.isAuthenticated();
if (isAuthenticated) {
return Promise.resolve();
}
localStorage.setItem(PreviousLocationStorageKey, window.location.href);
return Promise.reject();
},
checkError: async ({ status }) => {
if (status === 401 || status === 403) {
throw new Error("Unauthorized");
}
},
getPermissions: async () => {
if (!(await client.isAuthenticated())) {
return;
}
await client.getIdTokenClaims();
Promise.resolve();
},
getIdentity: async () => {
if (!(await client.isAuthenticated())) {
throw new Error("User not authenticated");
}
const user = await client.getUser();
if (!user) {
throw new Error("User not found");
}
return {
id: user.sub,
fullName: user.name,
avatar: user.picture,
email: user.email,
} as UserIdentity;
},
handleCallback: async () => {
const query = window.location.search;
if (query.includes("code=") && query.includes("state=")) {
try {
await client.handleRedirectCallback();
return;
} catch (error) {
throw new Error("Failed to handle login callback: " + error);
}
}
throw new Error("Failed to handle login callback.");
},
};
-
Adds the logic to get access token in graphql provider as seen in these lines
-
Adds custom login page to the admin app. This is done by changing the default Login.tsx file.
-
Adds the
JWT Base Strategy
to the backend. This checks the JWT token sent in the request header and verifies it using the JWKS key provided by Auth0. This is done by adding the following code to thesrc/auth/jwt/base/jwt.strategy.base.ts
file. And then validating it in the database usingvalidateBase
function.
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { passportJwtSecret } from "jwks-rsa";
import { ExtractJwt, Strategy } from "passport-jwt";
import { Auth0User } from "./User";
import { UserInfo } from "../../UserInfo";
import { UserService } from "src/user/user.service";
export class JwtStrategyBase extends PassportStrategy(Strategy) {
constructor(
protected readonly configService: ConfigService,
protected readonly userService: UserService
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Extract JWT from the Authorization header
audience: configService.get("AUTH0_AUDIENCE"), // The resource server where the JWT is processed
issuer: `${configService.get("AUTH0_ISSUER_URL")}`, // The issuing Auth0 server
algorithms: ["RS256"], // Asymmetric signing algorithm
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${configService.get(
"AUTH0_ISSUER_URL"
)}.well-known/jwks.json`,
}),
});
}
// Validate the received JWT and construct the user object out of the decoded token.
async validateBase(payload: { user: Auth0User }): Promise<UserInfo | null> {
const user = await this.userService.findOne({
where: {
email: payload.user.email,
},
});
return user ? { ...user, roles: user?.roles as string[] } : null;
}
}
- Adds the
JWT stratgey
code which is editable by users to the backend. This is done by adding the following code to thesrc/auth/jwt/jwt.strategy.ts
file. And then validating it in the database usingvalidate
function.
import { Injectable } from "@nestjs/common";
import { JwtStrategyBase } from "./base/jwt.strategy.base";
import { ConfigService } from "@nestjs/config";
import { Auth0User } from "./base/User";
import { IAuthStrategy } from "../IAuthStrategy";
import { UserInfo } from "../UserInfo";
import { UserService } from "src/user/user.service";
@Injectable()
export class JwtStrategy extends JwtStrategyBase implements IAuthStrategy {
constructor(
protected readonly configService: ConfigService,
protected readonly userService: UserService
) {
super(configService, userService);
}
async validate(payload: { user: Auth0User }): Promise<UserInfo> {
const validatedUser = await this.validateBase(payload);
// If the entity is valid, return it
if (validatedUser) {
return validatedUser;
}
// Otherwise, make a new entity and return it
const userFields = payload.user;
const defaultData = {
email: userFields.email,
name: userFields.name,
username: userFields.name,
roles: ["admin"],
};
const newUser = await this.userService.create({
data: defaultData,
});
return { ...newUser, roles: newUser?.roles as string[] };
}
}
In this code, if a user is not found in the database then a new user is created with the default roles as admin
. This can be changed by the user as per their requirements and needs.
Note :- Please make sure that the field names in the
defaultData
object are present in the authentication entity. Also make sure of the role you wish to assign to the user. In this case it isadmin
.
- Adds the
User
type to thesrc/auth/jwt/base/User.ts
file. This is used to get the user data from the JWT token.
export interface Auth0User {
nickname: string;
username: string;
name: string;
email: string;
email_verified: boolean;
picture: string;
}
The types of the fields in this interface can be changed as per the requirements of the user and generally varies from application to application. You can find more information about the fields here. However, the email
field is required as it is used to identify the user. Also, if you want some fields you may have to change the scope in the src/auth-provider/ra-auth-auth0.ts
in the frontend.
Customization - Add social connections
With Auth0 you can add social connections to your application. This allows users to login to your application using their social media accounts. This allows you to provide a more personalized, secure and passwordless experience for your users. You can add social connections to your application by following the steps below:
-
Go to the Auth0 Social Connections page. You will see various options out there as can be seen below.
- Here I am choosing GitHub however, you can choose any of the options available. The steps are nearly similar to each other.
- After choosing the option, you will be redirected to the configuration page for that option. Here you can configure the connection as per your requirements. You can also add custom scopes to the connection. For more information on how to do this, see the Marketplace documentation.
Note :- Make sure to add the
-
Click the
Create
button to create the connection. This will redirect you to the Connection setup page where you can configure on which apps you should add this. -
Now, the connections should be visible in the login page of your application. You can see the login page of the example application below.
Future Work
Make the plugin more customizable by adding more options to the plugin settings. This will allow the user to customize the plugin as per their requirements. Some of the options that can be added are:
- Authentication using phone number
- Passwordless authentication
- Two factor authentication
- Adding custom roles
If you have any other suggestions, please feel free create an issue at the Auth0 plugin repo
Conclusion
Amplication's Auth0 Plugin provides a powerful but effortless way to add authentication to your application. It is easy to use and can be configured in a few minutes thus reducing complexity overhead.
I hope this blog post was helpful to you. If you have any questions or suggestions, please feel free to reach out to me on Twitter, GitHub or LinkedIn. I would love to hear from you.