From 5e15a64b7879598bd551d9ed5762a775be050541 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Mon, 7 Oct 2024 17:44:31 -0400 Subject: [PATCH 01/18] Draft --- src/components/apple-auth.tsx | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/components/apple-auth.tsx diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx new file mode 100644 index 0000000..6dc3be8 --- /dev/null +++ b/src/components/apple-auth.tsx @@ -0,0 +1,55 @@ +"use client" + +import { useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import { useAuth } from "@/providers/auth-provider" +import { useTurnkey } from "@turnkey/sdk-react" +import AppleLogin from "react-apple-login" +import { sha256 } from "viem" + +import { env } from "@/env.mjs" + +import { Skeleton } from "./ui/skeleton" + +// @todo: these will be used once we can create a custom google login button +const AppleAuth = () => { + const { authIframeClient } = useTurnkey() + + const [nonce, setNonce] = useState("") + const { loginWithOAuth } = useAuth() + + const searchParams = useSearchParams() + const token = searchParams.get("id_token") + + useEffect(() => { + if (authIframeClient?.iframePublicKey) { + const hashedPublicKey = sha256( + authIframeClient.iframePublicKey as `0x${string}` + ).replace(/^0x/, "") + + setNonce(hashedPublicKey) + } + if ( + typeof token !== undefined && + token !== null && + authIframeClient?.iframePublicKey !== undefined + ) { + console.log("we have a valid token") + console.log(token) + console.log(authIframeClient.iframePublicKey) + loginWithOAuth(token as string) + } + }, [authIframeClient?.iframePublicKey, token]) + + return ( + + ) +} + +export default AppleAuth From c2f4970fc10e9efbdb577000fdedcfb2a8d26d99 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Mon, 7 Oct 2024 18:08:54 -0400 Subject: [PATCH 02/18] Getting closer --- src/components/apple-auth.tsx | 46 +++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index 6dc3be8..551f781 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -7,20 +7,23 @@ import { useTurnkey } from "@turnkey/sdk-react" import AppleLogin from "react-apple-login" import { sha256 } from "viem" -import { env } from "@/env.mjs" - -import { Skeleton } from "./ui/skeleton" - -// @todo: these will be used once we can create a custom google login button const AppleAuth = () => { const { authIframeClient } = useTurnkey() - - const [nonce, setNonce] = useState("") const { loginWithOAuth } = useAuth() + const [nonce, setNonce] = useState("") + const [storedToken, setStoredToken] = useState(null) // Store the token locally const searchParams = useSearchParams() - const token = searchParams.get("id_token") + // Get token from query string params and store in state when available + useEffect(() => { + const token = searchParams.get("id_token") + if (token) { + setStoredToken(token) // Store token if available + } + }, [searchParams]) + + // Generate nonce based on iframePublicKey useEffect(() => { if (authIframeClient?.iframePublicKey) { const hashedPublicKey = sha256( @@ -29,17 +32,24 @@ const AppleAuth = () => { setNonce(hashedPublicKey) } - if ( - typeof token !== undefined && - token !== null && - authIframeClient?.iframePublicKey !== undefined - ) { - console.log("we have a valid token") - console.log(token) - console.log(authIframeClient.iframePublicKey) - loginWithOAuth(token as string) + }, [authIframeClient?.iframePublicKey]) + + // Trigger loginWithOAuth when both token and iframePublicKey are available + useEffect(() => { + if (storedToken && authIframeClient?.iframePublicKey) { + console.log("Both token and iframePublicKey are available") + console.log("ID Token:", storedToken) + console.log("iframePublicKey:", authIframeClient.iframePublicKey) + + // Call the OAuth login function with the stored token + loginWithOAuth(storedToken) } - }, [authIframeClient?.iframePublicKey, token]) + }, [storedToken, authIframeClient?.iframePublicKey, loginWithOAuth]) + + // Ensure nonce is set correctly before rendering AppleLogin + if (!nonce) { + return
Loading...
// Or some appropriate loading state + } return ( Date: Mon, 7 Oct 2024 19:04:43 -0400 Subject: [PATCH 03/18] Working copy --- package.json | 2 ++ pnpm-lock.yaml | 27 ++++++++++++++++++ src/components/apple-auth.tsx | 53 ++++++++++++++++++++++------------- src/components/auth.tsx | 9 +++++- src/env.mjs | 3 ++ 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index db74f7d..2fc8191 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,9 @@ "next": "14.1.1", "next-themes": "^0.2.1", "react": "^18.3.1", + "react-apple-login": "^1.1.6", "react-dom": "^18.3.1", + "react-facebook-login": "^4.1.1", "react-hook-form": "^7.53.0", "react-jazzicon": "^1.0.4", "react-qr-code": "^2.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28fd57a..79e599d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -92,9 +92,15 @@ dependencies: react: specifier: ^18.3.1 version: 18.3.1 + react-apple-login: + specifier: ^1.1.6 + version: 1.1.6(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1) react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-facebook-login: + specifier: ^4.1.1 + version: 4.1.1(react@18.3.1) react-hook-form: specifier: ^7.53.0 version: 7.53.0(react@18.3.1) @@ -8290,6 +8296,19 @@ packages: engines: {node: '>= 0.6'} dev: false + /react-apple-login@1.1.6(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-ySV6ax0aB+ksA7lKzhr4MvsgjwSH068VtdHJXS+7rL380IJnNQNl14SszR31k3UqB8q8C1H1oyjJFGq4MyO6tw==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + prop-types: ^15.5.4 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /react-devtools-core@5.3.1: resolution: {integrity: sha512-7FSb9meX0btdBQLwdFOwt6bGqvRPabmVMMslv8fgoSPqXyuGpgQe36kx8gR86XPw7aV1yVouTp6fyZ0EH+NfUw==} dependencies: @@ -8310,6 +8329,14 @@ packages: scheduler: 0.23.2 dev: false + /react-facebook-login@4.1.1(react@18.3.1): + resolution: {integrity: sha512-COnHEHlYGTKipz4963safFAK9PaNTcCiXfPXMS/yxo8El+/AJL5ye8kMJf23lKSSGGPgqFQuInskIHVqGqTvSw==} + peerDependencies: + react: ^16.0.0 + dependencies: + react: 18.3.1 + dev: false + /react-hook-form@7.53.0(react@18.3.1): resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} engines: {node: '>=18.0.0'} diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index 551f781..84608a9 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -7,12 +7,20 @@ import { useTurnkey } from "@turnkey/sdk-react" import AppleLogin from "react-apple-login" import { sha256 } from "viem" -const AppleAuth = () => { +import { env } from "@/env.mjs" + +import { Skeleton } from "./ui/skeleton" + +const AppleAuth = ({ loading }: { loading: boolean }) => { + const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID + const { authIframeClient } = useTurnkey() const { loginWithOAuth } = useAuth() const [nonce, setNonce] = useState("") const [storedToken, setStoredToken] = useState(null) // Store the token locally + const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called + const searchParams = useSearchParams() // Get token from query string params and store in state when available @@ -34,31 +42,36 @@ const AppleAuth = () => { } }, [authIframeClient?.iframePublicKey]) - // Trigger loginWithOAuth when both token and iframePublicKey are available + // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once useEffect(() => { - if (storedToken && authIframeClient?.iframePublicKey) { - console.log("Both token and iframePublicKey are available") - console.log("ID Token:", storedToken) - console.log("iframePublicKey:", authIframeClient.iframePublicKey) - + if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) { // Call the OAuth login function with the stored token loginWithOAuth(storedToken) - } - }, [storedToken, authIframeClient?.iframePublicKey, loginWithOAuth]) - // Ensure nonce is set correctly before rendering AppleLogin - if (!nonce) { - return
Loading...
// Or some appropriate loading state - } + // Set flag to prevent further calls + setHasLoggedIn(true) + } + }, [ + storedToken, + authIframeClient?.iframePublicKey, + hasLoggedIn, + loginWithOAuth, + ]) return ( - + <> + {nonce ? ( + + ) : ( + + )} + ) } diff --git a/src/components/auth.tsx b/src/components/auth.tsx index 75cea4e..c730758 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { useRouter } from "next/navigation" +import { useRouter, useSearchParams } from "next/navigation" import { useAuth } from "@/providers/auth-provider" import { zodResolver } from "@hookform/resolvers/zod" import { useTurnkey } from "@turnkey/sdk-react" @@ -22,6 +22,7 @@ import { import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" +import AppleAuth from "./apple-auth" import GoogleAuth from "./google-auth" import { Icons } from "./icons" import Legal from "./legal" @@ -37,6 +38,11 @@ export default function Auth() { const [loadingAction, setLoadingAction] = useState(null) const router = useRouter() + const searchParams = useSearchParams() + + // Extract specific query params + const paramValue = searchParams.get("fun") // Replace 'paramKey' with the actual param key you expect + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -45,6 +51,7 @@ export default function Auth() { }) useEffect(() => { + console.log(paramValue) if (user) { router.push("/dashboard") } diff --git a/src/env.mjs b/src/env.mjs index 09417c0..77bcffe 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -5,6 +5,7 @@ import { z } from "zod" export const env = createEnv({ client: { NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID: z.string().min(1), + NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID: z.string().min(1), NEXT_PUBLIC_RP_ID: z.string().optional(), NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), NEXT_PUBLIC_APP_URL: z.string().optional(), @@ -28,6 +29,8 @@ export const env = createEnv({ WARCHEST_PRIVATE_KEY_ID: z.string().min(1), }, runtimeEnv: { + NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID: + process.env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID, NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID, NEXT_PUBLIC_RP_ID: process.env.NEXT_PUBLIC_RP_ID, From 3eb393adb3a49aaafcb88f64694206ae538e6ec2 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Mon, 7 Oct 2024 19:07:15 -0400 Subject: [PATCH 04/18] Dynamic redirectURI --- src/components/apple-auth.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index 84608a9..2ef9803 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -11,8 +11,9 @@ import { env } from "@/env.mjs" import { Skeleton } from "./ui/skeleton" -const AppleAuth = ({ loading }: { loading: boolean }) => { +const AppleAuth = () => { const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID + const redirectURI = env.NEXT_PUBLIC_APP_URL const { authIframeClient } = useTurnkey() const { loginWithOAuth } = useAuth() @@ -63,7 +64,7 @@ const AppleAuth = ({ loading }: { loading: boolean }) => { {nonce ? ( Date: Mon, 7 Oct 2024 19:08:12 -0400 Subject: [PATCH 05/18] Removing Facebook --- package.json | 1 - pnpm-lock.yaml | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/package.json b/package.json index 2fc8191..b9c52d0 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "react": "^18.3.1", "react-apple-login": "^1.1.6", "react-dom": "^18.3.1", - "react-facebook-login": "^4.1.1", "react-hook-form": "^7.53.0", "react-jazzicon": "^1.0.4", "react-qr-code": "^2.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79e599d..8867e18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,6 @@ dependencies: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - react-facebook-login: - specifier: ^4.1.1 - version: 4.1.1(react@18.3.1) react-hook-form: specifier: ^7.53.0 version: 7.53.0(react@18.3.1) @@ -8329,14 +8326,6 @@ packages: scheduler: 0.23.2 dev: false - /react-facebook-login@4.1.1(react@18.3.1): - resolution: {integrity: sha512-COnHEHlYGTKipz4963safFAK9PaNTcCiXfPXMS/yxo8El+/AJL5ye8kMJf23lKSSGGPgqFQuInskIHVqGqTvSw==} - peerDependencies: - react: ^16.0.0 - dependencies: - react: 18.3.1 - dev: false - /react-hook-form@7.53.0(react@18.3.1): resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} engines: {node: '>=18.0.0'} From e6fef699ca151240e60f84b4a23cdd9e4be53971 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Mon, 7 Oct 2024 19:10:30 -0400 Subject: [PATCH 06/18] Cleanup --- .env.example | 1 + src/components/auth.tsx | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 8125d31..78874f3 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,7 @@ NEXT_PUBLIC_BASE_URL=https://api.turnkey.com NEXT_PUBLIC_ORGANIZATION_ID= NEXT_PUBLIC_RP_ID= NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID= +NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID= # Sensitive values ideally for server only ALCHEMY_API_KEY= diff --git a/src/components/auth.tsx b/src/components/auth.tsx index c730758..9ff2b42 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -1,7 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { useRouter, useSearchParams } from "next/navigation" +import { useRouter } from "next/navigation" import { useAuth } from "@/providers/auth-provider" import { zodResolver } from "@hookform/resolvers/zod" import { useTurnkey } from "@turnkey/sdk-react" @@ -38,11 +38,6 @@ export default function Auth() { const [loadingAction, setLoadingAction] = useState(null) const router = useRouter() - const searchParams = useSearchParams() - - // Extract specific query params - const paramValue = searchParams.get("fun") // Replace 'paramKey' with the actual param key you expect - const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -51,7 +46,6 @@ export default function Auth() { }) useEffect(() => { - console.log(paramValue) if (user) { router.push("/dashboard") } From 23b32b19b9df5af2d413fccc4adaabfe0ddcb057 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Mon, 7 Oct 2024 19:19:09 -0400 Subject: [PATCH 07/18] Loading property --- src/components/apple-auth.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index 2ef9803..f18076c 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -11,7 +11,7 @@ import { env } from "@/env.mjs" import { Skeleton } from "./ui/skeleton" -const AppleAuth = () => { +const AppleAuth = ({ loading }: { loading: boolean }) => { const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID const redirectURI = env.NEXT_PUBLIC_APP_URL @@ -46,6 +46,8 @@ const AppleAuth = () => { // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once useEffect(() => { if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) { + console.log("doing the thing to the things") + // Call the OAuth login function with the stored token loginWithOAuth(storedToken) From c582178a04dfa78b589612bb3c782040a5b97ff9 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Tue, 8 Oct 2024 12:42:48 -0400 Subject: [PATCH 08/18] Reorganizing --- src/app/(landing)/oauth-callback/page.tsx | 60 +++++++++++++++++++++++ src/components/apple-auth.tsx | 32 +----------- 2 files changed, 62 insertions(+), 30 deletions(-) create mode 100644 src/app/(landing)/oauth-callback/page.tsx diff --git a/src/app/(landing)/oauth-callback/page.tsx b/src/app/(landing)/oauth-callback/page.tsx new file mode 100644 index 0000000..0e88923 --- /dev/null +++ b/src/app/(landing)/oauth-callback/page.tsx @@ -0,0 +1,60 @@ +"use client" + +import { Suspense, useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import { useAuth } from "@/providers/auth-provider" +import { useTurnkey } from "@turnkey/sdk-react" +import { Loader, Send } from "lucide-react" +import { sha256 } from "viem" + +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Icons } from "@/components/icons" + +function OAuthProcessCallback() { + const searchParams = useSearchParams() + + const { authIframeClient } = useTurnkey() + const { loginWithOAuth } = useAuth() + + const [storedToken, setStoredToken] = useState(null) // Store the token locally + const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called + + // Get token from query string params and store in state when available + useEffect(() => { + const token = searchParams.get("id_token") + if (token) { + setStoredToken(token) // Store token if available + } + }, [searchParams]) + + // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once + useEffect(() => { + if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) { + // Call the OAuth login function with the stored token + loginWithOAuth(storedToken) + + // Set flag to prevent further calls + setHasLoggedIn(true) + } + }, [ + storedToken, + authIframeClient?.iframePublicKey, + hasLoggedIn, + loginWithOAuth, + ]) + + return
Redirecting...
+} + +export default function OAuth() { + return ( + Loading...}> + + + ) +} diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index f18076c..8c03037 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -13,7 +13,7 @@ import { Skeleton } from "./ui/skeleton" const AppleAuth = ({ loading }: { loading: boolean }) => { const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID - const redirectURI = env.NEXT_PUBLIC_APP_URL + const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/oauth-callback` const { authIframeClient } = useTurnkey() const { loginWithOAuth } = useAuth() @@ -22,16 +22,6 @@ const AppleAuth = ({ loading }: { loading: boolean }) => { const [storedToken, setStoredToken] = useState(null) // Store the token locally const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called - const searchParams = useSearchParams() - - // Get token from query string params and store in state when available - useEffect(() => { - const token = searchParams.get("id_token") - if (token) { - setStoredToken(token) // Store token if available - } - }, [searchParams]) - // Generate nonce based on iframePublicKey useEffect(() => { if (authIframeClient?.iframePublicKey) { @@ -43,31 +33,13 @@ const AppleAuth = ({ loading }: { loading: boolean }) => { } }, [authIframeClient?.iframePublicKey]) - // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once - useEffect(() => { - if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) { - console.log("doing the thing to the things") - - // Call the OAuth login function with the stored token - loginWithOAuth(storedToken) - - // Set flag to prevent further calls - setHasLoggedIn(true) - } - }, [ - storedToken, - authIframeClient?.iframePublicKey, - hasLoggedIn, - loginWithOAuth, - ]) - return ( <> {nonce ? ( From c868936bf1fd0db88295b21a2fe8f684687fff8f Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Tue, 8 Oct 2024 12:59:16 -0400 Subject: [PATCH 09/18] Full redirect loop working - now just time to tidy up --- src/app/(landing)/oauth-callback/page.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/(landing)/oauth-callback/page.tsx b/src/app/(landing)/oauth-callback/page.tsx index 0e88923..b3ea283 100644 --- a/src/app/(landing)/oauth-callback/page.tsx +++ b/src/app/(landing)/oauth-callback/page.tsx @@ -26,9 +26,13 @@ function OAuthProcessCallback() { // Get token from query string params and store in state when available useEffect(() => { - const token = searchParams.get("id_token") - if (token) { - setStoredToken(token) // Store token if available + const fragment = window.location.hash + if (fragment) { + const params = new URLSearchParams(fragment.slice(1)) // Remove the "#" and parse parameters + const token = params.get("id_token") + if (token) { + setStoredToken(token) // Store token if available + } } }, [searchParams]) From 3e3d227fd804ac0d8a4848516fb2e88267d36a51 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Tue, 8 Oct 2024 13:24:21 -0400 Subject: [PATCH 10/18] Hooking into new helper methods and simplifying component signature --- src/app/(landing)/oauth-callback/page.tsx | 6 +++--- src/components/apple-auth.tsx | 2 +- src/components/auth.tsx | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/(landing)/oauth-callback/page.tsx b/src/app/(landing)/oauth-callback/page.tsx index b3ea283..1ae5f24 100644 --- a/src/app/(landing)/oauth-callback/page.tsx +++ b/src/app/(landing)/oauth-callback/page.tsx @@ -19,7 +19,7 @@ function OAuthProcessCallback() { const searchParams = useSearchParams() const { authIframeClient } = useTurnkey() - const { loginWithOAuth } = useAuth() + const { loginWithApple } = useAuth() const [storedToken, setStoredToken] = useState(null) // Store the token locally const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called @@ -40,7 +40,7 @@ function OAuthProcessCallback() { useEffect(() => { if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) { // Call the OAuth login function with the stored token - loginWithOAuth(storedToken) + loginWithApple(storedToken) // Set flag to prevent further calls setHasLoggedIn(true) @@ -49,7 +49,7 @@ function OAuthProcessCallback() { storedToken, authIframeClient?.iframePublicKey, hasLoggedIn, - loginWithOAuth, + loginWithApple, ]) return
Redirecting...
diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index 8c03037..cd2ae70 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -11,7 +11,7 @@ import { env } from "@/env.mjs" import { Skeleton } from "./ui/skeleton" -const AppleAuth = ({ loading }: { loading: boolean }) => { +const AppleAuth = () => { const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/oauth-callback` diff --git a/src/components/auth.tsx b/src/components/auth.tsx index 9ff2b42..5c6a185 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -145,6 +145,7 @@ export default function Auth() { + From 4af5a9848d04ea0dc5e182a0ea05e24d2d8d0e80 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Tue, 8 Oct 2024 13:26:09 -0400 Subject: [PATCH 11/18] Cleanup --- src/app/(landing)/oauth-callback/page.tsx | 10 ---------- src/components/apple-auth.tsx | 5 ----- 2 files changed, 15 deletions(-) diff --git a/src/app/(landing)/oauth-callback/page.tsx b/src/app/(landing)/oauth-callback/page.tsx index 1ae5f24..e98554e 100644 --- a/src/app/(landing)/oauth-callback/page.tsx +++ b/src/app/(landing)/oauth-callback/page.tsx @@ -4,16 +4,6 @@ import { Suspense, useEffect, useState } from "react" import { useSearchParams } from "next/navigation" import { useAuth } from "@/providers/auth-provider" import { useTurnkey } from "@turnkey/sdk-react" -import { Loader, Send } from "lucide-react" -import { sha256 } from "viem" - -import { - Card, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Icons } from "@/components/icons" function OAuthProcessCallback() { const searchParams = useSearchParams() diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index cd2ae70..faceb3b 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -1,8 +1,6 @@ "use client" import { useEffect, useState } from "react" -import { useSearchParams } from "next/navigation" -import { useAuth } from "@/providers/auth-provider" import { useTurnkey } from "@turnkey/sdk-react" import AppleLogin from "react-apple-login" import { sha256 } from "viem" @@ -16,11 +14,8 @@ const AppleAuth = () => { const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/oauth-callback` const { authIframeClient } = useTurnkey() - const { loginWithOAuth } = useAuth() const [nonce, setNonce] = useState("") - const [storedToken, setStoredToken] = useState(null) // Store the token locally - const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called // Generate nonce based on iframePublicKey useEffect(() => { From 534522474af4dd9a1fc0705f6eb8ced36dba20de Mon Sep 17 00:00:00 2001 From: Taylor Dawson Date: Tue, 8 Oct 2024 19:57:32 -0700 Subject: [PATCH 12/18] Update styles for button and callback page --- src/app/(landing)/oauth-callback/page.tsx | 20 ++++++++++++++++++- src/components/apple-auth.tsx | 24 +++++++++++++++-------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/app/(landing)/oauth-callback/page.tsx b/src/app/(landing)/oauth-callback/page.tsx index e98554e..67441e9 100644 --- a/src/app/(landing)/oauth-callback/page.tsx +++ b/src/app/(landing)/oauth-callback/page.tsx @@ -4,6 +4,10 @@ import { Suspense, useEffect, useState } from "react" import { useSearchParams } from "next/navigation" import { useAuth } from "@/providers/auth-provider" import { useTurnkey } from "@turnkey/sdk-react" +import { Loader } from "lucide-react" + +import { Card, CardHeader, CardTitle } from "@/components/ui/card" +import { Icons } from "@/components/icons" function OAuthProcessCallback() { const searchParams = useSearchParams() @@ -42,7 +46,21 @@ function OAuthProcessCallback() { loginWithApple, ]) - return
Redirecting...
+ return ( +
+ + + + +
+ + Redirecting... +
+
+
+
+
+ ) } export default function OAuth() { diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index faceb3b..6f2e7c4 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -6,12 +6,13 @@ import AppleLogin from "react-apple-login" import { sha256 } from "viem" import { env } from "@/env.mjs" +import { siteConfig } from "@/config/site" import { Skeleton } from "./ui/skeleton" const AppleAuth = () => { const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID - const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/oauth-callback` + const redirectURI = `${siteConfig.url.base}/oauth-callback` const { authIframeClient } = useTurnkey() @@ -31,13 +32,20 @@ const AppleAuth = () => { return ( <> {nonce ? ( - +
+ +
) : ( )} From b0e4b4606be48245f6ea056d786026c6a6d7c6ec Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Wed, 16 Oct 2024 13:18:16 -0400 Subject: [PATCH 13/18] Basic FB integration --- .../facebook-callback/exchange-token.tsx | 44 +++++++++ src/app/(landing)/facebook-callback/page.tsx | 96 +++++++++++++++++++ src/components/auth.tsx | 8 ++ src/components/facebook-auth.tsx | 70 ++++++++++++++ src/env.mjs | 3 + src/providers/auth-provider.tsx | 7 ++ 6 files changed, 228 insertions(+) create mode 100644 src/app/(landing)/facebook-callback/exchange-token.tsx create mode 100644 src/app/(landing)/facebook-callback/page.tsx create mode 100644 src/components/facebook-auth.tsx diff --git a/src/app/(landing)/facebook-callback/exchange-token.tsx b/src/app/(landing)/facebook-callback/exchange-token.tsx new file mode 100644 index 0000000..1680ab2 --- /dev/null +++ b/src/app/(landing)/facebook-callback/exchange-token.tsx @@ -0,0 +1,44 @@ +"use server" + +import { env } from "@/env.mjs" + +export async function exchangeToken(code: string, codeVerifier: string) { + const url = "https://graph.facebook.com/v21.0/oauth/access_token" + + const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/facebook-callback` + const clientID = env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID + + // Create URLSearchParams with the required parameters + const params = new URLSearchParams({ + client_id: clientID, + redirect_uri: redirectURI, + code: code, + code_verifier: codeVerifier, + }) + + try { + const target = `${url}?${params.toString()}` + + // Perform a GET request with the params appended to the URL + const response = await fetch(target, { + method: "GET", + }) + + if (!response.ok) { + throw new Error(`Token exchange failed: ${response.statusText}`) + } + + const data = await response.json() + + // Extract id_token from the response + const idToken = data.id_token + if (!idToken) { + throw new Error("id_token not found in response") + } + + return idToken + } catch (error) { + console.error("Error during token exchange:", error) + throw error + } +} diff --git a/src/app/(landing)/facebook-callback/page.tsx b/src/app/(landing)/facebook-callback/page.tsx new file mode 100644 index 0000000..a1b694f --- /dev/null +++ b/src/app/(landing)/facebook-callback/page.tsx @@ -0,0 +1,96 @@ +"use client" + +import { Suspense, useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import { useAuth } from "@/providers/auth-provider" +import { useTurnkey } from "@turnkey/sdk-react" +import { Loader } from "lucide-react" + +import { Card, CardHeader, CardTitle } from "@/components/ui/card" +import { Icons } from "@/components/icons" + +import { exchangeToken } from "./exchange-token" + +function FacebookProcessCallback() { + const searchParams = useSearchParams() + + const { authIframeClient } = useTurnkey() + const { loginWithFacebook } = useAuth() + + const [storedCode, setStoredCode] = useState(null) // Store the token locally + const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called + + const getToken = async () => { + const codeVerifier = + "lqRVABO1ifZALKrJ3VZAmASUzuulW7sLkfYVhpwlmwPVUPkPmbvkhBlP3t6TgPHlOr6lmbmSZBBz9L2QFRcmOZCVaSiQWZBRsRxn" // Replace with your generated code_challenge + + const token = await exchangeToken(storedCode || "", codeVerifier) + + console.log("got token!") + console.log(token) + + return token + } + + // Get token from query string params and store in state when available + useEffect(() => { + const code = searchParams.get("code") + const verifier = searchParams.get("verifier") + if (code) { + setStoredCode(code) // Store token if available + } + }, [searchParams]) + + // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once + useEffect(() => { + const handleTokenExchange = async () => { + try { + // Get the token asynchronously + const token = await getToken() + + // Perform the Facebook login with the token + loginWithFacebook(token) + + // Set flag to prevent further calls + setHasLoggedIn(true) + } catch (error) { + console.error("Error during token exchange:", error) + } + } + + if (storedCode && authIframeClient?.iframePublicKey && !hasLoggedIn) { + // Call the async handler to exchange the token + handleTokenExchange() + } + }, [ + storedCode, + authIframeClient?.iframePublicKey, + hasLoggedIn, + loginWithFacebook, + getToken, + ]) + + return ( +
+ + + + +
+ + Redirecting... +
+
+
+
+
+ ) +} + +export default function Facebook() { + return ( + Loading...}> + + + ) +} diff --git a/src/components/auth.tsx b/src/components/auth.tsx index 5c6a185..eda6040 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -23,6 +23,7 @@ import { Input } from "@/components/ui/input" import { Separator } from "@/components/ui/separator" import AppleAuth from "./apple-auth" +import FacebookAuth from "./facebook-auth" import GoogleAuth from "./google-auth" import { Icons } from "./icons" import Legal from "./legal" @@ -79,6 +80,12 @@ export default function Auth() { } } + const fbCallback = (resp: any) => { + console.log(resp) + } + + // "EAARXv0lqRVABOzD44ZBXz0SrK5yYoPdyq5sW9zgllG4y0KbPbEGqFUPZCwa82eH34eeT96i2ZBa2J1ufS48OP4xfTOYFdZBSTKECXTUeHjq8eRTT8CYTVZAc9vfzVwfUNwQJGsJKrkOaFS7ouxR6DtcNSElhUUAOf01VbgHxGPklSZB3pRk4WYoBiZC01Tloj9XVRZC98TXI1GRPHbDYrwJpNZB0Sj9a00AZDZD" + return ( <> @@ -146,6 +153,7 @@ export default function Auth() { + diff --git a/src/components/facebook-auth.tsx b/src/components/facebook-auth.tsx new file mode 100644 index 0000000..dae663f --- /dev/null +++ b/src/components/facebook-auth.tsx @@ -0,0 +1,70 @@ +"use client" + +import { useEffect, useState } from "react" +import { useAuth } from "@/providers/auth-provider" +import { useTurnkey } from "@turnkey/sdk-react" +import AppleLogin from "react-apple-login" +import { sha256 } from "viem" + +import { env } from "@/env.mjs" +import { siteConfig } from "@/config/site" + +import { Skeleton } from "./ui/skeleton" + +const FacebookAuth = () => { + const { authIframeClient } = useTurnkey() + + const [nonce, setNonce] = useState("") + + const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/facebook-callback` + + const clientID = env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID + + // Generate nonce based on iframePublicKey + useEffect(() => { + if (authIframeClient?.iframePublicKey) { + const hashedPublicKey = sha256( + authIframeClient.iframePublicKey as `0x${string}` + ).replace(/^0x/, "") + + setNonce(hashedPublicKey) + } + }, [authIframeClient?.iframePublicKey]) + + const redirectToFacebook = () => { + const state = "state123abc" // Replace with a state parameter + const codeVerifier = + "lqRVABO1ifZALKrJ3VZAmASUzuulW7sLkfYVhpwlmwPVUPkPmbvkhBlP3t6TgPHlOr6lmbmSZBBz9L2QFRcmOZCVaSiQWZBRsRxn" // Replace with your generated code_challenge + const codeChallengeMethod = "plain" // The method used to generate the code_challenge + const codeChallenge = codeVerifier + + // Generate the Facebook OAuth URL server-side + const params = new URLSearchParams({ + client_id: clientID, + scope: "openid", + response_type: "code", + redirect_uri: redirectURI, + state: state, + code_challenge: codeChallenge, + code_challenge_method: codeChallengeMethod, + nonce: nonce, + } as any) + + const facebookOAuthURL = `https://www.facebook.com/v11.0/dialog/oauth?${params.toString()}` + window.location.href = facebookOAuthURL + } + + return ( + <> + {nonce ? ( +
+ +
+ ) : ( + + )} + + ) +} + +export default FacebookAuth diff --git a/src/env.mjs b/src/env.mjs index 77bcffe..ff0ee2a 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -12,6 +12,7 @@ export const env = createEnv({ NEXT_PUBLIC_BASE_URL: z.string().min(1), NEXT_PUBLIC_ORGANIZATION_ID: z.string().min(1), NEXT_PUBLIC_ALCHEMY_API_KEY: z.string().min(1), + NEXT_PUBLIC_FACEBOOK_CLIENT_ID: z.string().min(1), }, server: { NEXT_PUBLIC_RP_ID: z.string().optional(), @@ -27,10 +28,12 @@ export const env = createEnv({ TURNKEY_WARCHEST_API_PRIVATE_KEY: z.string().min(1), TURNKEY_WARCHEST_ORGANIZATION_ID: z.string().min(1), WARCHEST_PRIVATE_KEY_ID: z.string().min(1), + NEXT_PUBLIC_FACEBOOK_CLIENT_ID: z.string().min(1), }, runtimeEnv: { NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID: process.env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID, + NEXT_PUBLIC_FACEBOOK_CLIENT_ID: process.env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID, NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID, NEXT_PUBLIC_RP_ID: process.env.NEXT_PUBLIC_RP_ID, diff --git a/src/providers/auth-provider.tsx b/src/providers/auth-provider.tsx index 3ae9102..5f46121 100644 --- a/src/providers/auth-provider.tsx +++ b/src/providers/auth-provider.tsx @@ -98,6 +98,7 @@ const AuthContext = createContext<{ loginWithOAuth: (credential: string, providerName: string) => Promise loginWithGoogle: (credential: string) => Promise loginWithApple: (credential: string) => Promise + loginWithFacebook: (credential: string) => Promise logout: () => Promise }>({ state: initialState, @@ -107,6 +108,7 @@ const AuthContext = createContext<{ loginWithOAuth: async () => {}, loginWithGoogle: async () => {}, loginWithApple: async () => {}, + loginWithFacebook: async () => {}, logout: async () => {}, }) @@ -301,6 +303,10 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { await loginWithOAuth(credential, "Apple Auth - Embedded Wallet") } + const loginWithFacebook = async (credential: string) => { + await loginWithOAuth(credential, "Facebook Auth - Embedded Wallet") + } + const logout = async () => { await turnkey?.logoutUser() googleLogout() @@ -317,6 +323,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { loginWithOAuth, loginWithGoogle, loginWithApple, + loginWithFacebook, logout, }} > From 24d61a67003ee63efe0791528c7a00aac2bd323e Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Wed, 16 Oct 2024 15:22:17 -0700 Subject: [PATCH 14/18] Handling challenge and verifier --- package.json | 1 + pnpm-lock.yaml | 11 ++++++++ src/app/(landing)/facebook-callback/page.tsx | 21 ++++++++++----- src/components/facebook-auth.tsx | 10 +++---- src/env.mjs | 2 ++ src/lib/facebook-utils.ts | 28 ++++++++++++++++++++ 6 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 src/lib/facebook-utils.ts diff --git a/package.json b/package.json index b9c52d0..2fc8191 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react": "^18.3.1", "react-apple-login": "^1.1.6", "react-dom": "^18.3.1", + "react-facebook-login": "^4.1.1", "react-hook-form": "^7.53.0", "react-jazzicon": "^1.0.4", "react-qr-code": "^2.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8867e18..79e599d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ dependencies: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-facebook-login: + specifier: ^4.1.1 + version: 4.1.1(react@18.3.1) react-hook-form: specifier: ^7.53.0 version: 7.53.0(react@18.3.1) @@ -8326,6 +8329,14 @@ packages: scheduler: 0.23.2 dev: false + /react-facebook-login@4.1.1(react@18.3.1): + resolution: {integrity: sha512-COnHEHlYGTKipz4963safFAK9PaNTcCiXfPXMS/yxo8El+/AJL5ye8kMJf23lKSSGGPgqFQuInskIHVqGqTvSw==} + peerDependencies: + react: ^16.0.0 + dependencies: + react: 18.3.1 + dev: false + /react-hook-form@7.53.0(react@18.3.1): resolution: {integrity: sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==} engines: {node: '>=18.0.0'} diff --git a/src/app/(landing)/facebook-callback/page.tsx b/src/app/(landing)/facebook-callback/page.tsx index a1b694f..27df2f4 100644 --- a/src/app/(landing)/facebook-callback/page.tsx +++ b/src/app/(landing)/facebook-callback/page.tsx @@ -9,6 +9,7 @@ import { Loader } from "lucide-react" import { Card, CardHeader, CardTitle } from "@/components/ui/card" import { Icons } from "@/components/icons" +import { verifierSegmentToChallenge } from "../../../lib/facebook-utils" import { exchangeToken } from "./exchange-token" function FacebookProcessCallback() { @@ -18,13 +19,12 @@ function FacebookProcessCallback() { const { loginWithFacebook } = useAuth() const [storedCode, setStoredCode] = useState(null) // Store the token locally + const [storedState, setStoredState] = useState(null) // Store the token locally const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called const getToken = async () => { - const codeVerifier = - "lqRVABO1ifZALKrJ3VZAmASUzuulW7sLkfYVhpwlmwPVUPkPmbvkhBlP3t6TgPHlOr6lmbmSZBBz9L2QFRcmOZCVaSiQWZBRsRxn" // Replace with your generated code_challenge - - const token = await exchangeToken(storedCode || "", codeVerifier) + const verifier = await verifierSegmentToChallenge(storedState || "") + const token = await exchangeToken(storedCode || "", verifier) console.log("got token!") console.log(token) @@ -35,10 +35,13 @@ function FacebookProcessCallback() { // Get token from query string params and store in state when available useEffect(() => { const code = searchParams.get("code") - const verifier = searchParams.get("verifier") + const state = searchParams.get("state") if (code) { setStoredCode(code) // Store token if available } + if (state) { + setStoredState(state) // Store token if available + } }, [searchParams]) // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once @@ -58,12 +61,18 @@ function FacebookProcessCallback() { } } - if (storedCode && authIframeClient?.iframePublicKey && !hasLoggedIn) { + if ( + storedCode && + storedState && + authIframeClient?.iframePublicKey && + !hasLoggedIn + ) { // Call the async handler to exchange the token handleTokenExchange() } }, [ storedCode, + storedState, authIframeClient?.iframePublicKey, hasLoggedIn, loginWithFacebook, diff --git a/src/components/facebook-auth.tsx b/src/components/facebook-auth.tsx index dae663f..d8a2105 100644 --- a/src/components/facebook-auth.tsx +++ b/src/components/facebook-auth.tsx @@ -9,6 +9,7 @@ import { sha256 } from "viem" import { env } from "@/env.mjs" import { siteConfig } from "@/config/site" +import { generateChallengePair } from "../lib/facebook-utils" import { Skeleton } from "./ui/skeleton" const FacebookAuth = () => { @@ -31,12 +32,11 @@ const FacebookAuth = () => { } }, [authIframeClient?.iframePublicKey]) - const redirectToFacebook = () => { + const redirectToFacebook = async () => { + const { verifier, codeChallenge } = await generateChallengePair() + const state = "state123abc" // Replace with a state parameter - const codeVerifier = - "lqRVABO1ifZALKrJ3VZAmASUzuulW7sLkfYVhpwlmwPVUPkPmbvkhBlP3t6TgPHlOr6lmbmSZBBz9L2QFRcmOZCVaSiQWZBRsRxn" // Replace with your generated code_challenge const codeChallengeMethod = "plain" // The method used to generate the code_challenge - const codeChallenge = codeVerifier // Generate the Facebook OAuth URL server-side const params = new URLSearchParams({ @@ -44,7 +44,7 @@ const FacebookAuth = () => { scope: "openid", response_type: "code", redirect_uri: redirectURI, - state: state, + state: verifier, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod, nonce: nonce, diff --git a/src/env.mjs b/src/env.mjs index ff0ee2a..d205353 100644 --- a/src/env.mjs +++ b/src/env.mjs @@ -18,6 +18,7 @@ export const env = createEnv({ NEXT_PUBLIC_RP_ID: z.string().optional(), NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(), NEXT_PUBLIC_APP_URL: z.string().optional(), + FACEBOOK_SECRET_SALT: z.string().min(1), TURNKEY_API_PUBLIC_KEY: z.string().min(1), TURNKEY_API_PRIVATE_KEY: z.string().min(1), NEXT_PUBLIC_BASE_URL: z.string().min(1), @@ -41,6 +42,7 @@ export const env = createEnv({ process.env.NEXT_PUBLIC_VERCEL_URL, NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL, + FACEBOOK_SECRET_SALT: process.env.FACEBOOK_SECRET_SALT, TURNKEY_API_PUBLIC_KEY: process.env.TURNKEY_API_PUBLIC_KEY, TURNKEY_API_PRIVATE_KEY: process.env.TURNKEY_API_PRIVATE_KEY, NEXT_PUBLIC_ORGANIZATION_ID: process.env.NEXT_PUBLIC_ORGANIZATION_ID, diff --git a/src/lib/facebook-utils.ts b/src/lib/facebook-utils.ts new file mode 100644 index 0000000..6e3edf4 --- /dev/null +++ b/src/lib/facebook-utils.ts @@ -0,0 +1,28 @@ +"use server" + +import crypto from "crypto" + +import { env } from "@/env.mjs" + +export const generateChallengePair = async (): Promise<{ + verifier: string + codeChallenge: string +}> => { + // Step 1: Generate a random 48-character verifier + const verifier = crypto.randomBytes(32).toString("base64url") // URL-safe Base64 string + + const codeChallenge = await verifierSegmentToChallenge(verifier) + + // Return both the verifier and the codeChallenge to the client + return { verifier, codeChallenge } +} + +export const verifierSegmentToChallenge = async ( + segment: string +): Promise => { + const salt = env.FACEBOOK_SECRET_SALT + const saltedVerifier = segment + salt + + // Step 3: Hash the salted verifier using SHA-256 + return crypto.createHash("sha256").update(saltedVerifier).digest("base64url") +} From 4256b6a2f69d48cef9f57f1553cb07ef6cb250bb Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Wed, 16 Oct 2024 15:31:58 -0700 Subject: [PATCH 15/18] Cleanup --- .env.example | 6 +++++- .../(landing)/facebook-callback/exchange-token.tsx | 3 --- src/app/(landing)/facebook-callback/page.tsx | 13 +++++-------- src/components/auth.tsx | 6 ------ src/components/facebook-auth.tsx | 11 ++++------- 5 files changed, 14 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 78874f3..18137f5 100644 --- a/.env.example +++ b/.env.example @@ -4,10 +4,14 @@ NEXT_PUBLIC_ORGANIZATION_ID= NEXT_PUBLIC_RP_ID= NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID= NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID= +NEXT_PUBLIC_FACEBOOK_CLIENT_ID= # Sensitive values ideally for server only ALCHEMY_API_KEY= COINGECKO_API_KEY= TURNKEY_API_PUBLIC_KEY= -TURNKEY_API_PRIVATE_KEY= \ No newline at end of file +TURNKEY_API_PRIVATE_KEY= + +# randomly generated secret +FACEBOOK_SECRET_SALT= \ No newline at end of file diff --git a/src/app/(landing)/facebook-callback/exchange-token.tsx b/src/app/(landing)/facebook-callback/exchange-token.tsx index 1680ab2..73ea4aa 100644 --- a/src/app/(landing)/facebook-callback/exchange-token.tsx +++ b/src/app/(landing)/facebook-callback/exchange-token.tsx @@ -8,7 +8,6 @@ export async function exchangeToken(code: string, codeVerifier: string) { const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/facebook-callback` const clientID = env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID - // Create URLSearchParams with the required parameters const params = new URLSearchParams({ client_id: clientID, redirect_uri: redirectURI, @@ -19,7 +18,6 @@ export async function exchangeToken(code: string, codeVerifier: string) { try { const target = `${url}?${params.toString()}` - // Perform a GET request with the params appended to the URL const response = await fetch(target, { method: "GET", }) @@ -38,7 +36,6 @@ export async function exchangeToken(code: string, codeVerifier: string) { return idToken } catch (error) { - console.error("Error during token exchange:", error) throw error } } diff --git a/src/app/(landing)/facebook-callback/page.tsx b/src/app/(landing)/facebook-callback/page.tsx index 27df2f4..1d20de0 100644 --- a/src/app/(landing)/facebook-callback/page.tsx +++ b/src/app/(landing)/facebook-callback/page.tsx @@ -18,17 +18,14 @@ function FacebookProcessCallback() { const { authIframeClient } = useTurnkey() const { loginWithFacebook } = useAuth() - const [storedCode, setStoredCode] = useState(null) // Store the token locally - const [storedState, setStoredState] = useState(null) // Store the token locally - const [hasLoggedIn, setHasLoggedIn] = useState(false) // Track if loginWithOAuth has been called + const [storedCode, setStoredCode] = useState(null) + const [storedState, setStoredState] = useState(null) + const [hasLoggedIn, setHasLoggedIn] = useState(false) const getToken = async () => { const verifier = await verifierSegmentToChallenge(storedState || "") const token = await exchangeToken(storedCode || "", verifier) - console.log("got token!") - console.log(token) - return token } @@ -37,10 +34,10 @@ function FacebookProcessCallback() { const code = searchParams.get("code") const state = searchParams.get("state") if (code) { - setStoredCode(code) // Store token if available + setStoredCode(code) } if (state) { - setStoredState(state) // Store token if available + setStoredState(state) } }, [searchParams]) diff --git a/src/components/auth.tsx b/src/components/auth.tsx index eda6040..aed23ba 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -80,12 +80,6 @@ export default function Auth() { } } - const fbCallback = (resp: any) => { - console.log(resp) - } - - // "EAARXv0lqRVABOzD44ZBXz0SrK5yYoPdyq5sW9zgllG4y0KbPbEGqFUPZCwa82eH34eeT96i2ZBa2J1ufS48OP4xfTOYFdZBSTKECXTUeHjq8eRTT8CYTVZAc9vfzVwfUNwQJGsJKrkOaFS7ouxR6DtcNSElhUUAOf01VbgHxGPklSZB3pRk4WYoBiZC01Tloj9XVRZC98TXI1GRPHbDYrwJpNZB0Sj9a00AZDZD" - return ( <> diff --git a/src/components/facebook-auth.tsx b/src/components/facebook-auth.tsx index d8a2105..e37c9eb 100644 --- a/src/components/facebook-auth.tsx +++ b/src/components/facebook-auth.tsx @@ -1,9 +1,7 @@ "use client" import { useEffect, useState } from "react" -import { useAuth } from "@/providers/auth-provider" import { useTurnkey } from "@turnkey/sdk-react" -import AppleLogin from "react-apple-login" import { sha256 } from "viem" import { env } from "@/env.mjs" @@ -17,7 +15,7 @@ const FacebookAuth = () => { const [nonce, setNonce] = useState("") - const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/facebook-callback` + const redirectURI = `${siteConfig.url.base}/facebook-callback` const clientID = env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID @@ -35,19 +33,18 @@ const FacebookAuth = () => { const redirectToFacebook = async () => { const { verifier, codeChallenge } = await generateChallengePair() - const state = "state123abc" // Replace with a state parameter - const codeChallengeMethod = "plain" // The method used to generate the code_challenge + const codeChallengeMethod = "sha256" // Generate the Facebook OAuth URL server-side const params = new URLSearchParams({ client_id: clientID, - scope: "openid", - response_type: "code", redirect_uri: redirectURI, state: verifier, code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod, nonce: nonce, + scope: "openid", + response_type: "code", } as any) const facebookOAuthURL = `https://www.facebook.com/v11.0/dialog/oauth?${params.toString()}` From c66cc4e0d2fe132f82925d2cf9e1971b29511310 Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Wed, 16 Oct 2024 15:53:25 -0700 Subject: [PATCH 16/18] Logging for debugging --- src/app/(landing)/facebook-callback/exchange-token.tsx | 4 ++++ src/app/(landing)/facebook-callback/page.tsx | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/app/(landing)/facebook-callback/exchange-token.tsx b/src/app/(landing)/facebook-callback/exchange-token.tsx index 73ea4aa..5a41d4a 100644 --- a/src/app/(landing)/facebook-callback/exchange-token.tsx +++ b/src/app/(landing)/facebook-callback/exchange-token.tsx @@ -18,6 +18,10 @@ export async function exchangeToken(code: string, codeVerifier: string) { try { const target = `${url}?${params.toString()}` + console.log(target) + + return "" + const response = await fetch(target, { method: "GET", }) diff --git a/src/app/(landing)/facebook-callback/page.tsx b/src/app/(landing)/facebook-callback/page.tsx index 1d20de0..06cabde 100644 --- a/src/app/(landing)/facebook-callback/page.tsx +++ b/src/app/(landing)/facebook-callback/page.tsx @@ -49,10 +49,10 @@ function FacebookProcessCallback() { const token = await getToken() // Perform the Facebook login with the token - loginWithFacebook(token) + // loginWithFacebook(token) - // Set flag to prevent further calls - setHasLoggedIn(true) + // // Set flag to prevent further calls + // setHasLoggedIn(true) } catch (error) { console.error("Error during token exchange:", error) } From c84fad5a64730bc39db2c967d5984a219bf5ccdc Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Wed, 16 Oct 2024 16:03:56 -0700 Subject: [PATCH 17/18] Revert "Logging for debugging" This reverts commit c66cc4e0d2fe132f82925d2cf9e1971b29511310. --- src/app/(landing)/facebook-callback/exchange-token.tsx | 4 ---- src/app/(landing)/facebook-callback/page.tsx | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app/(landing)/facebook-callback/exchange-token.tsx b/src/app/(landing)/facebook-callback/exchange-token.tsx index 5a41d4a..73ea4aa 100644 --- a/src/app/(landing)/facebook-callback/exchange-token.tsx +++ b/src/app/(landing)/facebook-callback/exchange-token.tsx @@ -18,10 +18,6 @@ export async function exchangeToken(code: string, codeVerifier: string) { try { const target = `${url}?${params.toString()}` - console.log(target) - - return "" - const response = await fetch(target, { method: "GET", }) diff --git a/src/app/(landing)/facebook-callback/page.tsx b/src/app/(landing)/facebook-callback/page.tsx index 06cabde..1d20de0 100644 --- a/src/app/(landing)/facebook-callback/page.tsx +++ b/src/app/(landing)/facebook-callback/page.tsx @@ -49,10 +49,10 @@ function FacebookProcessCallback() { const token = await getToken() // Perform the Facebook login with the token - // loginWithFacebook(token) + loginWithFacebook(token) - // // Set flag to prevent further calls - // setHasLoggedIn(true) + // Set flag to prevent further calls + setHasLoggedIn(true) } catch (error) { console.error("Error during token exchange:", error) } From 9f7762cb82a136f99e4cabd1161f45fa18dcaa5a Mon Sep 17 00:00:00 2001 From: Robin Arenson Date: Wed, 16 Oct 2024 16:04:37 -0700 Subject: [PATCH 18/18] Pull redirect correctly --- src/app/(landing)/facebook-callback/exchange-token.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/(landing)/facebook-callback/exchange-token.tsx b/src/app/(landing)/facebook-callback/exchange-token.tsx index 73ea4aa..a8a258a 100644 --- a/src/app/(landing)/facebook-callback/exchange-token.tsx +++ b/src/app/(landing)/facebook-callback/exchange-token.tsx @@ -1,12 +1,13 @@ "use server" import { env } from "@/env.mjs" +import { siteConfig } from "@/config/site" export async function exchangeToken(code: string, codeVerifier: string) { const url = "https://graph.facebook.com/v21.0/oauth/access_token" - const redirectURI = `${env.NEXT_PUBLIC_APP_URL}/facebook-callback` const clientID = env.NEXT_PUBLIC_FACEBOOK_CLIENT_ID + const redirectURI = `${siteConfig.url.base}/facebook-callback` const params = new URLSearchParams({ client_id: clientID,