Skip to content

Commit f959c7f

Browse files
committed
wip One-click single sign on
1 parent 2a9297d commit f959c7f

File tree

3 files changed

+91
-79
lines changed

3 files changed

+91
-79
lines changed

components/nav/common.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import NoteIcon from '../../svgs/notification-4-fill.svg'
1212
import { useMe } from '../me'
1313
import { abbrNum } from '../../lib/format'
1414
import { useServiceWorker } from '../serviceworker'
15-
import { signOut } from 'next-auth/react'
15+
import { signIn, signOut } from 'next-auth/react'
1616
import Badges from '../badge'
1717
import { randInRange } from '../../lib/rand'
1818
import { useLightning } from '../lightning'
@@ -248,18 +248,45 @@ export function SignUpButton ({ className = 'py-0', width }) {
248248

249249
export default function LoginButton () {
250250
const router = useRouter()
251-
const handleLogin = useCallback(async pathname => await router.push({
252-
pathname,
253-
query: { callbackUrl: window.location.origin + router.asPath }
254-
}), [router])
251+
252+
useEffect(() => {
253+
if (router.query.type === 'sync') {
254+
console.log('signing in with sync')
255+
console.log('token', router.query.token)
256+
console.log('callbackUrl', router.query.callbackUrl)
257+
signIn('sync', { token: router.query.token, callbackUrl: router.query.callbackUrl, redirect: false })
258+
}
259+
}, [router.query.type, router.query.token, router.query.callbackUrl])
260+
261+
const handleLogin = useCallback(async () => {
262+
// todo: custom domain check
263+
const mainDomain = process.env.NEXT_PUBLIC_URL.replace(/^https?:\/\//, '')
264+
const isCustomDomain = window.location.hostname !== mainDomain
265+
266+
if (isCustomDomain && router.query.type !== 'noAuth') {
267+
// TODO: dirty of previous iterations, refactor
268+
// redirect to sync endpoint on main domain
269+
const protocol = window.location.protocol
270+
const mainDomainUrl = `${protocol}//${mainDomain}`
271+
const currentUrl = window.location.origin + router.asPath
272+
273+
window.location.href = `${mainDomainUrl}/api/auth/sync?redirectUrl=${encodeURIComponent(currentUrl)}`
274+
} else {
275+
// normal login on main domain
276+
await router.push({
277+
pathname: '/login',
278+
query: { callbackUrl: window.location.origin + router.asPath }
279+
})
280+
}
281+
}, [router])
255282

256283
return (
257284
<Button
258285
className='align-items-center px-3 py-1'
259286
id='login'
260287
style={{ borderWidth: '2px', width: '150px' }}
261288
variant='outline-grey-darkmode'
262-
onClick={() => handleLogin('/login')}
289+
onClick={handleLogin}
263290
>
264291
login
265292
</Button>

pages/api/auth/[...nextauth].js

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,38 @@ const getProviders = res => [
274274
return await pubkeyAuth(credentials, req, res, 'nostrAuthPubkey')
275275
}
276276
}),
277+
CredentialsProvider({
278+
id: 'sync',
279+
name: 'Auth Sync',
280+
credentials: {
281+
token: { label: 'token', type: 'text' }
282+
},
283+
authorize: async ({ token }, req) => {
284+
try {
285+
const verificationToken = await prisma.verificationToken.findUnique({ where: { token } })
286+
if (!verificationToken) return null
287+
288+
// has to be a sync token
289+
if (!verificationToken.identifier.startsWith('sync:')) return null
290+
291+
// sync has user id
292+
const userId = parseInt(verificationToken.identifier.split(':')[1], 10)
293+
if (!userId) return null
294+
295+
// delete the token to prevent reuse
296+
await prisma.verificationToken.delete({
297+
where: { id: verificationToken.id }
298+
})
299+
if (new Date() > verificationToken.expires) return null
300+
301+
// return the user
302+
return await prisma.user.findUnique({ where: { id: userId } })
303+
} catch (error) {
304+
console.error('auth sync error:', error)
305+
return null
306+
}
307+
}
308+
}),
277309
GitHubProvider({
278310
clientId: process.env.GITHUB_ID,
279311
clientSecret: process.env.GITHUB_SECRET,
@@ -431,7 +463,7 @@ export default async (req, res) => {
431463
await NextAuth(req, res, getAuthOptions(req, res))
432464
}
433465

434-
function generateRandomString (length = 6, charset = BECH32_CHARSET) {
466+
export function generateRandomString (length = 6, charset = BECH32_CHARSET) {
435467
const bytes = randomBytes(length)
436468
let result = ''
437469

pages/api/auth/sync.js

Lines changed: 25 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,38 @@
11
import { getServerSession } from 'next-auth/next'
2-
import { getAuthOptions } from './[...nextauth]'
3-
import { serialize } from 'cookie'
4-
import { datePivot } from '@/lib/time'
2+
import { getAuthOptions, generateRandomString } from './[...nextauth]'
3+
import prisma from '@/api/models'
54

6-
// TODO: dirty of previous iterations, refactor
7-
// UNSAFE UNSAFE UNSAFE tokens are visible in the URL
85
export default async function handler (req, res) {
9-
console.log(req.query)
10-
if (req.query.token) {
11-
const session = JSON.parse(decodeURIComponent(req.query.token))
12-
return saveCookie(req, res, session)
13-
} else {
14-
const { redirectUrl } = req.query
15-
const session = await getServerSession(req, res, getAuthOptions(req))
16-
// TODO: use session to create a verification token
17-
if (session) {
18-
console.log('session', session)
19-
console.log('req.cookies', req.cookies)
20-
21-
const userId = session.user.id
22-
const multiAuthCookieName = `multi_auth.${userId}`
23-
const multiAuthToken = req.cookies[multiAuthCookieName]
24-
25-
if (!multiAuthToken) {
26-
console.error('No multi_auth token found for user', userId)
27-
return res.status(400).json({ error: 'No multi_auth token found' })
28-
}
29-
30-
const transferData = {
31-
session,
32-
multiAuthToken,
33-
userId
34-
}
35-
36-
// redirect back to the custom domain with the token data
37-
const callbackUrl = new URL('/api/auth/sync', redirectUrl)
38-
callbackUrl.searchParams.set('token', encodeURIComponent(JSON.stringify(transferData)))
39-
callbackUrl.searchParams.set('redirectUrl', req.query.redirectUrl || '/')
40-
41-
return res.redirect(callbackUrl.toString())
42-
}
43-
return res.redirect(redirectUrl)
6+
const { redirectUrl } = req.query
7+
if (!redirectUrl) {
8+
return res.status(400).json({ error: 'Missing redirectUrl parameter' })
449
}
45-
}
4610

47-
export async function saveCookie (req, res, tokenData) {
48-
if (!tokenData) {
49-
return res.status(400).json({ error: 'Missing token' })
11+
const session = await getServerSession(req, res, getAuthOptions(req, res))
12+
13+
if (!session) {
14+
// TODO: redirect to login page, this goes to login overlapping other paths
15+
return res.redirect(redirectUrl + '/login?callbackUrl=' + encodeURIComponent(redirectUrl))
5016
}
5117

5218
try {
53-
const secure = process.env.NODE_ENV === 'development'
54-
const expiresAt = datePivot(new Date(), { months: 1 })
55-
const cookieOptions = {
56-
path: '/',
57-
httpOnly: true,
58-
secure,
59-
sameSite: 'lax',
60-
expires: expiresAt
61-
}
62-
// extract the data from the token
63-
const { multiAuthToken, userId } = tokenData
64-
console.log('Received session and multi_auth token for user', userId)
65-
66-
// set the session cookie
67-
const sessionCookieName = secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
68-
// create cookies
69-
const sessionCookie = serialize(sessionCookieName, multiAuthToken, cookieOptions)
70-
// also set the multi_auth cookie on the custom domain
71-
const multiAuthCookie = serialize(`multi_auth.${userId}`, multiAuthToken, cookieOptions)
72-
// set the cookie pointer
73-
const pointerCookie = serialize('multi_auth.user-id', userId, cookieOptions)
19+
const token = generateRandomString()
20+
// create a sync token
21+
await prisma.verificationToken.create({
22+
data: {
23+
identifier: `sync:${session.user.id}`,
24+
token,
25+
expires: new Date(Date.now() + 1 * 60 * 1000) // 1 minute
26+
}
27+
})
7428

75-
// set the cookies in the response
76-
res.setHeader('Set-Cookie', [sessionCookie, multiAuthCookie, pointerCookie])
29+
const customDomainCallback = new URL('/?type=sync', redirectUrl)
30+
customDomainCallback.searchParams.set('token', token)
31+
customDomainCallback.searchParams.set('callbackUrl', redirectUrl)
7732

78-
// redirect to the home page or a specified return URL
79-
const returnTo = req.query.redirectUrl || '/'
80-
return res.redirect(returnTo)
33+
return res.redirect(customDomainCallback.toString())
8134
} catch (error) {
82-
console.error('Error processing auth callback:', error)
83-
return res.status(500).json({ error: 'Failed to process authentication' })
35+
console.error('Error generating token:', error)
36+
return res.status(500).json({ error: 'Failed to generate token' })
8437
}
8538
}

0 commit comments

Comments
 (0)