Authentication
Sep 30, 2025

Go Passwordless: Scalable authentication with Fiber and Scalekit

Srinivas Karre
Founding Engineer

TL;DR

  • Passwordless core: Scalekit handles OTP/magic link issuance + verification; Go Fiber manages session state (authRequestId, email) per user.
  • Session-first design: Fiber’s session middleware isolates user flows, ensuring integrity even under high concurrency.
  • Fail-safe patterns: JSON validation + structured error handling provide clear feedback while preventing malformed or malicious requests.
  • Performance levers: Goroutines, caching (sync.Map/Redis), and async API calls keep the flow non-blocking and scalable.
  • Deployment ready: Containerized with Docker + Compose, scalable with Redis/Kubernetes, production hardened with retries, logging, and secure sessions.

You’re working on a high-performance application built with Go. The system is fast, handles concurrent requests like a champ, and scales effortlessly, thanks to Go’s goroutines and channels. The challenge? Authentication. Your team decided to implement passwordless authentication, but as the project progresses, you realize that integrating a secure and seamless passwordless flow isn’t as simple as it seems. Go’s simplicity and speed make it an excellent choice for back-end logic, but the intricacies of managing authentication tokens, sending magic links, handling session states, and maintaining security add a layer of complexity you didn’t anticipate.

That’s where Scalekit’s Go SDK comes into play. Scalekit abstracts away the heavy lifting of passwordless authentication by offering a clean, efficient API for integrating OTP and magic link flows. It seamlessly fits into your Go-based architecture, enabling you to focus on scaling your business rather than worrying about security loopholes or reinventing authentication workflows. With the Scalekit Go SDK, you can securely send authentication codes, handle session management, and verify users without worrying about the nuances of token management or the underlying infrastructure.

In this blog, you’ll learn how to integrate Scalekit’s Go SDK into your application, empowering you to implement passwordless authentication in just a few steps. By the end, you’ll have the foundation for a fast, secure, and efficient passwordless flow that fits naturally into your Go-based infrastructure.

Efficient session management in Go Fiber for passwordless authentication

When building a passwordless authentication system in Go, session management is a critical piece of the puzzle. Unlike JavaScript-based frameworks like Express, which rely heavily on external session packages, Go’s concurrency model and Fiber framework enable a minimalist approach that strikes a balance between performance and security.

Go Fiber is optimized for handling concurrent requests efficiently, thanks to its non-blocking I/O operations. This makes it an ideal framework for passwordless authentication, as it can handle multiple simultaneous requests without degrading performance. However, ensuring that each user's session data (like authRequestId and email) is correctly isolated during the authentication process is crucial for maintaining security and integrity.

In this section, we’ll walk through the process of setting up session middleware in Go Fiber, its integration with Scalekit’s passwordless authentication system, and how to ensure that each user's authentication flow remains independent even when handling multiple requests concurrently.

Setting up Go Fiber with session middleware

Fiber provides a simple and performant way to manage sessions, which is essential for tracking user state during the authentication process. Let’s look at how you can integrate session middleware in your Go application or you can use the Go sample app here for reference:

First, ensure you have the necessary dependencies:

go get github.com/gofiber/fiber/v2 go get github.com/gofiber/session/v2

Then, initialize Fiber and session middleware in your application:

