Changes to third party flow
We need to start by maintaining an allow list of emails. You can either store this list in your own database, or then use the metadata feature provided by SuperTokens to store this. This may seem like a strange use case of the user metadata recipe we provide, but it works.
You want to implement the following functions on your backend:
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import UserMetadata from "supertokens-node/recipe/usermetadata"
async function addEmailToAllowlist(email: string) {
let existingData = await UserMetadata.getUserMetadata("emailAllowList");
let allowList: string[] = existingData.metadata.allowList || [];
allowList = [...allowList, email];
await UserMetadata.updateUserMetadata("emailAllowList", {
allowList
});
}
async function isEmailAllowed(email: string) {
let existingData = await UserMetadata.getUserMetadata("emailAllowList");
let allowList: string[] = existingData.metadata.allowList || [];
return allowList.includes(email);
}
import "github.com/supertokens/supertokens-golang/recipe/usermetadata"
func addEmailToAllowlist(email string) error {
existingData, err := usermetadata.GetUserMetadata("emailAllowList")
if err != nil {
return err
}
allowList := []string{}
allowListFromMetadata, ok := existingData["allowList"].([]string)
if ok {
allowList = allowListFromMetadata
}
allowList = append(allowList, email)
_, err = usermetadata.UpdateUserMetadata("emailAllowList", map[string]interface{}{
"allowList": allowList,
})
return err
}
func isEmailAllowed(email string) (bool, error) {
existingData, err := usermetadata.GetUserMetadata("emailAllowList")
if err != nil {
return false, err
}
allowList := []string{}
allowListFromMetadata, ok := existingData["allowList"].([]string)
if ok {
allowList = allowListFromMetadata
}
for _, allowedEmail := range allowList {
if allowedEmail == email {
return true, nil
}
}
return false, nil
}
from supertokens_python.recipe.usermetadata.asyncio import get_user_metadata, update_user_metadata
from typing import List
async def add_email_to_allow_list(email: str):
metadataResult = await get_user_metadata("emailAllowList")
allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
allow_list.append(email)
await update_user_metadata("emailAllowList", {
"allowList": allow_list
})
async def is_email_allowed(email: str):
metadataResult = await get_user_metadata("emailAllowList")
allow_list: List[str] = metadataResult.metadata["allowList"] if "allowList" in metadataResult.metadata else []
return email in allow_list
important
Remember to initialise the user metadata recipe on the backend recipeList
during supertokens.init
.
Multi Tenancy
For a multi tenant setup, you can even store an allow list per tenant. This would allow you to limit sign ups for different emails for different tenants. If you are doing this, then you would also need to pass in the tenantID to the functions above, which you can obtain from the input to the api overrides shown below.
After that, we override the signInUpPOST
API and the signInUp
recipe function to check if the input email is allowed durign sign up. If not allowed, we send back a user friendly message to the frontend.
- NodeJS
- GoLang
- Python
- Other Frameworks
Important
import ThirdParty from "supertokens-node/recipe/thirdparty";
import supertokens from "supertokens-node";
ThirdParty.init({
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
signInUp: async function (input) {
let existingUsers = await supertokens.listUsersByAccountInfo(input.tenantId, {
email: input.email
});
if (existingUsers.length === 0) {
// this means that the email is new and is a sign up
if (!(await isEmailAllowed(input.email))) {
// email is not in allow list, so we disallow
throw new Error("No sign up")
}
}
// We allow the sign in / up operation
return originalImplementation.signInUp(input);
}
}
},
apis: (originalImplementation) => {
return {
...originalImplementation,
signInUpPOST: async function (input) {
try {
return await originalImplementation.signInUpPOST!(input);
} catch (err: any) {
if (err.message === "No sign up") {
// this error was thrown from our function override above.
// so we send a useful message to the user
return {
status: "GENERAL_ERROR",
message: "Sign ups are disabled. Please contact the admin."
}
}
throw err;
}
}
}
}
}
})
import (
"errors"
"github.com/supertokens/supertokens-golang/recipe/thirdparty"
"github.com/supertokens/supertokens-golang/recipe/thirdparty/tpmodels"
"github.com/supertokens/supertokens-golang/supertokens"
)
func isEmailAllowed(email string) (bool, error) {
// ... from previous code snippet
return false, nil
}
func main() {
thirdparty.Init(&tpmodels.TypeInput{
Override: &tpmodels.OverrideStruct{
Functions: func(originalImplementation tpmodels.RecipeInterface) tpmodels.RecipeInterface {
ogSignInUp := *originalImplementation.SignInUp
(*originalImplementation.SignInUp) = func(thirdPartyID, thirdPartyUserID, email string, oAuthTokens map[string]interface{}, rawUserInfoFromProvider tpmodels.TypeRawUserInfoFromProvider, tenantId string, userContext supertokens.UserContext) (tpmodels.SignInUpResponse, error) {
existingUsers, err := thirdparty.GetUsersByEmail(tenantId, email)
if err != nil {
return tpmodels.SignInUpResponse{}, err
}
if len(existingUsers) == 0 {
// this means that the email is new and is a sign up
allowed, err := isEmailAllowed(email)
if err != nil {
return tpmodels.SignInUpResponse{}, err
}
if !allowed {
return tpmodels.SignInUpResponse{}, errors.New("No sign up")
}
}
// We allow the sign in / up operation
return ogSignInUp(thirdPartyID, thirdPartyUserID, email, oAuthTokens, rawUserInfoFromProvider, tenantId, userContext)
}
return originalImplementation
},
APIs: func(originalImplementation tpmodels.APIInterface) tpmodels.APIInterface {
originalSignInUpPOST := *originalImplementation.SignInUpPOST
(*originalImplementation.SignInUpPOST) = func(provider *tpmodels.TypeProvider, input tpmodels.TypeSignInUpInput, tenantId string, options tpmodels.APIOptions, userContext supertokens.UserContext) (tpmodels.SignInUpPOSTResponse, error) {
resp, err := originalSignInUpPOST(provider, input, tenantId, options, userContext)
if err.Error() == "No sign up" {
// this error was thrown from our function override above.
// so we send a useful message to the user
return tpmodels.SignInUpPOSTResponse{
GeneralError: &supertokens.GeneralErrorResponse{
Message: "Sign ups are disabled. Please contact the admin.",
},
}, nil
}
return resp, err
}
return originalImplementation
},
},
})
}
from supertokens_python import init, InputAppInfo
from supertokens_python.types import GeneralErrorResponse
from supertokens_python.recipe import thirdparty
from supertokens_python.recipe.thirdparty.asyncio import get_users_by_email
from supertokens_python.recipe.thirdparty.interfaces import APIInterface, RecipeInterface, SignInUpOkResult, APIOptions
from typing import Optional, Dict, Any
from supertokens_python.recipe.thirdparty.provider import Provider, RedirectUriInfo
from supertokens_python.recipe.thirdparty.types import RawUserInfoFromProvider
async def is_email_allowed(email: str):
# from previous code snippet..
return False
def override_thirdparty_functions(original_implementation: RecipeInterface):
original_sign_in_up = original_implementation.sign_in_up
async def sign_in_up(
third_party_id: str,
third_party_user_id: str,
email: str,
oauth_tokens: Dict[str, Any],
raw_user_info_from_provider: RawUserInfoFromProvider,
tenant_id: str,
user_context: Dict[str, Any]
) -> SignInUpOkResult:
existing_users = await get_users_by_email(email, tenant_id, user_context)
if (len(existing_users) == 0):
if not await is_email_allowed(email):
raise Exception("No sign up")
# this means this email is new so we allow sign up
return await original_sign_in_up(third_party_id, third_party_user_id, email, oauth_tokens, raw_user_info_from_provider, tenant_id, user_context)
raise Exception("No sign up")
original_implementation.sign_in_up = sign_in_up
return original_implementation
def override_thirdparty_apis(original_implementation: APIInterface):
original_sign_in_up_post = original_implementation.sign_in_up_post
async def sign_in_up_post(
provider: Provider,
redirect_uri_info: Optional[RedirectUriInfo],
oauth_tokens: Optional[Dict[str, Any]],
tenant_id: str,
api_options: APIOptions,
user_context: Dict[str, Any]
):
try:
return await original_sign_in_up_post(provider, redirect_uri_info, oauth_tokens, tenant_id, api_options, user_context)
except Exception as e:
if str(e) == "No sign up":
return GeneralErrorResponse("Seems like you already have an account with another method. Please use that instead.")
raise e
original_implementation.sign_in_up_post = sign_in_up_post
return original_implementation
init(
app_info=InputAppInfo(
api_domain="...", app_name="...", website_domain="..."),
framework='...',
recipe_list=[
thirdparty.init(
override=thirdparty.InputOverrideConfig(
apis=override_thirdparty_apis,
functions=override_thirdparty_functions
),
)
]
)
signInUpPOST
is called when the user is redirected to the app from the third party provider post login. The API calls the signInUp
recipe function in which we check:
- If there exists a user with the input email, it means they are signing in and so we allow the operation.
- Otherwise, we check if the input email is allowed by calling our
isEmailAllowed
function (which we implemented above). If not allowed, we throw an error with a custom message. - Finally, we override the
signInUpPOST
API to catch this custom error and return a message to the frontend which will be displayed to the user.
We can add emails to the allow list by calling the addEmailToAllowlist
function we implemented above.