Skip to content

Instantly share code, notes, and snippets.

@aasutossh
Forked from kalafut/otp_demo.go
Created April 30, 2024 11:38
Show Gist options
  • Select an option

  • Save aasutossh/4ea37f1778b143f3e1c8ce768da1fe13 to your computer and use it in GitHub Desktop.

Select an option

Save aasutossh/4ea37f1778b143f3e1c8ce768da1fe13 to your computer and use it in GitHub Desktop.

Revisions

  1. @kalafut kalafut revised this gist Jan 9, 2024. 2 changed files with 9 additions and 17 deletions.
    4 changes: 2 additions & 2 deletions otp_demo.go
    Original file line number Diff line number Diff line change
    @@ -97,13 +97,13 @@ func main() {
    }

    // validate expiration
    if record.Get("expiration").(types.DateTime).Time().Before(time.Now()) {
    if record.GetDateTime("expiration").Time().Before(time.Now()) {
    app.Dao().DeleteRecord(record)
    return unauthorizedErr
    }

    // validate otp
    if record.GetString("otp") != data.OTP {
    if !security.Equal(record.GetString("otp"), data.OTP) {
    attempts := record.GetInt("attempts") + 1
    if attempts > 3 {
    app.Dao().DeleteRecord(record)
    22 changes: 7 additions & 15 deletions otp_demo_client.dart
    Original file line number Diff line number Diff line change
    @@ -3,32 +3,24 @@
    // Step 1

    // Start the auth with an email
    final resp = await post(Uri.parse("$pburl/otp-auth"), body: {
    final resp = await pb.send("/otp-auth", method: "POST", body: {
    "email": emailController.text,
    });

    // Save this token for the next step
    final verifyToken = jsonDecode(resp.body)['verifyToken'];

    final otpToken = resp['verifyToken'];

    //
    // ... email with OTP is received and we want to complete the auth...
    //


    // Step 2
    final headers = <String, String>{
    'Content-Type': 'application/json',
    };
    final resp = await post(Uri.parse("$pburl/otp-verify"),
    headers: headers,
    body: jsonEncode({
    "verifyToken": widget.otpToken, // saved from Step 1
    "otp": otpController.text, // code entered by the user
    }));

    final data = jsonDecode(resp.body);
    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(data);
    final auth = RecordAuth.fromJson(resp);
    pb.authStore.save(auth.token, auth.record);
  2. @kalafut kalafut revised this gist Jan 8, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion otp_demo_client.dart
    Original file line number Diff line number Diff line change
    @@ -29,6 +29,6 @@ final resp = await post(Uri.parse("$pburl/otp-verify"),

    final data = jsonDecode(resp.body);

    // Complete the process but updating the auth store.
    // Complete the process by updating the auth store.
    final auth = RecordAuth.fromJson(data);
    pb.authStore.save(auth.token, auth.record);
  3. @kalafut kalafut revised this gist Jan 8, 2024. No changes.
  4. @kalafut kalafut revised this gist Jan 8, 2024. No changes.
  5. @kalafut kalafut revised this gist Jan 8, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion otp_demo.go
    Original file line number Diff line number Diff line change
    @@ -103,7 +103,7 @@ func main() {
    }

    // validate otp
    if record.Get("otp").(string) != data.OTP {
    if record.GetString("otp") != data.OTP {
    attempts := record.GetInt("attempts") + 1
    if attempts > 3 {
    app.Dao().DeleteRecord(record)
  6. @kalafut kalafut revised this gist Jan 8, 2024. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion otp_demo_client.dart
    Original file line number Diff line number Diff line change
    @@ -28,6 +28,7 @@ final resp = await post(Uri.parse("$pburl/otp-verify"),
    }));

    final data = jsonDecode(resp.body);
    final auth = RecordAuth.fromJson(data); // from Pocketbase SDK

    // Complete the process but updating the auth store.
    final auth = RecordAuth.fromJson(data);
    pb.authStore.save(auth.token, auth.record);
  7. @kalafut kalafut revised this gist Jan 8, 2024. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions otp_demo.go
    Original file line number Diff line number Diff line change
    @@ -25,10 +25,10 @@ func main() {
    // 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.
    //
    // 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
  8. @kalafut kalafut renamed this gist Jan 8, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  9. @kalafut kalafut created this gist Jan 8, 2024.
    33 changes: 33 additions & 0 deletions otp_demo.dart
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,33 @@
    // Example Flutter snippets to interact with the OTP auth

    // Step 1

    // Start the auth with an email
    final resp = await post(Uri.parse("$pburl/otp-auth"), body: {
    "email": emailController.text,
    });

    // Save this token for the next step
    final verifyToken = jsonDecode(resp.body)['verifyToken'];


    //
    // ... email with OTP is received and we want to complete the auth...
    //


    // Step 2
    final headers = <String, String>{
    'Content-Type': 'application/json',
    };
    final resp = await post(Uri.parse("$pburl/otp-verify"),
    headers: headers,
    body: jsonEncode({
    "verifyToken": widget.otpToken, // saved from Step 1
    "otp": otpController.text, // code entered by the user
    }));

    final data = jsonDecode(resp.body);
    final auth = RecordAuth.fromJson(data); // from Pocketbase SDK

    pb.authStore.save(auth.token, auth.record);
    143 changes: 143 additions & 0 deletions otp_demo.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,143 @@
    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.Get("expiration").(types.DateTime).Time().Before(time.Now()) {
    app.Dao().DeleteRecord(record)
    return unauthorizedErr
    }

    // validate otp
    if record.Get("otp").(string) != 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)
    }
    }