package main import ( "log" "github.com/gofiber/fiber/v2" "github.com/gofiber/session/v2" ) func main() { // Create a new Fiber app instance app := fiber.New() // Set up session middleware for storing session data store := session.New() // Example route to test session management app.Get("/session-test", func(c *fiber.Ctx) error { // Retrieve session data for the current request sess := store.Get(c) // Store user-specific data in the session sess.Set("user", "john.doe@example.com") sess.Save() return c.SendString("Session data saved for user!") }) // Start the server on port 3000 log.Fatal(app.Listen(":3000")) }

In this example:

  • store := session.New() initializes the session store.
  • store.Get(c) retrieves the session associated with the request context.
  • sess.Set() stores user-specific data in the session.

Fiber supports multiple session stores (in-memory, Redis, etc.), which can be swapped depending on your scalability needs. For small applications or development, the in-memory store is sufficient, but for production, you may want to use a more robust store like Redis.

Session management with Scalekit: Storing authRequestId and pending email

Integrating Scalekit’s passwordless authentication requires managing session data carefully. Scalekit relies on authRequestId (a unique identifier for each passwordless request) and email to associate the authentication flow with the right user. Storing these values securely in the session ensures smooth flow handling.

Let’s look at how to implement session storage for authRequestId and email during the /request-auth route:

app.Post("/request-auth", func(c *fiber.Ctx) error { type reqBody struct { Email string `json:"email"` } var body reqBody if err := c.BodyParser(&body); err != nil { log.Println("Invalid request body for /request-auth") return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } email := body.Email if email == "" { log.Println("No email provided for /request-auth") return c.Status(400).JSON(fiber.Map{"error": "Email required"}) } // Validate email format if _, err := mail.ParseAddress(email); err != nil { log.Println("Invalid email format for /request-auth") return c.Status(400).JSON(fiber.Map{"error": "Invalid email"}) } // Send passwordless email request to Scalekit resp, err := scalekitClient.Passwordless().SendPasswordlessEmail( c.Context(), email, &scalekit.SendPasswordlessOptions{ MagiclinkAuthUri: redirectURI, ExpiresIn: 600, }, ) if err != nil { log.Printf("Error sending passwordless email for %s: %v", email, err) return c.Status(500).JSON(fiber.Map{"error": "Failed to send passwordless email"}) } // Store authRequestId and email in session sess := store.Get(c) if sess == nil { log.Println("Session error: could not get session for /request-auth") return c.Status(500).SendString("Session error") } sess.Set("authRequestId:"+email, resp.AuthRequestId) sess.Set("pendingEmail", email) sess.Save() log.Printf("Passwordless email sent to %s, authRequestId: %v", email, resp.AuthRequestId) return c.JSON(fiber.Map{"message": "Passwordless email sent! Check your email."}) })

Session flow breakdown:

  1. Email submission: The user provides their email, and it’s validated.
  2. Scalekit API call: The server requests Scalekit to send an OTP or magic link.
  3. Session storage: The authRequestId and email are stored in the session to associate the request with the user’s session.

The authRequestId ensures each passwordless request is uniquely identified, and storing it in the session ties the flow to the user. When the user returns with their OTP or magic link, the session data ensures the system can correctly identify their request.

Handling session data across requests

When the user returns with an OTP or magic link, we retrieve the authRequestId from the session to verify their identity. Here's how to handle OTP verification:

app.Post("/verify-otp", func(c *fiber.Ctx) error { type reqBody struct { OTP string `json:"otp"` } var body reqBody if err := c.BodyParser(&body); err != nil { log.Println("Invalid request body for OTP verification") return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } otp := body.OTP if otp == "" { log.Println("Missing OTP for verification") return c.Status(400).JSON(fiber.Map{"error": "OTP required"}) } // Retrieve session data sess := store.Get(c) if sess == nil { log.Println("Session error: could not get session for OTP verify") return c.Status(500).JSON(fiber.Map{"error": "Session error"}) } // Retrieve email and authRequestId from session email, ok := sess.Get("pendingEmail").(string) if !ok || email == "" { log.Println("No pending email found in session for OTP verify") return c.Status(400).JSON(fiber.Map{"error": "No OTP request found. Please request OTP again."}) } authRequestId, ok := sess.Get("authRequestId:" + email).(string) if !ok || authRequestId == "" { log.Println("No authRequestId found in session for email: " + email) return c.Status(400).JSON(fiber.Map{"error": "No OTP request found for this email. Please request OTP again."}) } // Verify OTP with Scalekit _, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( c.Context(), &scalekit.VerifyPasswordlessOptions{ Code: otp, AuthRequestId: authRequestId, }, ) if err != nil { log.Printf("OTP verification failed for %s: %v", email, err) return c.Status(401).JSON(fiber.Map{"error": "Invalid OTP"}) } // Authenticate the user by setting their email in the session sess.Set("email", email) sess.Delete("pendingEmail") sess.Delete("authRequestId:" + email) sess.Save() log.Printf("User authenticated via OTP: %s", email) return c.JSON(fiber.Map{"message": "OTP verified!", "email": email}) })

Session handling during OTP verification:

  • The authRequestId and pendingEmail are retrieved from the session.
  • If the OTP is valid, the user is authenticated by storing their email in the session.
  • The session is updated to ensure the user's authentication flow is completed successfully.

By ensuring session data is properly managed, each user's authentication flow remains isolated and secure even when handling multiple requests simultaneously.

Efficiently handling concurrent requests in Go Fiber for passwordless authentication

As your application scales, efficiently managing concurrent requests becomes essential, especially for passwordless authentication systems. Handling multiple users simultaneously is a common requirement in modern applications, and Go’s goroutines make it a powerful tool for achieving this. However, the key challenge when dealing with passwordless login systems (like OTPs or magic links) is ensuring that each user’s authentication flow remains isolated, even during high concurrency.

In contrast to frameworks like Express (Node.js) or Next.js (React), which rely on event-driven concurrency, Go Fiber leverages goroutines, lightweight threads that allow your app to handle multiple requests concurrently without performance degradation. However, managing data integrity in this concurrent environment is crucial, especially for session-based systems like passwordless authentication. Go Fiber’s session management ensures that user-specific data (such as authRequestId and email) remains isolated, providing both security and scalability.

In this section, we’ll explore how Go Fiber handles concurrent requests, ensuring that user data is managed securely, and how it integrates with Scalekit’s passwordless authentication system.

Go Fiber’s concurrency model

Go’s concurrency model is based on goroutines, which are lightweight, non-blocking threads. Unlike traditional threading models, goroutines are multiplexed onto a small number of system threads, making Go highly efficient and scalable, even for high-traffic applications.

Go Fiber leverages this concurrency model, enabling multiple requests to be processed simultaneously without blocking the system. When users interact with the authentication system (e.g., entering an OTP or clicking a magic link), their requests are handled independently, allowing the system to remain responsive.

For passwordless authentication, when users submit their email, these requests can be processed concurrently without blocking other operations. However, there’s a critical requirement: data isolation. Each user’s session data must remain independent to ensure secure and accurate authentication.

Ensuring data isolation in concurrent requests

When a user requests passwordless authentication, Scalekit generates a unique authRequestId that is tied to the user’s email. This authRequestId must be securely stored in the session to ensure that the correct request is verified when the user returns with an OTP or magic link.

Here’s the flow for handling concurrent requests:

  1. User email submission: The user submits their email to initiate the passwordless authentication flow.
  2. Scalekit API call: The system makes an API request to Scalekit to generate a one-time passcode or magic link.
  3. Session storage: The authRequestId and email are stored in the session, linking the authentication request to the user.
User email submission flow

Once the user submits their email, the server requests Scalekit to generate an OTP or magic link. Upon receiving a successful response, the system notifies the user

Here’s an example of how this works with Go Fiber:

app.Post("/request-auth", func(c *fiber.Ctx) error { type reqBody struct { Email string `json:"email"` } var body reqBody if err := c.BodyParser(&body); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } email := body.Email if email == "" { return c.Status(400).JSON(fiber.Map{"error": "Email required"}) } // Validate email format if _, err := mail.ParseAddress(email); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid email"}) } // Send passwordless authentication email (OTP or Magic Link) resp, err := scalekitClient.Passwordless().SendPasswordlessEmail( c.Context(), email, &scalekit.SendPasswordlessOptions{ MagiclinkAuthUri: redirectURI, ExpiresIn: 600, }, ) if err != nil { return c.Status(500).JSON(fiber.Map{"error": "Failed to send passwordless email"}) } // Store authRequestId and email in session sess := store.Get(c) if sess == nil { return c.Status(500).SendString("Session error") } sess.Set("authRequestId:"+email, resp.AuthRequestId) sess.Set("pendingEmail", email) sess.Save() return c.JSON(fiber.Map{"message": "Passwordless email sent! Check your email."}) })

Key points:

  • Each user’s email is tied to a unique authRequestId, ensuring session data is isolated.
  • The authRequestId and email are stored in the session to accurately track the user’s authentication flow, even with multiple concurrent requests.

How Go Fiber handles multiple authentication requests

Go’s goroutines ensure that authentication requests are processed concurrently, without blocking the server. This allows multiple users to submit their email for OTP or magic link verification at the same time, without degrading system performance.

Here’s how Go Fiber efficiently handles concurrent requests:

  • Non-blocking I/O: Go’s concurrency model ensures that each user’s request is processed independently, without blocking others.
  • Session management: Each request has its own authRequestId and email stored in the session, ensuring that data remains isolated, even under high traffic.
  • Concurrency isolation: Fiber’s session store ensures that multiple requests do not interfere with each other, even when many users are interacting with the authentication system simultaneously.

The role of Goroutines and session management

Goroutines allow the app to handle multiple requests concurrently while minimizing resource usage. In the case of passwordless authentication:

  • Each request is isolated: Each user’s authentication request (with their unique authRequestId and email) is handled independently.
  • Session management ensures that even with many concurrent requests, session data remains separate for each user, ensuring a secure and reliable authentication process.

Key takeaways:

  • Concurrency with goroutines: Go’s lightweight goroutines allow for efficient concurrent request handling, ensuring the application remains responsive and scalable.
  • Session management: Proper session handling ensures each user’s authentication flow remains isolated, even when many users interact with the system concurrently.
  • Scalability: By leveraging Go Fiber’s non-blocking I/O and goroutines, the app can scale to handle large numbers of concurrent users, making it ideal for high-traffic applications.

With this setup, you’re equipped to handle concurrent passwordless authentication requests, ensuring that each user’s session data remains secure and independent. In the next section, we’ll focus on validating incoming data to ensure that only well-formed and secure requests are processed.

Learn more about migrating from passwords to passwordless authentication

JSON validation for secure authentication data

Ensuring the integrity and validity of incoming data is crucial when building a passwordless authentication system. Invalid or improperly validated data can lead to authentication failures, security vulnerabilities, or even application crashes. Go’s explicit approach to validation gives developers full control over how data is parsed and validated, which is especially important for user inputs like emails, OTP codes, or magic link tokens in a passwordless flow.

In this section, we’ll demonstrate how to use the go-playground/validator library to validate request data, focusing on validating user inputs (such as email addresses, OTP values, and magic link tokens) before proceeding with the passwordless authentication flow.

Using go-playground/validator for JSON validation

Go’s built-in JSON handling works well, but using a dedicated library like go-playground/validator ensures that data is validated properly before being processed. This library simplifies struct-based validation, offering tags like required and email to enforce data format and presence.

To get started, install the validator library:

go get github.com/go-playground/validator/v10

Once installed, you can validate fields like email addresses or OTP tokens to ensure they are properly formatted before processing them.

Validating email submission

When users submit their email for passwordless authentication, it’s important to validate that the email format is correct. We’ll use a combination of go-playground/validator and Go’s built-in mail.ParseAddress to ensure the email is both valid and correctly formatted.

Here’s how you can validate the email in the /request-auth route:

package main import ( "log" "github.com/gofiber/fiber/v2" "github.com/go-playground/validator/v10" "net/mail" ) var validate *validator.Validate func main() { // Initialize Fiber app and validator library app := fiber.New() validate = validator.New() // /request-auth route to handle email submission app.Post("/request-auth", func(c *fiber.Ctx) error { type reqBody struct { Email string `json:"email" validate:"required,email"` } // Parse the incoming request body var body reqBody if err := c.BodyParser(&body); err != nil { log.Println("Invalid request body") return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } // Validate email format if err := validate.Struct(body); err != nil { log.Println("Invalid email format:", err) return c.Status(400).JSON(fiber.Map{"error": "Invalid email"}) } // Further validation with mail.ParseAddress if _, err := mail.ParseAddress(body.Email); err != nil { log.Println("Invalid email format") return c.Status(400).JSON(fiber.Map{"error": "Invalid email"}) } // Proceed with Scalekit passwordless authentication (API call) // ... return c.JSON(fiber.Map{"message": "Passwordless email sent! Check your email."}) }) log.Fatal(app.Listen(":3000")) }

Key concepts:

  • The validate:"required,email" tag ensures that the email field is both required and properly formatted.
  • We use validate.Struct(body) to validate the entire request structure.
  • mail.ParseAddress performs a double-check to validate the email format.

Validating OTP and magic link tokens

Similarly, you must validate OTP and magic link tokens to ensure they are present and valid before proceeding with verification. Here’s how to validate OTP and magic link tokens:

app.Post("/verify-otp", func(c *fiber.Ctx) error { type reqBody struct { OTP string `json:"otp" validate:"required"` } var body reqBody if err := c.BodyParser(&body); err != nil { log.Println("Invalid OTP request") return c.Status(400).JSON(fiber.Map{"error": "Invalid OTP request"}) } // Validate OTP field if err := validate.Struct(body); err != nil { log.Println("OTP is required") return c.Status(400).JSON(fiber.Map{"error": "OTP is required"}) } // Proceed with OTP verification (Scalekit API call) // ... return c.JSON(fiber.Map{"message": "OTP verified!"}) }) app.Post("/verify-magic-link", func(c *fiber.Ctx) error { type reqBody struct { Token string `json:"token" validate:"required"` } var body reqBody if err := c.BodyParser(&body); err != nil { log.Println("Invalid magic link request") return c.Status(400).JSON(fiber.Map{"error": "Invalid magic link request"}) } // Validate token field if err := validate.Struct(body); err != nil { log.Println("Magic link token is required") return c.Status(400).JSON(fiber.Map{"error": "Magic link token is required"}) } // Proceed with magic link verification (Scalekit API call) // ... return c.JSON(fiber.Map{"message": "Magic link verified!"}) })

Why JSON validation is crucial

Validating incoming data before processing is critical to ensure the integrity and security of your authentication flow. It helps prevent:

  • Authentication failures: Invalid or missing data could lead to broken authentication flows.
  • Security vulnerabilities: Malformed or malicious data could lead to potential exploits, such as unauthorized access or SQL injection.

For passwordless authentication systems, validating fields like OTP codes or magic link tokens ensures that malicious actors cannot exploit the system by sending improper requests.

Next steps: Session management for verification

With JSON validation in place, we’ve ensured that only secure, well-formed data enters the authentication flow. The next step involves handling session management, ensuring that each user’s authentication process remains secure and independent across multiple requests.

Session management for passwordless authentication

Session management is essential when dealing with passwordless authentication systems like OTP and magic link flows. In Go, managing user session data efficiently is crucial for both security and user experience. Go Fiber’s concurrency model and session middleware provide a simple yet powerful solution for storing and isolating user data across multiple requests.

Concurrent authentication requests flow

Go Fiber’s concurrency model allows multiple users to authenticate at the same time without affecting each other’s session data. Each user’s authentication request is processed independently.

In this section, we'll demonstrate how to securely manage session data in Go Fiber to track and isolate the authentication flow for each user during passwordless login.

Setting up session management with Go Fiber

Go Fiber simplifies session management through middleware, which stores user-specific data across requests. For passwordless authentication, you need to store the authRequestId and email in the session, ensuring that each user’s authentication flow remains intact and isolated.

Here’s how to set up session management in your Go application using Fiber:

package main import ( "log" "github.com/gofiber/fiber/v2" "github.com/gofiber/session/v2" "github.com/joho/godotenv" "github.com/scalekit-inc/scalekit-sdk-go/v2" ) func main() { app := fiber.New() store := session.New() if err := godotenv.Load(); err != nil { log.Println("Error loading .env") } clientID := os.Getenv("SCALEKIT_CLIENT_ID") clientSecret := os.Getenv("SCALEKIT_CLIENT_SECRET") environmentURL := os.Getenv("SCALEKIT_ENVIRONMENT_URL") redirectURI := os.Getenv("SCALEKIT_REDIRECT_URI") scalekitClient := scalekit.NewScalekitClient(environmentURL, clientID, clientSecret) // /request-auth route app.Post("/request-auth", func(c *fiber.Ctx) error { var body struct { Email string `json:"email"` } if err := c.BodyParser(&body); err != nil { log.Println("Invalid request body") return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } // Validate email email := body.Email if email == "" { log.Println("Email required") return c.Status(400).JSON(fiber.Map{"error": "Email required"}) } // Send passwordless email via Scalekit templateType := scalekit.TemplateTypeSignin resp, err := scalekitClient.Passwordless().SendPasswordlessEmail( c.Context(), email, &scalekit.SendPasswordlessOptions{ MagiclinkAuthUri: redirectURI, State: "state", Template: &templateType, ExpiresIn: 600, }, ) if err != nil { log.Printf("Error sending email: %v", err) return c.Status(500).JSON(fiber.Map{"error": "Failed to send passwordless email"}) } // Store session data sess := store.Get(c) if sess == nil { log.Println("Session error") return c.Status(500).SendString("Session error") } sess.Set("authRequestId:"+email, resp.AuthRequestId) sess.Set("pendingEmail", email) sess.Save() return c.JSON(fiber.Map{"message": "Passwordless email sent! Check your email.", "email": email}) }) log.Fatal(app.Listen(":3000")) }

Key points:

  • store.Get(c) retrieves the session for the current request.
  • sess.Set() stores the authRequestId and email in the session.
  • sess.Save() persists session data for subsequent requests.

Isolating each user’s authentication flow

Each user’s session must remain isolated to ensure their authentication flow is separate from others. The key to this is storing both the authRequestId and email in the session.

Here’s how the flow works:

  1. User submits email: The user provides their email for passwordless authentication.
  2. Scalekit API call: The server calls Scalekit to generate an OTP or magic link.
  3. Session storage: The authRequestId and email are stored in the session, linking the authentication request to the user.

When the user returns with their OTP or magic link, the authRequestId is retrieved from the session to verify the request.

OTP verification flow

After the user submits the OTP, the server retrieves the session data (authRequestId and email) to verify the OTP with Scalekit. The result is then communicated back to the user.

Managing session data for OTP and magic link verification

When verifying OTPs or magic links, the authRequestId must be retrieved from the session to validate the authentication request.

Here’s how the session data is used for OTP verification:

app.Post("/verify-otp", func(c *fiber.Ctx) error { type reqBody struct { OTP string `json:"otp"` } var body reqBody if err := c.BodyParser(&body); err != nil { log.Println("Invalid OTP request") return c.Status(400).JSON(fiber.Map{"error": "Invalid OTP request"}) } // Retrieve session data sess := store.Get(c) if sess == nil { log.Println("Session error") return c.Status(500).JSON(fiber.Map{"error": "Session error"}) } // Get pending email and authRequestId from session email, ok := sess.Get("pendingEmail").(string) if !ok || email == "" { return c.Status(400).JSON(fiber.Map{"error": "No OTP request found"}) } authRequestId, ok := sess.Get("authRequestId:" + email).(string) if !ok || authRequestId == "" { return c.Status(400).JSON(fiber.Map{"error": "No authRequestId found"}) } // Verify OTP with Scalekit _, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( c.Context(), &scalekit.VerifyPasswordlessOptions{ Code: body.OTP, AuthRequestId: authRequestId, }, ) if err != nil { log.Printf("OTP verification failed: %v", err) return c.Status(401).JSON(fiber.Map{"error": "Invalid OTP"}) } // Authenticate user sess.Set("email", email) sess.Delete("pendingEmail") sess.Delete("authRequestId:" + email) sess.Save() return c.JSON(fiber.Map{"message": "OTP verified!", "email": email}) })

Key concepts:

  • Session data isolation: Ensures each user’s authRequestId and email are tied to their session, preventing interference with other users’ sessions.
  • Session management: By using Fiber’s session middleware, we can manage user state across multiple requests, ensuring a seamless passwordless authentication process.

Key takeaways

  • Session management: Fiber’s session middleware allows you to securely store authRequestId and email for each user, ensuring proper session tracking.
  • Data integrity: Storing session data prevents data mixing, ensuring each user’s authentication flow is isolated, even with multiple users.
  • Scalability: The isolated session management approach ensures your application can handle concurrent users without data interference, maintaining both security and performance.

With session management in place, your application can securely track each user’s authentication state. Next, we will dive into error handling patterns to ensure users get appropriate feedback while maintaining system security.

Error handling patterns for passwordless authentication

Effective error handling is essential for maintaining a smooth user experience and a secure authentication system. In passwordless authentication, errors can occur at various stages, including network issues, invalid user data, or service failures. Proper error handling ensures that users receive clear feedback and that potential failures don’t disrupt the authentication flow.

In this section, we’ll explore how to handle errors in Go Fiber applications, focusing on:

  • Catching and handling errors appropriately.
  • Returning clear, consistent error messages.
  • Using HTTP status codes and JSON responses for better feedback.

Go Fiber: Error handling with middleware

Fiber makes error handling easy with middleware, allowing you to catch errors early and provide appropriate feedback. Here's how you can set up a global error handler in your Go Fiber app:

package main import ( "log" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/recover" "github.com/gofiber/fiber/v2/middleware/logger" ) func main() { app := fiber.New() // Middleware: recover from panics, log requests app.Use(recover.New()) app.Use(logger.New()) // Global error handler app.Use(func(c *fiber.Ctx) error { if err := c.Next(); err != nil { // Handle error and return JSON response c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) log.Printf("Error occurred: %v", err) return err } return nil }) log.Fatal(app.Listen(":3000")) }

Key Points:

  • recover.New() middleware catches panics and prevents crashes.
  • logger.New() middleware logs requests for easier debugging.
  • A global error handler catches unhandled errors and returns a consistent, clear error response in JSON format.

Handling authentication failures

During passwordless authentication, several errors can occur, including invalid emails, expired OTPs, or invalid magic links. Each of these scenarios needs specific, actionable error messages to help users resolve issues.

Here’s how to handle common errors in the authentication flow:

1. Invalid email format

When submitting an email for passwordless login, we catch invalid email formats and return a helpful error message:

app.Post("/request-auth", func(c *fiber.Ctx) error { var body struct { Email string `json:"email"` } if err := c.BodyParser(&body); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } email := body.Email if email == "" { return c.Status(400).JSON(fiber.Map{"error": "Email required"}) } // Further logic... })

2. Expired or incorrect OTP

When verifying an OTP, handle expiration or incorrect codes:

app.Post("/verify-otp", func(c *fiber.Ctx) error { var body struct { OTP string `json:"otp"` } if err := c.BodyParser(&body); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid OTP request"}) } // Verify OTP with Scalekit _, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( c.Context(), &scalekit.VerifyPasswordlessOptions{ Code: body.OTP, AuthRequestId: authRequestId, }, ) if err != nil { if err.Error() == "OTP expired" { return c.Status(401).JSON(fiber.Map{"error": "OTP expired. Please request a new one."}) } return c.Status(401).JSON(fiber.Map{"error": "Invalid OTP. Try again."}) } return c.JSON(fiber.Map{"message": "OTP verified!"}) })

3. Invalid or expired magic link

For magic link verification, handle invalid or expired links with appropriate error messages:

app.Post("/verify-magic-link", func(c *fiber.Ctx) error { var body struct { Token string `json:"token"` } if err := c.BodyParser(&body); err != nil { return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"}) } // Verify magic link with Scalekit _, err := scalekitClient.Passwordless().VerifyPasswordlessEmail( c.Context(), &scalekit.VerifyPasswordlessOptions{ LinkToken: body.Token, AuthRequestId: authRequestId, }, ) if err != nil { if err.Error() == "Link expired" { return c.Status(401).JSON(fiber.Map{"error": "Magic link expired. Request a new one."}) } return c.Status(401).JSON(fiber.Map{"error": "Invalid magic link. Try again."}) } return c.JSON(fiber.Map{"message": "Magic link verified!"}) })

Best practices for error handling in authentication systems

To ensure a smooth user experience and system security, follow these best practices:

  • Return specific errors: Always return clear, specific error messages (e.g., “Invalid OTP” or “Magic link expired”) to guide users in resolving issues.
  • HTTP status codes: Use appropriate HTTP status codes, such as 400 for invalid data, 401 for unauthorized, and 500 for server errors, to help users and API consumers understand the issue.
  • Logging: Log server-side errors for troubleshooting, but avoid exposing sensitive details to users.
  • Consistent responses: Ensure error responses have a consistent structure across your application for better readability and user understanding.

By implementing these error handling patterns, you ensure that your passwordless authentication flow is robust, secure, and user-friendly.

Performance optimization for passwordless authentication

Optimizing performance in passwordless authentication is essential, particularly in high-traffic applications. A slow or lagging authentication flow can frustrate users, leading to abandoned attempts. To maintain a smooth experience, we need to focus on improving response times and handling multiple requests efficiently.

In this section, we'll explore strategies for optimizing performance, including concurrency, caching, and minimizing external dependencies.

1. Efficient request handling with Go’s concurrency

Go’s goroutines allow for efficient handling of concurrent requests. Unlike traditional thread-based models used in frameworks like Node.js or React, Go can handle multiple requests concurrently with minimal memory overhead, making it ideal for high-concurrency scenarios like passwordless authentication.

By processing each request asynchronously using goroutines, we can ensure that the server remains responsive, even under heavy load. Here’s how we handle passwordless requests concurrently:

app.Post("/request-auth", func(c *fiber.Ctx) error { go func() { // Send the passwordless email in a separate goroutine email := c.FormValue("email") // Send request to Scalekit API for passwordless email _, err := scalekitClient.Passwordless().SendPasswordlessEmail( c.Context(), email, &scalekit.SendPasswordlessOptions{ ExpiresIn: 600, }, ) if err != nil { log.Printf("Error sending email: %v", err) } }() return c.JSON(fiber.Map{"message": "Passwordless email is being sent, check your inbox!"}) })

Key points:

  • Goroutines allow non-blocking request processing.
  • The main thread is freed up to handle other requests while the email is sent in the background.
  • Ensure proper management of goroutine concurrency to avoid overwhelming system resources

2. Caching responses for faster verification

Caching is another key optimization technique. By caching frequently used data, such as OTP codes or magic link tokens, we can reduce the number of external API calls (e.g., to Scalekit), thus speeding up the authentication process.

Here’s an example of caching OTP data using Go’s sync.Map for in-memory caching:

import ( "sync" "time" ) var otpCache sync.Map func cacheOTP(email string, otp string, authRequestId string) { otpCache.Store(email, map[string]interface{}{ "otp": otp, "authRequestId": authRequestId, "expiresAt": time.Now().Add(5 * time.Minute).Unix(), }) } func getCachedOTP(email string) (string, string, bool) { if otpData, found := otpCache.Load(email); found { data := otpData.(map[string]interface{}) if time.Now().Unix() > data["expiresAt"].(int64) { // OTP expired otpCache.Delete(email) return "", "", false } return data["otp"].(string), data["authRequestId"].(string), true } return "", "", false }

Key benefits:

  • In-memory caching reduces the number of calls to external services, speeding up OTP validation.
  • Cached data expires after a set time (5 minutes in this case), ensuring data freshness.

3. Optimizing external API calls

External API calls, such as those to Scalekit for OTP or magic link verification, introduce network latency. To mitigate this, we use several strategies:

  • Limit API calls: Only call Scalekit when necessary. If a user’s email is valid, cache the session state to avoid repeated API calls.
  • Asynchronous processing: Offload external API calls to separate threads using goroutines to prevent blocking.
  • Retry logic: Implement retry mechanisms for transient errors like network issues to ensure smooth operation.

Example of retry logic with exponential backoff:

func callScalekitWithRetry(maxRetries int) error { var err error for i := 0; i < maxRetries; i++ { err = scalekitClient.Passwordless().SendPasswordlessEmail( c.Context(), email, &scalekit.SendPasswordlessOptions{ ExpiresIn: 600, }, ) if err == nil { return nil } log.Printf("Retry %d/%d: Error calling Scalekit: %v", i+1, maxRetries, err) time.Sleep(time.Second * time.Duration(i)) // Exponential backoff } return err }

Key takeaways:

  • Retry logic improves the resilience of the system.
  • Asynchronous processing keeps the main application flow responsive.

4. Minimize synchronous external dependencies

While some operations, like generating OTPs or sending magic links, require synchronous calls, these can create bottlenecks. To minimize this:

  • Use goroutines for non-blocking tasks like sending emails.
  • For tasks that don’t require immediate feedback, consider using a background job processing system (e.g., worker pools or queue-based systems) to offload work from the main thread.

Example:

  • Email sending: Use goroutines to send emails asynchronously.
  • Background jobs: Use libraries like golang.org/x/sync/errgroup to handle tasks like bulk email sending without blocking the user flow.

By leveraging Go’s concurrency model, caching frequently accessed data, optimizing external API calls, and minimizing synchronous dependencies, you can significantly enhance the performance and scalability of your passwordless authentication flow. These optimizations help ensure that your authentication system can handle high traffic and provide users with a seamless experience.

Container deployment: Scaling with docker

Once your Go Fiber application is built and optimized, it's time to deploy it in a scalable, easy-to-manage way. Docker is a great solution for this, as it allows you to containerize your application and run it consistently across different environments. In this section, we’ll walk through the steps for containerizing your Go Fiber app, configuring it for production, and deploying it within a Docker container.

1. Dockerizing your Go Fiber application

To containerize your Go Fiber app, start by creating a Dockerfile. This file defines the environment your application will run in, including dependencies and build steps.

Here’s an example Dockerfile for your Go Fiber app:

# Build stage: Use the official Golang image to build the app FROM golang:1.19-alpine as builder # Set the current working directory inside the container WORKDIR /app # Copy go.mod and go.sum files COPY go.mod go.sum ./ # Download dependencies RUN go mod tidy # Copy the application code COPY . . # Build the Go application RUN GOOS=linux GOARCH=amd64 go build -o app . # Final stage: Create a smaller image for runtime FROM alpine:latest # Set the working directory inside the container WORKDIR /root/ # Install SSL certificates for running the Go app RUN apk --no-cache add ca-certificates # Copy the built Go app from the builder image COPY --from=builder /app/app . # Expose port 3000 for the Go app EXPOSE 3000 # Command to run the Go app CMD ["./app"]

Key points:

  • The app is first built in a multi-stage Dockerfile, optimizing the image size.
  • The Go binary is built for Linux and copied into a minimal Alpine-based image.
  • The container exposes port 3000, the default for Go Fiber apps.

2. Building and running the docker container

Once you’ve created your Dockerfile, build and run the Docker container:

Build the image:

docker build -t go-fiber-passwordless .

This will tag your image as go-fiber-passwordless.

Run the container:

docker run -p 3000:3000 go-fiber-passwordless

This command runs the application, mapping port 3000 inside the container to port 3000 on your host machine. You can now access the Go Fiber app at http://localhost:3000.

3. Using docker compose for multi-container setup

In production, you may need to run multiple services (e.g., database, cache, or load balancing). Docker Compose allows you to define and run multi-container applications.

Here’s an example docker-compose.yml file for a Go Fiber app and a Redis cache:

version: '3' services: app: build: . ports: - "3000:3000" environment: - SCALEKIT_CLIENT_ID=${SCALEKIT_CLIENT_ID} - SCALEKIT_CLIENT_SECRET=${SCALEKIT_CLIENT_SECRET} - SCALEKIT_ENVIRONMENT_URL=${SCALEKIT_ENVIRONMENT_URL} - SCALEKIT_REDIRECT_URI=${SCALEKIT_REDIRECT_URI} depends_on: - redis redis: image: redis:alpine ports: - "6379:6379"

Key points:

  • App: The Go Fiber app container is built and runs on port 3000.
  • Redis: A Redis container is added for caching purposes or session management.
  • The depends_on ensures Redis starts before the app.

To start both containers with Docker Compose, use:

docker-compose up

4. Deploying your containerized application

After local testing, you can deploy your containerized app using platforms like Docker Hub, Kubernetes, or cloud services like AWS, Google Cloud Run, or Heroku.

Push to Docker Hub:

docker login docker tag go-fiber-passwordless yourdockerhubusername/go-fiber-passwordless docker push yourdockerhubusername/go-fiber-passwordless

Kubernetes: Deploy using Helm or Kubernetes manifests.

Cloud platforms: Services like AWS ECS, Google Cloud Run, and Azure App Service support Docker containers.

5. Scaling the authentication flow

One of the biggest advantages of containerization is scalability. If your app needs to handle more traffic, you can scale by running multiple containers.

To horizontally scale your application, use Kubernetes or Docker Swarm to manage multiple instances of the Go Fiber app. With Redis or another distributed session store, you can ensure that user sessions are handled across multiple containers.

Conclusion: A robust, scalable passwordless authentication system

In this blog, we’ve covered how to implement a passwordless authentication system using Go Fiber and Scalekit. We tackled common challenges such as maintaining performance, ensuring security, and providing a seamless user experience.

By integrating the Scalekit Go SDK, we built a reliable passwordless authentication flow, ensuring secure OTP and magic link generation. Go's concurrency model, powered by goroutines, allowed the system to handle multiple authentication requests simultaneously, improving performance. We also implemented robust error handling to ensure smooth user interactions during failures.

To optimize performance, we leveraged caching and containerized the app with Docker, enabling the app to scale seamlessly. This ensures the system remains efficient under high traffic without compromising speed or reliability.

Now that you’ve established a solid foundation for passwordless authentication, it’s time to deepen your expertise. Explore Scalekit’s documentation for advanced features like rate limiting, suspicious activity monitoring, and customizable security settings. Dive into additional blog topics around securing authentication workflows, implementing multi-factor authentication (MFA), and leveraging Scalekit’s SDK for further scalability. The next steps are about hardening your system to keep it both secure and scalable.

Learn more about modern authentication methods

FAQ

How does Scalekit handle OTP expiry, and can I customize the expiration time for OTPs?

Scalekit provides an expires_in parameter in its SendPasswordlessEmail API, which allows you to set the expiration time for OTPs and magic links. The default is 300 seconds (5 minutes), but you can customize it by passing a different value. Once the OTP expires, it can no longer be used, and users must request a new one. Scalekit automatically handles the expiration and clears expired tokens from its backend, ensuring security.

How does Scalekit ensure the security of the OTP and magic link generation process?

Scalekit uses AES-256 encryption to secure OTPs and magic links during generation and transmission. The tokens are one-time use, meaning once they are verified or expired, they cannot be reused. Additionally, the system employs rate-limiting to prevent abuse of the passwordless authentication process, limiting the number of requests a user can make within a set time frame. This ensures that the flow is both secure and resilient to brute force attacks.

What happens if my Go Fiber application needs to handle millions of concurrent requests during peak traffic?

To handle millions of concurrent requests, ensure that your Go Fiber application leverages horizontal scaling by running multiple instances of your app behind a load balancer. This can be achieved with containerization (e.g., Docker or Kubernetes). Additionally, use rate limiting to control the request load per user and optimize any external API calls to Scalekit by using asynchronous operations and caching. Offload tasks that don't need to be synchronous, like sending emails, to background jobs.

How do I ensure that my application is resilient to DDoS attacks when using passwordless authentication?

To make your application resilient to DDoS attacks, implement rate-limiting and IP blacklisting for endpoints like /request-auth to prevent abuse from malicious actors. Using a Web Application Firewall (WAF) in front of your app can help detect and block potential DDoS traffic. Additionally, consider using a queue-based system for processing authentication requests to ensure that requests are handled in a controlled manner during spikes in traffic.

How can I ensure my Go Fiber app’s session data remains secure across multiple instances or containers?

To ensure secure session management across multiple instances or containers, use a distributed session store like Redis or Consul. This allows session data (like authRequestId and email) to be stored centrally, making it accessible to all instances of your app. Additionally, use secure cookies with HTTPOnly and SameSite flags enabled to protect the session from unauthorized access and cross-site request forgery (CSRF) attacks.

No items found.
On this page
Share this article
Modernize login flows

Acquire enterprise customers with zero upfront cost

Every feature unlocked. No hidden fees.
Start Free
$0
/ month
1 million Monthly Active Users
100 Monthly Active Organizations
1 SSO and SCIM connection each
20K Tool Calls
10K Connected Accounts
Unlimited Dev & Prod environments