Setting up the 1st factor
caution
- SuperTokens is not yet optimised for 2FA implementation, so you have to add a lot of customisations for it to work. We are working on improving the development experience for 2FA as well as adding more factors like TOPT. Stay tuned.
- A demo app that uses the pre built UI can be found on our GitHub.
#
1) InitialisationStart by following the recipe guide for the first factor. In this guide, we will take the example of thirdpartyemailpassword recipe as being the first factor.
After following the backend quick setup section (or any of the framework specific integration guides), you should have all the auth APIs exposed to the frontend via the SuperTokens middleware. The supertokens.init
code on the server would look like this:
- NodeJS
- GoLang
- Python
- Express
- Hapi
- Fastify
- Koa
- Loopback
- Serverless
- Next.js
- Nest.js
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import ThirdPartyEmailPassword from"supertokens-node/recipe/thirdpartyemailpassword";
supertokens.init({
framework: "express",
supertokens: {
connectionURI: "",
apiKey: "",
},
appInfo: {
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
ThirdPartyEmailPassword.init({/*...*/}),
Session.init(), // initializes session features
UserMetadata.init() // initializes the user metadata feature
]
});
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import ThirdPartyEmailPassword from"supertokens-node/recipe/thirdpartyemailpassword";
supertokens.init({
framework: "hapi",
supertokens: {
connectionURI: "",
apiKey: "",
},
appInfo: {
// learn more about this on https://supertokens.com/docs/session/appinfo
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
ThirdPartyEmailPassword.init({/*...*/}),
Session.init(), // initializes session features
UserMetadata.init() // initializes the user metadata feature
]
});
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import ThirdPartyEmailPassword from"supertokens-node/recipe/thirdpartyemailpassword";
supertokens.init({
framework: "fastify",
supertokens: {
connectionURI: "",
apiKey: "",
},
appInfo: {
// learn more about this on https://supertokens.com/docs/session/appinfo
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
ThirdPartyEmailPassword.init({/*...*/}),
Session.init(), // initializes session features
UserMetadata.init() // initializes the user metadata feature
]
});
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import ThirdPartyEmailPassword from"supertokens-node/recipe/thirdpartyemailpassword";
supertokens.init({
framework: "koa",
supertokens: {
connectionURI: "",
apiKey: "",
},
appInfo: {
// learn more about this on https://supertokens.com/docs/session/appinfo
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
ThirdPartyEmailPassword.init({/*...*/}),
Session.init(), // initializes session features
UserMetadata.init() // initializes the user metadata feature
]
});
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import ThirdPartyEmailPassword from"supertokens-node/recipe/thirdpartyemailpassword";
supertokens.init({
framework: "loopback",
supertokens: {
connectionURI: "",
apiKey: "",
},
appInfo: {
// learn more about this on https://supertokens.com/docs/session/appinfo
appName: "<YOUR_APP_NAME>",
apiDomain: "<YOUR_API_DOMAIN>",
websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
apiBasePath: "/auth",
websiteBasePath: "/auth"
},
recipeList: [
ThirdPartyEmailPassword.init({/*...*/}),
Session.init(), // initializes session features
UserMetadata.init() // initializes the user metadata feature
]
});
important
Please refer the Serverless Deployment section in the ThirdPartyEmailPassword recipe guide
important
Please refer the NextJS section in the ThirdPartyEmailPassword recipe guide
important
Please refer the NestJS section in the ThirdPartyEmailPassword recipe guide
import (
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/thirdpartyemailpassword"
"github.com/supertokens/supertokens-golang/recipe/thirdpartyemailpassword/tpepmodels"
"github.com/supertokens/supertokens-golang/supertokens"
"github.com/supertokens/supertokens-golang/recipe/usermetadata"
)
func main() {
apiBasePath := "/auth"
websiteBasePath := "/auth"
err := supertokens.Init(supertokens.TypeInput{
Supertokens: &supertokens.ConnectionInfo{
ConnectionURI: "",
APIKey: "",
},
AppInfo: supertokens.AppInfo{
AppName: "<YOUR_APP_NAME>",
APIDomain: "<YOUR_API_DOMAIN>",
WebsiteDomain: "<YOUR_WEBSITE_DOMAIN>",
APIBasePath: &apiBasePath,
WebsiteBasePath: &websiteBasePath,
},
RecipeList: []supertokens.Recipe{
thirdpartyemailpassword.Init(&tpepmodels.TypeInput{/*...*/}),
session.Init(nil), // initializes session features
usermetadata.Init(nil), // initializes the user metadata feature
},
})
if err != nil {
panic(err.Error())
}
}
- FastAPI
- Flask
- Django
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import thirdpartyemailpassword, session, usermetadata
init(
app_info=InputAppInfo(
app_name="<YOUR_APP_NAME>",
api_domain="<YOUR_API_DOMAIN>",
website_domain="<YOUR_WEBSITE_DOMAIN>",
api_base_path="/auth",
website_base_path="/auth"
),
supertokens_config=SupertokensConfig(
connection_uri="",
api_key=""
),
framework='fastapi',
recipe_list=[
session.init(), # initializes session features
usermetadata.init(), # initializes the user metadata feature
thirdpartyemailpassword.init(
# ...
)
],
mode='asgi' # use wsgi if you are running using gunicorn
)
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import thirdpartyemailpassword, session, usermetadata
init(
app_info=InputAppInfo(
app_name="<YOUR_APP_NAME>",
api_domain="<YOUR_API_DOMAIN>",
website_domain="<YOUR_WEBSITE_DOMAIN>",
api_base_path="/auth",
website_base_path="/auth"
),
supertokens_config=SupertokensConfig(
connection_uri="",
api_key=""
),
framework='flask',
recipe_list=[
session.init(), # initializes session features
usermetadata.init(), # initializes the user metadata feature
thirdpartyemailpassword.init(
# ...
)
]
)
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import thirdpartyemailpassword, session, usermetadata
init(
app_info=InputAppInfo(
app_name="<YOUR_APP_NAME>",
api_domain="<YOUR_API_DOMAIN>",
website_domain="<YOUR_WEBSITE_DOMAIN>",
api_base_path="/auth",
website_base_path="/auth"
),
supertokens_config=SupertokensConfig(
connection_uri="",
api_key=""
),
framework='django',
recipe_list=[
session.init(), # initializes session features
usermetadata.init(), # initializes the user metadata feature
thirdpartyemailpassword.init(
# ...
)
],
mode='asgi' # use wsgi if you are running django server in sync mode
)
important
You should have also added the SuperTokens middleware
and errorHandler
(depending on your framework) to your application. We are not showing it in the above code snippet for brevity, but it is explained in the ThirdPartyEmailPassword recipe guide.
#
2) Adding SecondFactorClaimAfter sign up or sign in of the first factor, the existence of the session signifies the completion of the first factor, but we want to explicitly mark the second factor as incomplete. This can be done by overriding the createNewSession
function in the Session.init
config:
- NodeJS
- GoLang
- Python
import Session from "supertokens-node/recipe/session";
import UserMetadata from "supertokens-node/recipe/usermetadata";
import Passwordless from "supertokens-node/recipe/passwordless";
import { BooleanClaim } from "supertokens-node/recipe/session/claims";
/*
This will be used to modify the session's access token payload
to add {"2fa-completed": false} into it.
*/
export const SecondFactorClaim = new BooleanClaim({
fetchValue: () => false,
key: "2fa-completed",
});
Session.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
/* This function is called after signing in or signing up via the first factor */
createNewSession: async function (input) {
return originalImplementation.createNewSession({
...input,
accessTokenPayload: {
...input.accessTokenPayload,
...(await SecondFactorClaim.build(input.userId, input.userContext)),
},
});
},
};
},
},
})
import (
"net/http"
"github.com/supertokens/supertokens-golang/recipe/session"
"github.com/supertokens/supertokens-golang/recipe/session/claims"
"github.com/supertokens/supertokens-golang/recipe/session/sessmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func main() {
SecondFactorClaim, _ := claims.BooleanClaim("2fa-completed", func(userId string, userContext supertokens.UserContext) (interface{}, error) {
return false, nil
}, nil)
session.Init(&sessmodels.TypeInput{
Override: &sessmodels.OverrideStruct{
Functions: func(originalImplementation sessmodels.RecipeInterface) sessmodels.RecipeInterface {
oCreateNewSession := *originalImplementation.CreateNewSession
/* This function is called after signing in or signing up via the first factor */
nCreateNewSession := func(res http.ResponseWriter, userID string, accessTokenPayload map[string]interface{}, sessionData map[string]interface{}, userContext supertokens.UserContext) (sessmodels.SessionContainer, error) {
if accessTokenPayload == nil {
accessTokenPayload = map[string]interface{}{}
}
accessTokenPayload, err := SecondFactorClaim.Build(userID, accessTokenPayload, userContext)
if err != nil {
return nil, err
}
return oCreateNewSession(res, userID, accessTokenPayload, sessionData, userContext)
}
*originalImplementation.CreateNewSession = nCreateNewSession
return originalImplementation
},
},
})
}
from supertokens_python.recipe import session
from supertokens_python.recipe.session.claims import BooleanClaim
from supertokens_python.recipe.session.interfaces import RecipeInterface
from typing import Any, Dict, Union
# This will be used to modify the session's access token payload
# to add {"2fa-completed": false} into it.
SecondFactorClaim = BooleanClaim(
key="2fa-completed", fetch_value=lambda _, __: False)
def override_session_functions(original_implementation: RecipeInterface):
original_create_new_session = original_implementation.create_new_session
async def create_new_session(
request: Any,
user_id: str,
access_token_payload: Union[None, Dict[str, Any]],
session_data: Union[None, Dict[str, Any]],
user_context: Dict[str, Any],
):
# This function is called after signing in or signing up via the first factor
if access_token_payload is None:
access_token_payload = {}
access_token_payload = {**access_token_payload, **(await SecondFactorClaim.build(user_id, user_context))}
return await original_create_new_session(
request, user_id, access_token_payload, session_data, user_context
)
original_implementation.create_new_session = create_new_session
return original_implementation
session.init(
override=session.InputOverrideConfig(functions=override_session_functions)
)
We add SecondFactorClaim
into the access token payload. This will be set to false on session creation (see fetchValue
in the claim definition).