Compare commits

..

8 commits

5 changed files with 39 additions and 15 deletions

View file

@ -23,7 +23,7 @@ export default function LoginForm() {
required required
/> />
<Input <Input
label='Utiliser une Passkey' label='Utiliser une Passkey si disponible'
name='passkey' name='passkey'
type='checkbox' type='checkbox'
checked checked

View file

@ -4,7 +4,7 @@ import 'npm:iterator-polyfill'
import { FreshContext } from '$fresh/server.ts' import { FreshContext } from '$fresh/server.ts'
import { Contact, type Mail, send } from '@cohabit/cohamail/mod.ts' import { Contact, type Mail, send } from '@cohabit/cohamail/mod.ts'
import { magicLinkTemplate } from '@cohabit/cohamail/templates/mod.ts' import { magicLinkTemplate } from '@cohabit/cohamail/templates/mod.ts'
import { SessionHandlers } from '../../../src/session/mod.ts' import { SessionHandlers, SessionStore } from '../../../src/session/mod.ts'
import { respondApi } from '../../../src/utils.ts' import { respondApi } from '../../../src/utils.ts'
import { sleep } from '@jotsr/delayed' import { sleep } from '@jotsr/delayed'
import { User } from '@cohabit/ressources_manager/src/models/mod.ts' import { User } from '@cohabit/ressources_manager/src/models/mod.ts'
@ -39,19 +39,21 @@ export const handler: SessionHandlers = {
// generate magic link // generate magic link
const token = crypto.randomUUID() const token = crypto.randomUUID()
const endpoint = const endpoint =
`${ctx.url.origin}/api/magiclink?token=${token}&redirect=/profil` `${ctx.url.origin}/api/magiclink?token=${token}&session=${ctx.state.session.uuid}&redirect=/profil`
// save token to session // save token to session
ctx.state.session.flash<MagicLinkInfos>(`MAGIC_LINK__${token}`, { ctx.state.session.flash<MagicLinkInfos>(`MAGIC_LINK__${token}`, {
email, email,
remoteId: remoteId(ctx), remoteId: remoteId(request, ctx),
timestamp: Date.now(), timestamp: Date.now(),
}) })
// send mail to user // send mail to user
try { try {
if (user) { if (user) {
const ip = ctx.remoteAddr.hostname // Get user ip through proxy else from tcp connection
const ip = request.headers.get('X-FORWARDED-FOR') ??
ctx.remoteAddr.hostname
const device = request.headers.get('Sec-Ch-Ua-Platform') ?? undefined const device = request.headers.get('Sec-Ch-Ua-Platform') ?? undefined
await sendMagicLink(user, { device, ip, endpoint }) await sendMagicLink(user, { device, ip, endpoint })
@ -69,14 +71,24 @@ export const handler: SessionHandlers = {
) )
} }
}, },
async GET(_request, ctx) { async GET(request, ctx) {
const token = ctx.url.searchParams.get('token') const token = ctx.url.searchParams.get('token')
const redirect = ctx.url.searchParams.get('redirect') const redirect = ctx.url.searchParams.get('redirect')
const sessionId = ctx.url.searchParams.get('session')
// no token // no token or sessionId
if (token === null) { if (token === null || sessionId === null) {
return respondApi('error', 'no token provided', 400) return respondApi('error', 'no token or session provided', 400)
} }
// set session if 3rd party cookies was blocked
ctx.state.session = ctx.state.session ?? SessionStore.getSession(sessionId)
// no session available
if (ctx.state.session === null) {
return respondApi('error', 'no session datas', 401)
}
// wrong or timeout token // wrong or timeout token
const entry = ctx.state.session.get<MagicLinkInfos>(`MAGIC_LINK__${token}`) const entry = ctx.state.session.get<MagicLinkInfos>(`MAGIC_LINK__${token}`)
@ -87,12 +99,12 @@ export const handler: SessionHandlers = {
} }
// check remote id (same user/machine that has query the token) // check remote id (same user/machine that has query the token)
if (entry.remoteId === remoteId(ctx)) { if (entry.remoteId === remoteId(request, ctx)) {
const user = await getUserByMail(entry.email) const user = await getUserByMail(entry.email)
ctx.state.session.set('user', user) ctx.state.session.set('user', user)
if (redirect) { if (redirect) {
return Response.redirect(new URL(redirect, ctx.basePath)) return Response.redirect(new URL(redirect, ctx.url.origin))
} }
return respondApi('success', user) return respondApi('success', user)
@ -109,8 +121,16 @@ export const handler: SessionHandlers = {
} }
function remoteId( function remoteId(
{ headers }: { headers: Headers },
{ remoteAddr }: { remoteAddr: FreshContext['remoteAddr'] }, { remoteAddr }: { remoteAddr: FreshContext['remoteAddr'] },
): string { ): string {
const forwardedAddress = headers.get('X-FORWARDED-FOR')
const forwardedProto = headers.get('X-FORWARDED-PROTO')
if (forwardedAddress && forwardedProto) {
return `${forwardedProto}://${forwardedAddress}`
}
return `(${remoteAddr.transport}):${remoteAddr.hostname}:${remoteAddr.port}` return `(${remoteAddr.transport}):${remoteAddr.hostname}:${remoteAddr.port}`
} }

View file

@ -118,8 +118,7 @@ export const handler: SessionHandlers = {
} }
const { newCounter } = authenticationInfo const { newCounter } = authenticationInfo
const newPasskey = { ...passkey, counter: newCounter }
passkey.counter = newCounter
// Update credential store // Update credential store
const [credential] = await db.ressource.credential.list( const [credential] = await db.ressource.credential.list(
@ -133,7 +132,7 @@ export const handler: SessionHandlers = {
// Save credential to db // Save credential to db
await db.ressource.credential.set([ await db.ressource.credential.set([
credential.update({ store: passkey }), credential.update({ store: newPasskey }),
]) ])
// log user // log user

View file

@ -54,7 +54,7 @@ export const handler: SessionHandlers = {
const options = await generateRegistrationOptions({ const options = await generateRegistrationOptions({
rpName: relyingParty.name, rpName: relyingParty.name,
rpID: relyingParty.id, rpID: relyingParty.origin,
userName: user.login, userName: user.login,
attestationType: 'none', attestationType: 'none',
excludeCredentials, excludeCredentials,

View file

@ -1,6 +1,11 @@
export function getRelyingParty(url: string | URL) { export function getRelyingParty(url: string | URL) {
url = new URL(url) url = new URL(url)
// Prevent protocol mismatch when server is behind a proxy
if (!['localhost', '127.0.0.1', '0.0.0.0'].includes(url.hostname)) {
url.protocol = 'https'
}
return { return {
/** /**
* Human-readable title for your website * Human-readable title for your website