Auth requirements are quite varied. Therefore any auth solution must provide the ability to customise their APIs. Each solution uses its own terminology for this feature:
These features allow you to change the default behaviour of the auth APIs by:
How powerful these solutions are, depends on:
In this article, we will be talking about how to customise the auth APIs provided by SuperTokens using its “Override” feature. In order to understand that, we must first understand how SuperTokens fits within an app.
Here we can see the architecture diagram for the self-hosted version of SuperTokens. On the left, we have the client (browser, mobile app) which talks to your APIs. Your API layer has your application APIs (shown as /api1/, /api2/, ..) and also APIs automatically exposed by the SuperTokens backend SDKs via our middleware function (shown as /auth/signin, /auth/signout, ...).
The SuperTokens APIs talk to the SuperTokens Core (HTTP microservice) to persist data in the database. Your application APIs can also talk to the core if needed.
Keeping this in mind, the concept of override is that you can change the behaviour of the SuperTokens APIs (exposed to the frontend) as per your requirements (all within your API layer, in the language you already use). Think of this being similar to overrides in object-oriented programming where you have an original implementation, and you can modify its behaviour by overriding the existing functions. You can even call the “super” class implementation of that function in your override function.
Whilst this article is focused on a NodeJS backend, the concepts here are very similar to all the other backend SDKs provided by SuperTokens.
To override the default implementation, we must use the override config value when calling supertokens.init. Each recipe inside the recipeList, accepts an override config that can be used to change the behaviour of that recipe:
supertokens.init({
appInfo: {...},
recipeList: [
EmailPassword.init({
override: {
functions: (originalImplementation) => {
return originalImplementation
},
apis: (originalImplementation) => {
return originalImplementation
}
}
}),
Session.init({...})
]
})
In the above, we have defined the skeleton code for how to override the behaviour of the EmailPassword recipe. A very similar skeleton is applicable for overriding the Session (or any other) recipe.
There are two types of override:
You always want to try and use the override.functionsconfig since that will make the minimum change to the default behaviour. If the inputs to those functions don’t suffice for your use case, then you should override the APIs.
In both these types of overrides, they accept the originalImplementation variable as an input and the return is an object that has the same type as the originalImplementaion.
For EmailPassword recipe, the originalImplementationobject contains:
For Session recipe, the originalImplementationobject contains:
In the code snippet above, we are not modifying the default behaviour of any of these functions since we are simply returning the originalImplementation object. If you want to modify the signIn function, then we can do so like this:
EmailPassword.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
signIn: async function (input) {
let email = input.email;
let password = input.password;
// TODO: some custom logic before
let originalResponse = await originalImplementation.signIn(input);
// TODO: some custom logic after
return originalResponse;
}
}
},
apis: (originalImplementation) => {
return {
...originalImplementation
}
}
}
})
In the above code snippet, we have provided a custom signIn function that uses the original implementation’s signIn function. As marked above (in TODOcomments), we can write custom logic before or after calling the original implementation.
If we wish, we can even avoid calling the original implementation entirely and define our own logic. For example, if we wanted to use a different password hashing algorithm that is not supported by SuperTokens.
Sometimes, you may want to modify the default API to:
The function signature of all the API interface functions has an optionsparameter that contains the original request and response objects. You can read from the request object and write to the response object as you normally would in your own APIs.
For example, if you want to read the request’s origin header during the sign up API, you can do it as follows:
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signUpPOST: async function (input) {
if (originalImplementation.signUpPOST === undefined) {
throw new Error("should never come here.");
}
let origin = input.options.req.getHeaderValue("origin");
// TODO: do something with the origin
return originalImplementation.signUpPOST(input);
}
}
}
}
})
As you can see above, we can access the request object using input.options.req.
Likewise, if we want to send a custom response to the frontend, we can access the response object via input.options.res.
Finally, to disable an API that we provide, you can set it to undefined as follows:
EmailPassword.init({
override: {
apis: (originalImplementation) => {
return {
...originalImplementation,
signUpPOST: undefined
}
}
}
})
This will disable the sign up API, and requests to /auth/signup will be passed along to your APIs or yield a 404.
In the post, we saw how we can use the Overrides feature to modify the behaviour of any of the auth APIs exposed by SuperTokens. Whilst this blog focuses on NodeJS, the concept is the same in all the other SDKs we provide.
Written by the Folks at SuperTokens — hope you enjoyed! We are always available on our Discord server. Join us if you have any questions or need any help.
First Published here