-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request: passwordless authentication with email magic links #119
Comments
This is planned, but for now it is a low priority (I will post soon the roadmap for v1.0.0). |
Please add the Magic link sign-in method. it will be awoseome. |
Please do not add magic link in a first place, add an OTP code system instead. Magic link authenticates you on the current device you're using, so it's not really nice when you're checking the email on your phone but want to authenticate on Desktop. |
@ganigeorgiev I'd like to take a stab at a PR in the coming weeks. A few questions: I would call the feature MagicLink, so forms, tokens would be named something like The flow would be:
Then later it's a matter of updating the sdks, there's two approaches I can think of:
|
@fdidron "Step 5" is not enough, because we'll need to send back the authenticated user data to the application (web or mobile), which currently is not possible, so in other words - the "confirm" endpoint must be a user defined one, so that they can handle the magic-link-token->auth-token exchange. In any case, let's leave this feature aside for now. We can pick it again after the users refactoring in #376. |
@ganigeorgiev I hadn't seen that refactoring proposal, indeed it's better to wait for it before working on magic links. Will keep an eye on the release and will resume the conversation then. As an aside, I love the idea of having one user entity and leave the profile implementation details to developers. |
Funny, I had such a system on my own backend I built from scratch when I was learning Go, but since I had a months long break I would first need to understand my own code again and then @ganigeorgiev s as well to submit a PR for this 😅 |
Having wrestled with this in the past, I want to point out a couple of things that might save people time and frustration. You can either make the login link stand-alone and valid on any client, or limit its validity to the client that requested it, typically by setting a cookie when the link is requested and verifying it when the link is submitted. This link+cookie strategy can break in several ways:
The stand-alone link isn't without problems:
The above is why I nowadays prefer magic codes, which can provide the security of the link+cookie strategy without the brittleness, and with a reasonable cognitive cost from the user. |
Really hoping for OTP soon! |
A 6 digit code that expires in 15 minutes should be great |
This comment was marked as spam.
This comment was marked as spam.
Is there some "user-space" workarounds we can use for now to implement this on the application side? Or some way to extend the existing authentication methods? |
@khromov I think it's possible to do this in userland but I haven't tried it, you could also try writing some go/js and extend pocketbase directly |
I was just introduced to PocketBase (thanks, HN) and am very interested in seeing about moving the backend for a Flutter app to it. My app currently uses email + OTP (not clickable magic links), so the very first thing I wanted to do was see if an OTP auth for PocketBase could be created. The POC seemed to work, and I've shared the Go code and a few Dart excerpts in a gist. It was pretty straightforward, but with my ~3 hours of experience with PocketBase I don't know if I'm doing something bad, especially with the manual auth elements. I'd be keen to hear any comments from @ganigeorgiev on the correctness of the approach. cc @ghostdevv @khromov re: a user-space example. |
@kalafut From a brief look it looks fine to me. Some nitpics:
|
@ganigeorgiev Thanks for the quick review and pointers to some other PocketBase methods. Agree with all the feedback. I do have a todo around a cron task to tidy up the otp table, but didn't get to writing that bit yet (seemed pretty straightforward...) Thanks again for this project! I'm surprised I'd not bumped into it earlier. The Go + Dart/Flutter + SQLite stack is essentially what I'm running today, so it is a great fit. |
Thanks for that -- https://gist.github.com/matevarga/daf2273ddea0644ba6a6e91ae22442e0 slight extension that enables user creation. |
Hey @matevarga and @ganigeorgiev would you consider this extension in the gist usable as it is now or do you have some plans to merge soon? I am wondering how best to call this on my JS frontend, or am I better to wrap in a server side endpoint. |
I didn't plan to raise a PR (maybe I should?). The gist kinda works, there's one thing that was definitely missing -- after line 20, there should be Which is, in some way, possibly a bug? (@ganigeorgiev let me know if you think it's a bug, I'll raise a PR for the fix). |
Well I'm working on "the worlds fastest research survey builder" (maybe not with me as the lead engineer lmao, but thats the marketing hook :) ) And anyways when the user pays during the stripe step, I want to just create them an account. And then if they want to revisit there survey in future they can just login, but I dont want to ask them a password, as it just feels like friction and adding an email during payment just seems normal (you want a receipt) Anyways my workaround ideas to achieve this were as follows
If not available / usable in pocketbase:
-- To make the former passwordless approach work
In the latter alternative approach
|
@matevarga Just for info - the "passwordHash-tokenKey" thing is expected behavior. With the planned refactoring there will be a dedicated |
Great, thanks ! |
I will raise a PR after the planned refactoring, although you can already use the code as it is. It works well. |
I love PocketBase. My only complaint is lack of support for passwordless authentication, in particular 6-digit OTP via email. This feature would be greatly appreciated! ❤️🙏 |
Make a hook to send the otp/magiclink. Because, i need to send the otp/link to whatsapp or telegram. |
JS hook implementation. This one creates a user record if it doesn't exist, and assigns a random password. /// <reference path="../types.d.ts" />
routerAdd('POST', '/api/otp/auth', (c) => {
const dao = $app.dao()
const parsed = (() => {
const rawBody = readerToString(c.request().body)
try {
const parsed = JSON.parse(rawBody)
return parsed
} catch (e) {
throw new BadRequestError(
`Error parsing payload. You call this JSON? ${rawBody}`,
e,
)
}
})()
const email = parsed.email?.trim()
console.log(`email: ${email} `)
const code = $security.randomStringWithAlphabet(6, `0123456789`)
console.log(`otp: ${code}`)
// Delete if exists
try {
const record = dao.findFirstRecordByData('otp', `email`, email)
dao.deleteRecord(record)
} catch (e) {
console.error(`Error deleting record: ${e}`)
}
// Save the code
try {
const collection = dao.findCollectionByNameOrId('otp')
const record = new Record(collection, {
email,
code,
})
dao.saveRecord(record)
console.log(`*** otp record saved: ${record.id}`)
} catch (e) {
console.error(`Error saving otp: ${e}`)
throw new BadRequestError(`Error saving otp: ${e}`)
}
const message = new MailerMessage({
from: {
address: $app.settings().meta.senderAddress,
name: $app.settings().meta.senderName,
},
to: [{ address: email }],
subject: `Your 6-digit login code`,
text: `Your 6-digit login code is: ${code}\n\nPlease enter this code in the app to continue.\n\nIf you didn't request this code, please ignore this email.`,
})
$app.newMailClient().send(message)
return c.json(200, {
message: `Please check your email for your 6-digit code.`,
})
})
routerAdd('POST', '/api/otp/verify', (c) => {
const dao = $app.dao()
const parsed = (() => {
const rawBody = readerToString(c.request().body)
try {
const parsed = JSON.parse(rawBody)
return parsed
} catch (e) {
throw new BadRequestError(
`Error parsing payload. You call this JSON? ${rawBody}`,
e,
)
}
})()
console.log(`***${JSON.stringify(parsed)}`)
const email = parsed.email.trim()
const code = parsed.code
console.log(`***email: ${email} , code: ${code}`)
try {
const record = dao.findFirstRecordByData('otp', 'email', email)
const storedCode = record.getInt(`code`)
if (storedCode !== code) {
throw new BadRequestError(`Invalid code`)
}
const created = record.created.time().unixMilli()
const now = Date.now()
console.log(`***now:${now} created:${created}`)
if (now - created > 60000) {
// throw new BadRequestError(`Code expired`)
}
} catch (e) {
console.error(`Error confirming otp: ${e}`)
throw e
}
const userRecord = (() => {
try {
return dao.findFirstRecordByData('users', 'email', email)
} catch (e) {
console.error(`Error finding user: ${e}`)
const usersCollection = dao.findCollectionByNameOrId('users')
const user = new Record(usersCollection)
try {
const username = $app
.dao()
.suggestUniqueAuthRecordUsername(
'users',
'user' + $security.randomStringWithAlphabet(5, '123456789'),
)
user.set('username', username)
user.set('email', email)
user.set('subscription', 'free')
user.setPassword($security.randomString(20)) // Fake password (not used)
dao.saveRecord(user)
} catch (e) {
throw BadRequestError(`Could not create user: ${e}`)
}
}
})()
return $apis.recordAuthResponse($app, c, userRecord)
}) [
{
"id": "s3ezh62e61vy0ks",
"name": "otp",
"type": "base",
"system": false,
"schema": [
{
"system": false,
"id": "y5vzwab0",
"name": "email",
"type": "email",
"required": false,
"presentable": false,
"unique": false,
"options": {
"exceptDomains": null,
"onlyDomains": null
}
},
{
"system": false,
"id": "kbryaqyk",
"name": "code",
"type": "number",
"required": false,
"presentable": false,
"unique": false,
"options": {
"min": null,
"max": null,
"noDecimal": false
}
}
],
"indexes": [
"CREATE INDEX `idx_xUoTrf5` ON `otp` (\n `email`,\n `code`\n)",
"CREATE INDEX `idx_guKleJD` ON `otp` (`email`)",
"CREATE INDEX `idx_fUk039Z` ON `otp` (`code`)"
],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
}
] |
Hi, would it be possible to have an email passwordless auth flow?
Cheers
The text was updated successfully, but these errors were encountered: