-
-
Save aasutossh/4ea37f1778b143f3e1c8ce768da1fe13 to your computer and use it in GitHub Desktop.
PocketBase + Flutter OTP auth example (re: https://github.com/pocketbase/pocketbase/issues/119#issuecomment-1881906233)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"log" | |
"time" | |
"github.com/labstack/echo/v5" | |
"github.com/pocketbase/pocketbase" | |
"github.com/pocketbase/pocketbase/apis" | |
"github.com/pocketbase/pocketbase/core" | |
"github.com/pocketbase/pocketbase/models" | |
"github.com/pocketbase/pocketbase/tools/security" | |
"github.com/pocketbase/pocketbase/tools/types" | |
) | |
var unauthorizedErr = apis.NewUnauthorizedError("Invalid or expired OTP token", nil) | |
var badEmailErr = apis.NewBadRequestError("Invalid or unknown email", nil) | |
func main() { | |
app := pocketbase.New() | |
// To support OTP authentication, a new collection (otp_auth) was created with the following fields: | |
// | |
// otp - the generated OTP token | |
// user - relation to the user record | |
// expiration - the expiration date of the token | |
// attempts - the number of failed attempts to verify the token | |
// | |
// The user requests login and receives an opaque verifyToken in response, | |
// and the OTP is sent to the user's email. The user then submits the OTP | |
// and verifyToken to the server for verification. | |
app.OnBeforeServe().Add(func(e *core.ServeEvent) error { | |
// TODO: add a cron job to delete expired otp_auth records | |
// This is not a security related and can be done occasionally. | |
// Step 1: User requests an OTP token | |
// | |
// The otp-auth endpoint is used to generate an otp_auth record. If the user's email | |
// is found, the OTP is stored, emailed, and verifyToken (this record's ID) is returned | |
// to the caller. | |
e.Router.POST("/otp-auth", func(c echo.Context) error { | |
data := apis.RequestInfo(c).Data | |
email, ok := data["email"].(string) | |
if !ok { | |
return badEmailErr | |
} | |
user, err := app.Dao().FindAuthRecordByEmail("users", email) | |
if err != nil { | |
return badEmailErr | |
} | |
collection, err := app.Dao().FindCollectionByNameOrId("otp_auth") | |
if err != nil { | |
log.Println("find collection error", err) | |
return apis.NewBadRequestError("", nil) | |
} | |
record := models.NewRecord(collection) | |
record.Set("user", user.GetId()) | |
record.Set("expiration", time.Now().Add(time.Minute*10)) | |
otp := security.RandomStringWithAlphabet(6, "0123456789") | |
record.Set("otp", otp) | |
log.Println("otp", otp) // For testing purposes. Normally, you would send this to the user's email. | |
if err := app.Dao().SaveRecord(record); err != nil { | |
log.Println("save error", err) | |
return apis.NewBadRequestError("", nil) | |
} | |
return c.JSON(200, echo.Map{ | |
"verifyToken": record.GetId(), | |
}) | |
}) | |
// Step 2: User submits the OTP token for verification | |
// | |
// The otp-verify endpoint is used to verify the OTP token. If the token is valid, | |
// the user is authenticated and the standard RecordAuthResponse is returned. | |
e.Router.POST("/otp-verify", func(c echo.Context) error { | |
data := struct { | |
VerifyToken string `json:"verifyToken"` | |
OTP string `json:"otp"` | |
}{} | |
if err := c.Bind(&data); err != nil { | |
log.Println("bind error", err) | |
return unauthorizedErr | |
} | |
record, err := app.Dao().FindRecordById("otp_auth", data.VerifyToken) | |
if err != nil { | |
return unauthorizedErr | |
} | |
// validate expiration | |
if record.GetDateTime("expiration").Time().Before(time.Now()) { | |
app.Dao().DeleteRecord(record) | |
return unauthorizedErr | |
} | |
// validate otp | |
if !security.Equal(record.GetString("otp"), data.OTP) { | |
attempts := record.GetInt("attempts") + 1 | |
if attempts > 3 { | |
app.Dao().DeleteRecord(record) | |
return unauthorizedErr | |
} | |
record.Set("attempts", attempts) | |
if err := app.Dao().SaveRecord(record); err != nil { | |
log.Println("save error", err) | |
} | |
return unauthorizedErr | |
} | |
// At this point the OTP record is consumed and should be deleted | |
defer app.Dao().DeleteRecord(record) | |
if err := app.Dao().ExpandRecord(record, []string{"user"}, nil); len(err) > 0 { | |
log.Println("expand error", err) | |
return unauthorizedErr | |
} | |
user := record.ExpandedOne("user") | |
if user == nil { | |
return unauthorizedErr | |
} | |
return apis.RecordAuthResponse(app, c, user, nil) | |
}) | |
return nil | |
}) | |
if err := app.Start(); err != nil { | |
log.Fatal(err) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Example Flutter snippets to interact with the OTP auth | |
// Step 1 | |
// Start the auth with an email | |
final resp = await pb.send("/otp-auth", method: "POST", body: { | |
"email": emailController.text, | |
}); | |
// Save this token for the next step | |
final otpToken = resp['verifyToken']; | |
// | |
// ... email with OTP is received and we want to complete the auth... | |
// | |
// Step 2 | |
final resp = await pb.send("/otp-verify", method: "POST", body: { | |
"verifyToken": widget.otpToken, | |
"otp": otpController.text, | |
}); | |
// Complete the process by updating the auth store. | |
final auth = RecordAuth.fromJson(resp); | |
pb.authStore.save(auth.token, auth.record); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment