Skip to content

Commit

Permalink
docs: update with-phone-password to use MFA
Browse files Browse the repository at this point in the history
  • Loading branch information
porcellus committed Dec 14, 2023
1 parent ae18961 commit 322d83d
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 257 deletions.
142 changes: 41 additions & 101 deletions examples/with-phone-password/api-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { verifySession } from "supertokens-node/recipe/session/framework/express
import { middleware, errorHandler, SessionRequest } from "supertokens-node/framework/express";
import EmailPassword from "supertokens-node/recipe/emailpassword";
import Passwordless from "supertokens-node/recipe/passwordless";
import MultiFactorAuth from "supertokens-node/recipe/multifactorauth";
import parsePhoneNumber from "libphonenumber-js/max";
import { PhoneVerifiedClaim } from "./phoneVerifiedClaim";
import Dashboard from "supertokens-node/recipe/dashboard";
require("dotenv").config();

Expand All @@ -20,7 +20,7 @@ supertokens.init({
framework: "express",
supertokens: {
// TODO: This is a core hosted for demo purposes. You can use this, but make sure to change it to your core instance URI eventually.
connectionURI: "https://try.supertokens.com",
connectionURI: "http://localhost:3567",
apiKey: "<REQUIRED FOR MANAGED SERVICE, ELSE YOU CAN REMOVE THIS FIELD>",
},
appInfo: {
Expand Down Expand Up @@ -106,106 +106,46 @@ supertokens.init({
contactMethod: "PHONE",
flowType: "USER_INPUT_CODE",
override: {
apis: (oI) => {
return {
...oI,
createCodePOST: async function (input) {
if (oI.createCodePOST === undefined) {
throw new Error("Should never come here");
}
/**
*
* We want to make sure that the OTP being generated is for the
* same number that was used in the first login challenge. Otherwise
* someone could "hack" the frontend to change the phone number
* being sent for the second login challenge.
*/

let session = await Session.getSession(input.options.req, input.options.res, {
overrideGlobalClaimValidators: () => [],
});
if (session === undefined) {
throw new Error("Should never come here");
}

let phoneNumber: string = session.getAccessTokenPayload().phoneNumber;

if (!("phoneNumber" in input) || input.phoneNumber !== phoneNumber) {
throw new Error("Should never come here");
}

return oI.createCodePOST(input);
},
consumeCodePOST: async function (input) {
if (oI.consumeCodePOST === undefined) {
throw new Error("Should never come here");
}
// we should already have a session here since this is called
// after phone password login
let session = await Session.getSession(input.options.req, input.options.res, {
overrideGlobalClaimValidators: () => [],
});
if (session === undefined) {
throw new Error("Should never come here");
}

// we add the session to the user context so that the createNewSession
// function doesn't create a new session
input.userContext.session = session;
let resp = await oI.consumeCodePOST(input);

if (resp.status === "OK") {
// OTP verification was successful. We can now mark the
// session's payload as PhoneVerifiedClaim: true so that
// the user has access to API routes and the frontend UI
await session.setClaimValue(PhoneVerifiedClaim, true, input.userContext);
resp.user = (await supertokens.getUser(session.getUserId()))!;
}

return resp;
},
};
},
apis: (oI) => ({
...oI,
consumeCodePOST: async (input) => {
const resp = await oI.consumeCodePOST!(input);
if (resp.status === "OK") {
// We can this here without any additional checks, since we know that this is only used as a secondary factor
// with exactly this (phone + otp) config
await MultiFactorAuth.addToDefaultRequiredFactorsForUser(resp.user.id, "otp-phone");
}
return resp;
},
}),
},
}),
Session.init({
Session.init(),
MultiFactorAuth.init({
firstFactors: ["emailpassword"],
override: {
functions: (originalImplementation) => {
return {
...originalImplementation,
getGlobalClaimValidators: (input) => [
...input.claimValidatorsAddedByOtherRecipes,
PhoneVerifiedClaim.validators.hasValue(true),
],
createNewSession: async function (input) {
if (input.userContext.session !== undefined) {
// if it comes here, it means that we already have an
// existing session
return input.userContext.session;
} else {
// this is via phone number and password login. The user
// still needs to verify the phone number via an OTP
functions: (oI) => ({
...oI,
getMFARequirementsForAuth(input) {
if (!input.defaultRequiredFactorIdsForUser.includes("otp-phone")) {
return ["otp-phone"];
}
return [];
},
}),
apis: (oI) => ({
...oI,
mfaInfoGET: async (input) => {
const resp = await oI.mfaInfoGET(input);

// we also get the phone number of the user and save it in the
// session so that the OTP can be sent to it directly
let userInfo = await supertokens.getUser(input.userId, input.userContext);
return originalImplementation.createNewSession({
...input,
accessTokenPayload: {
...input.accessTokenPayload,
...PhoneVerifiedClaim.build(
input.userId,
input.recipeUserId,
input.tenantId,
input.userContext
),
phoneNumber: userInfo?.emails[0],
},
});
}
},
};
},
if (resp.status === "OK") {
resp.phoneNumber = resp.email;
// We want to remove "otp-email" and add "otp-phone", but it's simpler to just replace the array
resp.factors.isAlreadySetup = ["otp-phone"];
}
return resp;
},
}),
},
}),
Dashboard.init(),
Expand All @@ -223,10 +163,10 @@ app.use(
})
);

app.use(middleware());
app.use((middleware as any)());

// An example API that requires session verification
app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => {
app.get("/sessioninfo", (verifySession as any)(), async (req: any, res) => {
let session = req.session!;
res.send({
sessionHandle: session.getHandle(),
Expand All @@ -235,7 +175,7 @@ app.get("/sessioninfo", verifySession(), async (req: SessionRequest, res) => {
});
});

app.use(errorHandler());
app.use((errorHandler as any)());

app.use((err: any, req: any, res: any, next: any) => {
console.log(err);
Expand Down
6 changes: 0 additions & 6 deletions examples/with-phone-password/api-server/phoneVerifiedClaim.ts

This file was deleted.

4 changes: 2 additions & 2 deletions examples/with-phone-password/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"react-dom": "^18.1.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"supertokens-auth-react": "latest",
"supertokens-node": "latest",
"supertokens-auth-react": "github:supertokens/supertokens-auth-react#feat/mfa/tests",
"supertokens-node": "github:supertokens/supertokens-node#mfa-impl",
"ts-node-dev": "^2.0.0",
"typescript": "^4.6.4",
"web-vitals": "^2.1.4"
Expand Down
85 changes: 32 additions & 53 deletions examples/with-phone-password/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import EmailPassword from "supertokens-auth-react/recipe/emailpassword";
import Passwordless, { PasswordlessComponentsOverrideProvider } from "supertokens-auth-react/recipe/passwordless";
import MultiFactorAuth from "supertokens-auth-react/recipe/multifactorauth";
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import { EmailPasswordPreBuiltUI } from "supertokens-auth-react/recipe/emailpassword/prebuiltui";
import { MultiFactorAuthPreBuiltUI } from "supertokens-auth-react/recipe/multifactorauth/prebuiltui";
import Session, { SessionAuth } from "supertokens-auth-react/recipe/session";
import { BrowserRouter as Router, Routes, Route, useLocation } from "react-router-dom";
import * as reactRouterDom from "react-router-dom";
import Home from "./Home";
import Footer from "./Footer";
import SessionExpiredPopup from "./SessionExpiredPopup";
import PhoneVerification from "./PhoneVerification";
import PhoneNumberVerificationFooter from "./PhoneVerification/Footer";
import { PhoneVerifiedClaim } from "./phoneVerifiedClaim";

export function getApiDomain() {
const apiPort = process.env.REACT_APP_API_PORT || 3001;
Expand Down Expand Up @@ -77,15 +76,9 @@ SuperTokens.init({
},
},
}),
Session.init({
override: {
functions: (oI) => ({
...oI,
getGlobalClaimValidators: ({ claimValidatorsAddedByOtherRecipes }) => {
return [...claimValidatorsAddedByOtherRecipes, PhoneVerifiedClaim.validators.isTrue()];
},
}),
},
Session.init(),
MultiFactorAuth.init({
firstFactors: ["emailpassword"],
}),
],
});
Expand All @@ -105,48 +98,34 @@ function App() {

return (
<SuperTokensWrapper key={key}>
<PasswordlessComponentsOverrideProvider
components={{
PasswordlessUserInputCodeFormFooter_Override: ({ ...props }) => {
return <PhoneNumberVerificationFooter recipeImplementation={props.recipeImplementation} />;
},
}}>
<div className="App">
<div className="fill">
<Routes>
<Route
path="/auth/verify-phone"
element={
<SessionAuth>
<PhoneVerification />
</SessionAuth>
}
/>
{/* This shows the login UI on "/auth" route */}
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [
PasswordlessPreBuiltUI,
EmailPasswordPreBuiltUI,
])}
<Route
path="/"
element={
/* This protects the "/" route so that it shows
<Home /> only if the user is logged in.
Else it redirects the user to "/auth" */
<SessionAuth
onSessionExpired={() => {
updateShowSessionExpiredPopup(true);
}}>
<Home />
{showSessionExpiredPopup && <SessionExpiredPopup />}
</SessionAuth>
}
/>
</Routes>
</div>
<Footer />
<div className="App">
<div className="fill">
<Routes>
{/* This shows the login UI on "/auth" route */}
{getSuperTokensRoutesForReactRouterDom(reactRouterDom, [
PasswordlessPreBuiltUI,
EmailPasswordPreBuiltUI,
MultiFactorAuthPreBuiltUI,
])}
<Route
path="/"
element={
/* This protects the "/" route so that it shows
<Home /> only if the user is logged in.
Else it redirects the user to "/auth" */
<SessionAuth
onSessionExpired={() => {
updateShowSessionExpiredPopup(true);
}}>
<Home />
{showSessionExpiredPopup && <SessionExpiredPopup />}
</SessionAuth>
}
/>
</Routes>
</div>
</PasswordlessComponentsOverrideProvider>
<Footer />
</div>
</SuperTokensWrapper>
);
}
Expand Down
1 change: 0 additions & 1 deletion examples/with-phone-password/src/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import SuccessView from "./SuccessView";
import { useSessionContext } from "supertokens-auth-react/recipe/session";
import { useNavigate } from "react-router-dom";
import { signOut } from "supertokens-auth-react/recipe/emailpassword";
import { PhoneVerifiedClaim } from "../phoneVerifiedClaim";

export default function Home() {
const session = useSessionContext();
Expand Down
29 changes: 0 additions & 29 deletions examples/with-phone-password/src/PhoneVerification/Footer.tsx

This file was deleted.

Loading

0 comments on commit 322d83d

Please sign in to comment.