@@ -27,9 +33,7 @@ export default function Home({ maybeSecretCode }: Props) {
diff --git a/playground/connect-next/app/home/page.tsx b/playground/connect-next/app/(auth-required)/home/page.tsx
similarity index 81%
rename from playground/connect-next/app/home/page.tsx
rename to playground/connect-next/app/(auth-required)/home/page.tsx
index 9a28dbb27..cbf7e565d 100644
--- a/playground/connect-next/app/home/page.tsx
+++ b/playground/connect-next/app/(auth-required)/home/page.tsx
@@ -1,5 +1,5 @@
import { cookies } from 'next/headers';
-import Home from '@/app/home/client';
+import Home from '@/app/(auth-required)/home/client';
export default async function Page() {
const cookieStore = await cookies();
diff --git a/playground/connect-next/app/(auth-required)/layout.tsx b/playground/connect-next/app/(auth-required)/layout.tsx
new file mode 100644
index 000000000..0228d012a
--- /dev/null
+++ b/playground/connect-next/app/(auth-required)/layout.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import { ProtectedRoute } from '@/components/ProtectedRoute';
+
+export default function AuthenticatedLayout({ children }: { children: React.ReactNode }) {
+ return
;
+}
diff --git a/playground/connect-next/app/(auth-required)/passkey-list/actions.ts b/playground/connect-next/app/(auth-required)/passkey-list/actions.ts
new file mode 100644
index 000000000..ae9546d3e
--- /dev/null
+++ b/playground/connect-next/app/(auth-required)/passkey-list/actions.ts
@@ -0,0 +1,17 @@
+'use server';
+
+import { ConnectTokenType } from '@corbado/types';
+import { getCorbadoConnectToken, verifyAmplifyToken } from '@/lib/utils';
+
+export const getCorbadoToken = async (tokenType: ConnectTokenType, idToken?: string) => {
+ if (!idToken) {
+ throw new Error('idToken is required');
+ }
+
+ const { displayName, identifier } = await verifyAmplifyToken(idToken);
+
+ return getCorbadoConnectToken(tokenType, {
+ displayName: displayName,
+ identifier: identifier,
+ });
+};
diff --git a/playground/connect-next/app/passkey-list/page.tsx b/playground/connect-next/app/(auth-required)/passkey-list/page.tsx
similarity index 57%
rename from playground/connect-next/app/passkey-list/page.tsx
rename to playground/connect-next/app/(auth-required)/passkey-list/page.tsx
index 93a731dbb..03e85d21a 100644
--- a/playground/connect-next/app/passkey-list/page.tsx
+++ b/playground/connect-next/app/(auth-required)/passkey-list/page.tsx
@@ -1,22 +1,21 @@
'use client';
-export const runtime = 'edge';
+import { fetchAuthSession } from 'aws-amplify/auth';
import { CorbadoConnectPasskeyList } from '@corbado/connect-react';
-import { useRouter } from 'next/navigation';
import { getCorbadoToken } from './actions';
-import { getAppendToken } from '../actions';
export default function PasskeyListPage() {
- const router = useRouter();
-
return (
- tokenType === 'passkey-append' ? await getAppendToken() : await getCorbadoToken(tokenType)
- }
+ connectTokenProvider={async tokenType => {
+ const session = await fetchAuthSession();
+ const idToken = session.tokens?.idToken?.toString();
+
+ return getCorbadoToken(tokenType, idToken);
+ }}
/>
diff --git a/playground/connect-next/app/(auth-required)/post-login/actions.ts b/playground/connect-next/app/(auth-required)/post-login/actions.ts
new file mode 100644
index 000000000..fc26a4b99
--- /dev/null
+++ b/playground/connect-next/app/(auth-required)/post-login/actions.ts
@@ -0,0 +1,32 @@
+'use server';
+
+import { AppendStatus, ConnectTokenType } from '@corbado/types';
+import { cookies } from 'next/headers';
+import { getCorbadoConnectToken, verifyAmplifyToken } from '@/lib/utils';
+
+export const getCorbadoToken = async (idToken?: string) => {
+ if (!idToken) {
+ throw new Error('idToken is required');
+ }
+
+ const { displayName, identifier } = await verifyAmplifyToken(idToken);
+
+ return getCorbadoConnectToken('passkey-append' as ConnectTokenType, {
+ displayName: displayName,
+ identifier: identifier,
+ });
+};
+
+export async function postPasskeyAppend(appendStatus: AppendStatus, clientState: string) {
+ // update client side state
+ console.log(appendStatus);
+ if (appendStatus === 'complete' || appendStatus === 'complete-noop') {
+ const cookieStore = await cookies();
+ cookieStore.set({
+ name: 'cbo_client_state',
+ value: clientState,
+ httpOnly: true,
+ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
+ });
+ }
+}
diff --git a/playground/connect-next/app/(auth-required)/post-login/page.tsx b/playground/connect-next/app/(auth-required)/post-login/page.tsx
new file mode 100644
index 000000000..7e0ba3c24
--- /dev/null
+++ b/playground/connect-next/app/(auth-required)/post-login/page.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import { CorbadoConnectAppend } from '@corbado/connect-react';
+import { useRouter } from 'next/navigation';
+import { getCorbadoToken, postPasskeyAppend } from '@/app/(auth-required)/post-login/actions';
+import { fetchAuthSession } from 'aws-amplify/auth';
+import { AppendStatus } from '@corbado/types';
+
+export default function Page() {
+ const router = useRouter();
+
+ return (
+
+
+
+ router.push('/home')}
+ appendTokenProvider={async () => {
+ const session = await fetchAuthSession();
+ const idToken = session.tokens?.idToken?.toString();
+
+ return await getCorbadoToken(idToken);
+ }}
+ onComplete={async (appendStatus: AppendStatus, clientState: string) => {
+ await postPasskeyAppend(appendStatus, clientState);
+ router.push('/home');
+ }}
+ />
+
+
+
+ );
+}
diff --git a/playground/connect-next/app/actions.ts b/playground/connect-next/app/actions.ts
deleted file mode 100644
index 7e81bf958..000000000
--- a/playground/connect-next/app/actions.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-'use server';
-
-import { cookies } from 'next/headers';
-
-export async function getAppendToken() {
- const cookieStore = await cookies();
- const displayName = cookieStore.get('displayName');
- if (!displayName) {
- return null;
- }
-
- const identifier = cookieStore.get('identifier');
- if (!identifier) {
- return null;
- }
-
- console.log(displayName, identifier);
-
- // call backend API to get token
- const payload = {
- type: 'passkey-append',
- data: {
- displayName: displayName.value,
- identifier: identifier.value,
- },
- };
-
- const body = JSON.stringify(payload);
-
- const url = `${process.env.CORBADO_BACKEND_API_URL}/v2/connectTokens`;
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- Authorization: `Basic ${process.env.CORBADO_BACKEND_API_BASIC_AUTH}`,
- 'Content-Type': 'application/json',
- },
- cache: 'no-cache',
- body: body,
- });
-
- const out = await response.json();
-
- return out.secret;
-}
-
-export async function hello() {
- return 'Hello, World!';
-}
-
-export async function hello2() {
- return 'Hello, World2!';
-}
diff --git a/playground/connect-next/app/globals.css b/playground/connect-next/app/globals.css
index 51f319668..cdd296bfd 100644
--- a/playground/connect-next/app/globals.css
+++ b/playground/connect-next/app/globals.css
@@ -11,3 +11,68 @@ html {
font-family: Verdana, Helvetica, sans-serif;
font-size: 14px;
}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/playground/connect-next/app/layout.tsx b/playground/connect-next/app/layout.tsx
index 96e27fc1e..52aaa8c10 100644
--- a/playground/connect-next/app/layout.tsx
+++ b/playground/connect-next/app/layout.tsx
@@ -2,7 +2,7 @@
import { Inter } from 'next/font/google';
import './globals.css';
-import { CorbadoConnectProvider } from '@corbado/connect-react';
+import WrappedCorbadoConnectProvider from '@/components/ClientWrapper';
const inter = Inter({ subsets: ['latin'] });
@@ -14,13 +14,7 @@ export default function RootLayout({
return (
-
- {children}
-
+
{children}
);
diff --git a/playground/connect-next/app/login/ConventionalLogin.tsx b/playground/connect-next/app/login/ConventionalLogin.tsx
index 69b01c4fa..7077b14f4 100644
--- a/playground/connect-next/app/login/ConventionalLogin.tsx
+++ b/playground/connect-next/app/login/ConventionalLogin.tsx
@@ -1,76 +1,122 @@
-import { useState } from 'react';
+import React, { useState } from 'react';
+import PasswordForm from '@/app/login/PasswordForm';
+import { confirmSignIn, signIn } from 'aws-amplify/auth';
import { useRouter } from 'next/navigation';
-import Link from 'next/link';
-import { startConventionalLogin } from './actions';
+import ConfirmOTP from '@/components/ConfirmOTP';
+import { cookies } from 'next/headers';
+import { TOTP } from 'totp-generator';
+import { autoFillTOTP } from '@/app/login/actions';
-export type Props = {
- initialEmail: string;
- initialError: string;
+type Props = {
+ initialUserProvidedIdentifier: string;
};
-export default function ConventionalLogin({ initialEmail, initialError }: Props) {
- const [password, setPassword] = useState('');
- const [error, setError] = useState(initialError);
- const [email, setEmail] = useState(initialEmail);
+enum State {
+ ProvidePassword,
+ ProvideSMSCode,
+ ProvideTOTPCode,
+}
+
+export const ConventionalLogin = ({ initialUserProvidedIdentifier }: Props) => {
+ const [state, setState] = useState(State.ProvidePassword);
const router = useRouter();
- const onSubmit = async () => {
- setError('');
- const res = await startConventionalLogin(email, password);
- console.log(res);
+ const handleConventionalLogin = async (username: string, password: string): Promise
=> {
+ try {
+ const result = await signIn({
+ username,
+ password,
+ });
+
+ switch (result.nextStep.signInStep) {
+ case 'CONFIRM_SIGN_IN_WITH_SMS_CODE':
+ setState(State.ProvideSMSCode);
+ break;
+
+ case 'CONFIRM_SIGN_IN_WITH_TOTP_CODE':
+ setState(State.ProvideTOTPCode);
+ break;
+
+ case 'DONE':
+ router.push('/post-login');
- if (!res.success) {
- setError(res.message ?? 'An unknown error occurred. Please try again later.');
+ break;
- return;
+ default:
+ console.error('Unexpected next step', result.nextStep);
+ break;
+ }
+ } catch (e) {
+ if (e instanceof Error) {
+ return e.message;
+ }
+
+ return 'An error occurred';
}
+ };
+
+ const handleConfirmCode = async (code: string): Promise => {
+ try {
+ const res = await confirmSignIn({
+ challengeResponse: code,
+ });
+
+ if (res.isSignedIn) {
+ router.push('/post-login');
+ }
+ } catch (e) {
+ if (e instanceof Error) {
+ return e.message;
+ }
- if (res.screen === 'MFA_SOFTWARE_TOKEN') {
- router.push('/mfa-software-token');
- } else {
- router.push('/post-login');
+ return 'An error occurred';
}
};
+ let headline, sub: string;
+ let content: React.ReactNode;
+ switch (state) {
+ case State.ProvidePassword:
+ headline = 'Login';
+ sub = 'Use your email to log into you Example Corp account.';
+ content = (
+
+ );
+
+ break;
+ case State.ProvideSMSCode:
+ headline = 'Check your phone';
+ sub = 'We have sent an SMS to your phone.';
+ content = ;
+
+ break;
+ case State.ProvideTOTPCode:
+ headline = 'Check your authenticator';
+ sub = 'Please enter the code from your authenticator app.';
+ content = (
+
+ );
+
+ break;
+ default:
+ throw new Error(`Invalid state: ${state}`);
+ }
+
return (
-
-
Login
- {error &&
{error}
}
-
setEmail(e.target.value)}
- />
-
setPassword(e.target.value)}
- />
-
-
+ <>
+
-
-
- Signup for an account
-
-
-
+
{content}
+ >
);
-}
+};
+
+export default ConventionalLogin;
diff --git a/playground/connect-next/app/login/LoginComponent.tsx b/playground/connect-next/app/login/LoginComponent.tsx
index ea2b4d336..9cbf5c4d7 100644
--- a/playground/connect-next/app/login/LoginComponent.tsx
+++ b/playground/connect-next/app/login/LoginComponent.tsx
@@ -4,30 +4,56 @@ import { useRouter } from 'next/navigation';
import { CorbadoConnectLogin } from '@corbado/connect-react';
import { useState } from 'react';
import ConventionalLogin from '@/app/login/ConventionalLogin';
-import { postPasskeyLoginNew } from '@/app/login/actions';
+import { postPasskeyLogin } from './actions';
+import { confirmSignIn, signIn } from 'aws-amplify/auth';
export type Props = {
clientState: string | undefined;
};
+const decodeJwt = (token: string) => {
+ const [, payload] = token.split('.');
+ return JSON.parse(atob(payload));
+};
+
+type WithWebauthnId = {
+ webauthnId: string;
+};
+
export default function LoginComponent({ clientState }: Props) {
const router = useRouter();
const [conventionalLoginVisible, setConventionalLoginVisible] = useState(false);
const [email, setEmail] = useState('');
const [fallbackErrorMessage, setFallbackErrorMessage] = useState('');
- console.log('conventionalLoginVisible', conventionalLoginVisible);
+ const postPasskeyLoginNew = async (signedPasskeyData: string, clientState: string) => {
+ // decode JWT
+ const decoded = decodeJwt(signedPasskeyData) as WithWebauthnId;
+
+ try {
+ await signIn({
+ username: decoded.webauthnId,
+ options: { authFlowType: 'CUSTOM_WITHOUT_SRP' },
+ });
+
+ const resultConfirm = await confirmSignIn({
+ challengeResponse: signedPasskeyData,
+ });
+ console.log('resultConfirm', resultConfirm);
+
+ await postPasskeyLogin(clientState);
+
+ router.push('/post-login');
+ } catch (e) {
+ console.error(e);
+ }
+ };
return (
- {conventionalLoginVisible ? (
-
- ) : null}
+ {conventionalLoginVisible ?
: null}
{
@@ -44,9 +70,8 @@ export default function LoginComponent({ clientState }: Props) {
}}
onError={(error: string) => console.log('error', error)}
onLoaded={(msg: string) => console.log('component has loaded: ' + msg)}
- onComplete={async (signedPasskeyData: string, clientState: string) => {
- await postPasskeyLoginNew(signedPasskeyData, clientState);
- router.push('/post-login');
+ onComplete={async (signedPasskeyData: string, newClientState: string) => {
+ await postPasskeyLoginNew(signedPasskeyData, newClientState);
}}
onSignupClick={() => router.push('/signup')}
onHelpClick={() => alert('help requested')}
diff --git a/playground/connect-next/app/login/PasswordForm.tsx b/playground/connect-next/app/login/PasswordForm.tsx
new file mode 100644
index 000000000..0c0f65e31
--- /dev/null
+++ b/playground/connect-next/app/login/PasswordForm.tsx
@@ -0,0 +1,72 @@
+import { FormEvent, useState } from 'react';
+
+type Props = {
+ initialUserProvidedIdentifier: string;
+ initialError?: string;
+ onClick: (username: string, password: string) => Promise;
+};
+
+export const PasswordForm = ({ onClick, initialUserProvidedIdentifier }: Props) => {
+ const [username, setUsername] = useState(initialUserProvidedIdentifier);
+ const [password, setPassword] = useState('');
+ const [message, setMessage] = useState('');
+
+ const handleLogin = async (e: FormEvent) => {
+ e.preventDefault();
+ setMessage('Loading...');
+ const maybeError = await onClick(username, password);
+ if (maybeError) {
+ setMessage(maybeError);
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default PasswordForm;
diff --git a/playground/connect-next/app/login/actions.ts b/playground/connect-next/app/login/actions.ts
index 9ccb62476..10379a604 100644
--- a/playground/connect-next/app/login/actions.ts
+++ b/playground/connect-next/app/login/actions.ts
@@ -1,141 +1,26 @@
'use server';
import { cookies } from 'next/headers';
-import {
- AdminGetUserCommand,
- CognitoIdentityProviderClient,
- InitiateAuthCommand,
-} from '@aws-sdk/client-cognito-identity-provider';
-import crypto from 'crypto';
-import { TokenWrapper, verifyToken } from '@/app/utils';
+import { TOTP } from 'totp-generator';
-// Here we validate the JWT token (validation is too simple, don't use this in production)
-// Then we extract the cognitoID and retrieve the user's email from the user pool
-// Both values will then be set as a cookie
-export async function postPasskeyLogin(session: string) {
- console.log('postPasskeyLogin', session);
- const tokenWrapper = JSON.parse(session) as TokenWrapper;
- const decoded = await verifyToken(tokenWrapper.AccessToken);
- const username = decoded.username;
-
- // create client that loads profile from ~/.aws/credentials or environment variables
- const client = new CognitoIdentityProviderClient({
- region: process.env.AWS_REGION!,
- credentials: {
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
- },
- });
-
- const command = new AdminGetUserCommand({
- UserPoolId: process.env.AWS_COGNITO_USER_POOL_ID!,
- Username: username,
+export async function postPasskeyLogin(clientState: string) {
+ const cookieStore = await cookies();
+ cookieStore.set({
+ name: 'cbo_client_state',
+ value: clientState,
+ httpOnly: true,
+ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
});
-
- const response = await client.send(command);
-
- const email = response.UserAttributes?.find(attr => attr.Name === 'email')?.Value;
- if (email) {
- const cookieStore = await cookies();
- cookieStore.set('displayName', email);
- cookieStore.set('identifier', username);
- }
-
- return;
}
-export async function postPasskeyLoginNew(signedPasskeyData: string, clientState: string) {
- const url = `${process.env.CORBADO_BACKEND_API_URL}/v2/passkey/postLogin`;
- const body = JSON.stringify({
- signedPasskeyData: signedPasskeyData,
- });
-
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- Authorization: `Basic ${process.env.CORBADO_BACKEND_API_BASIC_AUTH}`,
- 'Content-Type': 'application/json',
- },
- cache: 'no-cache',
- body: body,
- });
-
- const out = await response.json();
-
- await postPasskeyLogin(out.session);
-
- // update client side state
+export async function autoFillTOTP() {
const cookieStore = await cookies();
- cookieStore.set({ name: 'cbo_client_state', value: clientState, httpOnly: true });
-}
-
-function createSecretHash(username: string, clientId: string, clientSecret: string) {
- return crypto
- .createHmac('sha256', clientSecret)
- .update(username + clientId)
- .digest('base64');
-}
-
-export async function startConventionalLogin(email: string, password: string) {
- try {
- if (!email || !password) {
- throw new Error('Email and password are required.');
- }
-
- const cookieStore = await cookies();
- cookieStore.set('displayName', email);
-
- const client = new CognitoIdentityProviderClient({
- region: process.env.AWS_REGION!,
- credentials: {
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
- },
- });
-
- const command = new InitiateAuthCommand({
- AuthFlow: 'USER_PASSWORD_AUTH',
- ClientId: process.env.AWS_COGNITO_CLIENT_ID!,
- AuthParameters: {
- USERNAME: email,
- PASSWORD: password,
- SECRET_HASH: createSecretHash(
- email,
- process.env.AWS_COGNITO_CLIENT_ID!,
- process.env.AWS_COGNITO_CLIENT_SECRET!,
- ),
- },
- });
-
- const response = await client.send(command);
- console.log(response);
-
- if (response.AuthenticationResult?.AccessToken) {
- // no MFA has been set up yet
-
- const decoded = await verifyToken(response.AuthenticationResult.AccessToken);
- const username = decoded.username;
- if (email) {
- cookieStore.set('identifier', username);
- }
-
- return { success: true };
- }
-
- if (response.Session && response.ChallengeName === 'SOFTWARE_TOKEN_MFA') {
- cookieStore.set('mfa_session', response.Session);
-
- return { success: true, screen: 'MFA_SOFTWARE_TOKEN' };
- }
-
- return { success: false, message: 'An error occurred. Please try again later.' };
- } catch (err) {
- if (err instanceof Error) {
- if (err.name === 'NotAuthorizedException') return { success: false, message: 'Incorrect username or password.' };
+ const maybeSecretCode = cookieStore.get('secretCode');
+ if (!maybeSecretCode) {
+ return;
+ }
- return { success: false, message: err.message };
- }
+ const { otp } = TOTP.generate(maybeSecretCode.value);
- return { success: false, message: 'An error occurred. Please try again later.' };
- }
+ return otp;
}
diff --git a/playground/connect-next/app/mfa-software-token/actions.ts b/playground/connect-next/app/mfa-software-token/actions.ts
deleted file mode 100644
index 743663b5c..000000000
--- a/playground/connect-next/app/mfa-software-token/actions.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-'use server';
-
-import { cookies } from 'next/headers';
-import {
- CognitoIdentityProviderClient,
- RespondToAuthChallengeCommand,
-} from '@aws-sdk/client-cognito-identity-provider';
-import crypto from 'crypto';
-import { verifyToken } from '@/app/utils';
-import { TOTP } from 'totp-generator';
-
-function createSecretHash(username: string, clientId: string, clientSecret: string) {
- return crypto
- .createHmac('sha256', clientSecret)
- .update(username + clientId)
- .digest('base64');
-}
-
-export async function startMFASoftwareToken(totp: string) {
- try {
- const cookieStore = await cookies();
- const session = cookieStore.get('mfa_session');
- const displayName = cookieStore.get('displayName');
-
- if (!totp || !session || !displayName) {
- throw new Error('Missing required fields.');
- }
-
- const client = new CognitoIdentityProviderClient({
- region: process.env.AWS_REGION!,
- credentials: {
- accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
- },
- });
-
- const challengeResponseCommand = new RespondToAuthChallengeCommand({
- ClientId: process.env.AWS_COGNITO_CLIENT_ID!,
- ChallengeName: 'SOFTWARE_TOKEN_MFA',
- Session: session.value,
- ChallengeResponses: {
- USERNAME: displayName.value,
- SOFTWARE_TOKEN_MFA_CODE: totp,
- SECRET_HASH: createSecretHash(
- displayName.value,
- process.env.AWS_COGNITO_CLIENT_ID!,
- process.env.AWS_COGNITO_CLIENT_SECRET!,
- ),
- },
- });
-
- const mfaResult = await client.send(challengeResponseCommand);
- console.log('MFA login complete', mfaResult);
-
- if (mfaResult.AuthenticationResult?.AccessToken) {
- // no MFA has been set up yet
-
- const decoded = await verifyToken(mfaResult.AuthenticationResult.AccessToken);
- if (decoded.username) {
- return { success: true };
- }
-
- return { success: false, message: 'An error occurred. Please try again later.' };
- }
-
- return { success: true, screen: 'MFA_SOFTWARE_TOKEN' };
- } catch (err) {
- if (err instanceof Error) {
- if (err.name === 'NotAuthorizedException') return { success: false, message: 'Incorrect username or password.' };
-
- return { success: false, message: err.message };
- }
-
- return { success: false, message: 'An error occurred. Please try again later.' };
- }
-}
-
-export async function generateTOTP() {
- const cookieStore = await cookies();
- const secretCode = cookieStore.get('secretCode');
- if (!secretCode) {
- return {
- success: false,
- message: 'Secret code not found. Autofill only works as long as the cookie set during signup is still there.',
- };
- }
-
- const { otp } = TOTP.generate(secretCode.value!);
-
- return { success: true, otp };
-}
diff --git a/playground/connect-next/app/mfa-software-token/page.tsx b/playground/connect-next/app/mfa-software-token/page.tsx
deleted file mode 100644
index f244a2029..000000000
--- a/playground/connect-next/app/mfa-software-token/page.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-'use client';
-
-import { useRouter } from 'next/navigation';
-import { useState } from 'react';
-import { generateTOTP, startMFASoftwareToken } from '@/app/mfa-software-token/actions';
-
-export default function LoginPage() {
- const router = useRouter();
- const [conventionalLoginVisible, setConventionalLoginVisible] = useState(false);
- const [totp, setTotp] = useState('');
- const [error, setError] = useState('');
-
- const onSubmit = async () => {
- setError('');
- const res = await startMFASoftwareToken(totp);
-
- if (!res.success) {
- setError(res.message ?? 'An unknown error occurred. Please try again later.');
-
- return;
- }
-
- if (res.screen === 'MFA_SOFTWARE_TOKEN') {
- router.push('/mfa-software-token');
- } else {
- router.push('/post-login');
- }
- };
-
- const onAutofillTOTP = async () => {
- setError('');
- const res = await generateTOTP();
-
- if (!res.success) {
- setError(res.message ?? 'An unknown error occurred. Please try again later.');
-
- return;
- }
-
- setTotp(res.otp ?? '');
- };
-
- return (
-
-
-
-
MFA
- {error &&
{error}
}
-
setTotp(e.target.value)}
- />
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/playground/connect-next/app/page.tsx b/playground/connect-next/app/page.tsx
index 9b0bb393e..576df74fd 100644
--- a/playground/connect-next/app/page.tsx
+++ b/playground/connect-next/app/page.tsx
@@ -1,8 +1,6 @@
'use client';
-export const runtime = 'edge';
import Link from 'next/link';
-import { hello, hello2 } from '@/app/actions';
export default function Home() {
return (
diff --git a/playground/connect-next/app/passkey-list-wv/actions.ts b/playground/connect-next/app/passkey-list-wv/actions.ts
new file mode 100644
index 000000000..bedbc4f2a
--- /dev/null
+++ b/playground/connect-next/app/passkey-list-wv/actions.ts
@@ -0,0 +1,20 @@
+'use server';
+
+import { cookies } from 'next/headers';
+import { ConnectTokenType } from '@corbado/types';
+import { getCorbadoConnectToken, verifyAmplifyToken } from '@/lib/utils';
+
+export const getCorbadoToken = async (tokenType: ConnectTokenType) => {
+ const cookieStore = await cookies();
+ const idToken = cookieStore.get('idToken');
+ if (!idToken) {
+ throw new Error('idToken is required');
+ }
+
+ const { displayName, identifier } = await verifyAmplifyToken(idToken.value);
+
+ return getCorbadoConnectToken(tokenType, {
+ displayName: displayName,
+ identifier: identifier,
+ });
+};
diff --git a/playground/connect-next/app/passkey-list-wv/page.tsx b/playground/connect-next/app/passkey-list-wv/page.tsx
new file mode 100644
index 000000000..4e6703b18
--- /dev/null
+++ b/playground/connect-next/app/passkey-list-wv/page.tsx
@@ -0,0 +1,15 @@
+'use client';
+import { CorbadoConnectPasskeyList } from '@corbado/connect-react';
+import { getCorbadoToken } from './actions';
+
+export default function PasskeyListPage() {
+ return (
+
+
+
+ getCorbadoToken(tokenType)} />
+
+
+
+ );
+}
diff --git a/playground/connect-next/app/passkey-list/actions.ts b/playground/connect-next/app/passkey-list/actions.ts
deleted file mode 100644
index 6f333ba94..000000000
--- a/playground/connect-next/app/passkey-list/actions.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-'use server';
-
-import { cookies } from 'next/headers';
-import { ConnectTokenType } from '@corbado/types';
-
-export async function getCorbadoToken(tokenType: ConnectTokenType) {
- const cookieStore = await cookies();
- const identifier = cookieStore.get('identifier');
- if (!identifier) {
- return null;
- }
-
- // call backend API to get token
- const payload = {
- type: tokenType,
- data: {
- identifier: identifier.value,
- },
- };
-
- const body = JSON.stringify(payload);
-
- const url = `${process.env.CORBADO_BACKEND_API_URL}/v2/connectTokens`;
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- Authorization: `Basic ${process.env.CORBADO_BACKEND_API_BASIC_AUTH}`,
- 'Content-Type': 'application/json',
- },
- cache: 'no-cache',
- body: body,
- });
-
- const out = await response.json();
- console.log(out);
-
- return out.secret;
-}
diff --git a/playground/connect-next/app/post-login-wv/actions.ts b/playground/connect-next/app/post-login-wv/actions.ts
new file mode 100644
index 000000000..6285d3f9a
--- /dev/null
+++ b/playground/connect-next/app/post-login-wv/actions.ts
@@ -0,0 +1,34 @@
+'use server';
+
+import { AppendStatus, ConnectTokenType } from '@corbado/types';
+import { cookies } from 'next/headers';
+import { getCorbadoConnectToken, verifyAmplifyToken } from '@/lib/utils';
+
+export const getCorbadoToken = async () => {
+ const cookieStore = await cookies();
+ const idToken = cookieStore.get('idToken');
+ if (!idToken) {
+ throw new Error('idToken is required');
+ }
+
+ const { displayName, identifier } = await verifyAmplifyToken(idToken.value);
+
+ return getCorbadoConnectToken('passkey-append' as ConnectTokenType, {
+ displayName: displayName,
+ identifier: identifier,
+ });
+};
+
+export async function postPasskeyAppend(appendStatus: AppendStatus, clientState: string) {
+ // update client side state
+ console.log(appendStatus);
+ if (appendStatus === 'complete' || appendStatus === 'complete-noop') {
+ const cookieStore = await cookies();
+ cookieStore.set({
+ name: 'cbo_client_state',
+ value: clientState,
+ httpOnly: true,
+ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
+ });
+ }
+}
diff --git a/playground/connect-next/app/post-login-wv/page.tsx b/playground/connect-next/app/post-login-wv/page.tsx
new file mode 100644
index 000000000..bbbcb7788
--- /dev/null
+++ b/playground/connect-next/app/post-login-wv/page.tsx
@@ -0,0 +1,27 @@
+'use client';
+import { CorbadoConnectAppend } from '@corbado/connect-react';
+import { getCorbadoToken, postPasskeyAppend } from '@/app/post-login-wv/actions';
+import { AppendStatus } from '@corbado/types';
+
+export default function PostLoginPage() {
+ return (
+
+
+
+ {
+ window.location.href = `auth://callback?status=${status}`;
+ }}
+ appendTokenProvider={async () => {
+ return await getCorbadoToken();
+ }}
+ onComplete={async (status: AppendStatus, clientSideState: string) => {
+ await postPasskeyAppend(status, clientSideState);
+ window.location.href = `auth://callback?status=${status}`;
+ }}
+ />
+
+
+
+ );
+}
diff --git a/playground/connect-next/app/post-login/actions.ts b/playground/connect-next/app/post-login/actions.ts
deleted file mode 100644
index 9a86aef84..000000000
--- a/playground/connect-next/app/post-login/actions.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-'use server';
-
-import { cookies } from 'next/headers';
-
-export async function postPasskeyAppend(_: string, clientState: string) {
- const cookieStore = await cookies();
- cookieStore.set({ name: 'cbo_client_state', value: clientState, httpOnly: true });
-}
diff --git a/playground/connect-next/app/post-login/page.tsx b/playground/connect-next/app/post-login/page.tsx
deleted file mode 100644
index 6d7e1d35a..000000000
--- a/playground/connect-next/app/post-login/page.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-'use client';
-import { postPasskeyAppend } from '@/app/post-login/actions';
-
-export const runtime = 'edge';
-
-import { CorbadoConnectAppend } from '@corbado/connect-react';
-import { useRouter } from 'next/navigation';
-import { getAppendToken } from '../actions';
-
-export default function PostLoginPage() {
- const router = useRouter();
-
- return (
-
-
-
- router.push('/home')}
- appendTokenProvider={async () => {
- const t = await getAppendToken();
- console.log(t);
-
- return t;
- }}
- onComplete={async (_, clientSideState: string) => {
- console.log('onComplete', clientSideState);
- await postPasskeyAppend('', clientSideState);
- router.push('/home');
- }}
- />
-
-
-
- );
-}
diff --git a/playground/connect-next/app/redirect/actions.ts b/playground/connect-next/app/redirect/actions.ts
new file mode 100644
index 000000000..0c3ea693b
--- /dev/null
+++ b/playground/connect-next/app/redirect/actions.ts
@@ -0,0 +1,12 @@
+'use server';
+
+import { cookies } from 'next/headers';
+
+export const setIdToken = async (token: string) => {
+ const cookieStore = await cookies();
+ cookieStore.set({
+ name: 'idToken',
+ value: token,
+ httpOnly: true,
+ });
+};
diff --git a/playground/connect-next/app/redirect/page.tsx b/playground/connect-next/app/redirect/page.tsx
new file mode 100644
index 000000000..8e0382ac4
--- /dev/null
+++ b/playground/connect-next/app/redirect/page.tsx
@@ -0,0 +1,42 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import { setIdToken } from './actions';
+import { Suspense, useEffect, useState } from 'react';
+
+export default function Page() {
+ return (
+
+
+
+ );
+}
+
+function Redirecting() {
+ const searchParams = useSearchParams();
+ const token = searchParams.get('token');
+ const redirectUrl = searchParams.get('redirectUrl');
+ const [loading, setLoading] = useState(true);
+ const router = useRouter();
+
+ useEffect(() => {
+ const init = async () => {
+ if (!token || !redirectUrl) {
+ return;
+ }
+
+ await setIdToken(token);
+ setLoading(false);
+ console.log('pushing redirectUrl', redirectUrl);
+ router.push(redirectUrl);
+ };
+
+ init();
+ }, []);
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ return Redirecting...
;
+}
diff --git a/playground/connect-next/app/signup/actions.ts b/playground/connect-next/app/signup/actions.ts
index 8e9338feb..e683bedae 100644
--- a/playground/connect-next/app/signup/actions.ts
+++ b/playground/connect-next/app/signup/actions.ts
@@ -1,125 +1,8 @@
'use server';
import { cookies } from 'next/headers';
-import { generateRandomString } from '@/utils/random';
-import {
- AdminCreateUserCommand,
- AdminInitiateAuthCommand,
- AdminSetUserMFAPreferenceCommand,
- AdminSetUserPasswordCommand,
- AssociateSoftwareTokenCommand,
- CognitoIdentityProviderClient,
- VerifySoftwareTokenCommand,
-} from '@aws-sdk/client-cognito-identity-provider';
-import { TOTP } from 'totp-generator';
-import CryptoJS from 'crypto-js';
-const cognitoUserPoolId = process.env.AWS_COGNITO_USER_POOL_ID!;
-const cognitoClientId = process.env.AWS_COGNITO_CLIENT_ID!;
-const cognitoClientSecret = process.env.AWS_COGNITO_CLIENT_SECRET!;
-const awsRegion = process.env.AWS_REGION!;
-const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID!;
-const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY!;
-
-export const createAccount = async (email: string, phone: string, password: string) => {
- // of course this is not secure, but it's just a demo ;)
-
- const randomUsername = generateRandomString(10);
+export const setTOTPSecretCode = async (secretCode: string) => {
const cookieStore = await cookies();
-
- cookieStore.set('displayName', email);
- cookieStore.set('identifier', randomUsername);
-
- // create client that loads profile from ~/.aws/credentials or environment variables
- const client = new CognitoIdentityProviderClient({
- region: awsRegion,
- credentials: {
- accessKeyId: awsAccessKeyId,
- secretAccessKey: awsSecretAccessKey,
- },
- });
-
- const command = new AdminCreateUserCommand({
- UserPoolId: cognitoUserPoolId,
- Username: randomUsername,
- ForceAliasCreation: true,
- MessageAction: 'SUPPRESS',
- UserAttributes: [
- {
- Name: 'email',
- Value: email,
- },
- {
- Name: 'email_verified',
- Value: 'true',
- },
- {
- Name: 'phone_number',
- Value: phone,
- },
- ],
- });
-
- await client.send(command);
-
- const passwordCommand = new AdminSetUserPasswordCommand({
- UserPoolId: cognitoUserPoolId,
- Username: randomUsername,
- Password: password,
- Permanent: true,
- });
-
- await client.send(passwordCommand);
-
- const initiateAuthCommand = new AdminInitiateAuthCommand({
- AuthFlow: 'ADMIN_USER_PASSWORD_AUTH',
- ClientId: cognitoClientId,
- UserPoolId: cognitoUserPoolId,
- AuthParameters: {
- USERNAME: randomUsername,
- PASSWORD: password,
- SECRET_HASH: await createSecretHash(randomUsername, cognitoClientId, cognitoClientSecret),
- },
- });
-
- const initiateAuthRes = await client.send(initiateAuthCommand);
-
- const associateSoftwareTokenCommand = new AssociateSoftwareTokenCommand({
- Session: initiateAuthRes.Session,
- AccessToken: initiateAuthRes.AuthenticationResult?.AccessToken,
- });
-
- const associateSoftwareTokenRes = await client.send(associateSoftwareTokenCommand);
- console.log('associateSoftwareTokenRes', associateSoftwareTokenRes);
-
- cookieStore.set('secretCode', associateSoftwareTokenRes.SecretCode!);
-
- const { otp } = TOTP.generate(associateSoftwareTokenRes.SecretCode!);
- console.log('otp', otp);
- const verifySoftwareTokenCommand = new VerifySoftwareTokenCommand({
- Session: initiateAuthRes.Session,
- AccessToken: initiateAuthRes.AuthenticationResult?.AccessToken,
- UserCode: otp,
- });
-
- const verifySoftwareTokenRes = await client.send(verifySoftwareTokenCommand);
- console.log('verifySoftwareTokenRes', verifySoftwareTokenRes);
-
- const setMfaPreferenceCommand = new AdminSetUserMFAPreferenceCommand({
- UserPoolId: cognitoUserPoolId,
- Username: randomUsername,
- SoftwareTokenMfaSettings: {
- Enabled: true,
- PreferredMfa: true,
- },
- });
- const setMfaPreferenceCommandRes = await client.send(setMfaPreferenceCommand);
- console.log('setMfaPreferenceCommandRes', setMfaPreferenceCommandRes);
-
- return;
-};
-
-const createSecretHash = async (username: string, clientId: string, clientSecret: string) => {
- const hmac = CryptoJS.HmacSHA256(username + clientId, clientSecret);
- return hmac.toString(CryptoJS.enc.Base64);
+ cookieStore.set('secretCode', secretCode);
};
diff --git a/playground/connect-next/app/signup/page.tsx b/playground/connect-next/app/signup/page.tsx
index 0a74d50bd..f2cf3c993 100644
--- a/playground/connect-next/app/signup/page.tsx
+++ b/playground/connect-next/app/signup/page.tsx
@@ -1,11 +1,12 @@
'use client';
-export const runtime = 'edge';
+import { TOTP } from 'totp-generator';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
-import { createAccount } from './actions';
-import { generateRandomString } from '@/utils/random';
+import { generateRandomString } from '@/lib/random';
+import { signUp, signIn, setUpTOTP, verifyTOTPSetup, updateMFAPreference } from 'aws-amplify/auth';
+import { setTOTPSecretCode } from '@/app/signup/actions';
export default function SignupPage() {
const router = useRouter();
@@ -13,8 +14,43 @@ export default function SignupPage() {
const [phone, setPhone] = useState('');
const [password, setPassword] = useState('');
- const signUp = async (email: string, phone: string, password: string) => {
- await createAccount(email, phone, password);
+ const onClickSignUp = async (email: string, phone: string, password: string) => {
+ const username = generateRandomString(10);
+
+ try {
+ const resSignUp = await signUp({
+ username: username,
+ password,
+ options: {
+ userAttributes: {
+ email: email,
+ phone_number: phone,
+ },
+ },
+ });
+
+ console.log(resSignUp);
+
+ const resLogin = await signIn({ username, password });
+ console.log(resLogin);
+
+ const setupRes = await setUpTOTP();
+ console.log('setupRes', setupRes);
+
+ await setTOTPSecretCode(setupRes.sharedSecret);
+
+ const { otp } = TOTP.generate(setupRes.sharedSecret);
+ console.log('otp', otp);
+
+ await verifyTOTPSetup({ code: otp });
+ await updateMFAPreference({
+ totp: 'PREFERRED',
+ });
+
+ router.push('/post-login');
+ } catch (err) {
+ console.error('Error during signup:', err);
+ }
};
return (
@@ -64,7 +100,7 @@ export default function SignupPage() {