> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/pocketbase/pocketbase/llms.txt
> Use this file to discover all available pages before exploring further.

# OTP authentication

> Enable passwordless authentication with one-time passwords in PocketBase

OTP (One-Time Password) authentication allows users to sign in using a temporary code sent to their email, providing a passwordless authentication experience.

## Configuration

You can enable OTP authentication in your auth collection settings:

```go theme={null}
type OTPConfig struct {
    Enabled       bool          `json:"enabled"`
    Duration      int64         `json:"duration"`      // in seconds
    Length        int           `json:"length"`        // password length
    EmailTemplate EmailTemplate `json:"emailTemplate"`
}
```

**Default configuration:**

```go theme={null}
OTP: OTPConfig{
    Enabled:  false,
    Duration: 180,  // 3 minutes
    Length:   8,    // 8-digit code
    EmailTemplate: defaultOTPTemplate,
}
```

<Note>
  OTP duration must be between 10 and 86400 seconds (24 hours). The minimum password length is 4 characters.
</Note>

## OTP authentication flow

The OTP authentication process consists of two steps:

<Steps>
  <Step title="Request OTP">
    User submits their email to request an OTP code.

    ```bash theme={null}
    curl -X POST http://localhost:8090/api/collections/users/request-otp \
      -H "Content-Type: application/json" \
      -d '{"email": "test@example.com"}'
    ```

    **Response (200):**

    ```json theme={null}
    {
      "otpId": "pb_otp_abc123xyz789"
    }
    ```

    <Info>
      The API always returns a 200 response with an `otpId` (real or dummy) to prevent email enumeration attacks.
    </Info>
  </Step>

  <Step title="Receive email">
    User receives an email with the OTP code (usually 8 digits by default).
  </Step>

  <Step title="Authenticate with OTP">
    User submits the OTP ID and password to complete authentication.

    ```bash theme={null}
    curl -X POST http://localhost:8090/api/collections/users/auth-with-otp \
      -H "Content-Type: application/json" \
      -d '{
        "otpId": "pb_otp_abc123xyz789",
        "password": "12345678"
      }'
    ```

    **Success response (200):**

    ```json theme={null}
    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "record": {
        "id": "RECORD_ID",
        "email": "test@example.com",
        "verified": true,
        ...
      }
    }
    ```
  </Step>
</Steps>

## Request OTP endpoint

**POST** `/api/collections/{collection}/request-otp`

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST http://localhost:8090/api/collections/users/request-otp \
    -H "Content-Type: application/json" \
    -d '{"email": "user@example.com"}'
  ```

  ```javascript JavaScript theme={null}
  const pb = new PocketBase('http://localhost:8090');

  const result = await pb.collection('users').requestOTP('user@example.com');
  console.log(result.otpId); // Save this for the next step
  ```

  ```go Go theme={null}
  import (
      "github.com/pocketbase/pocketbase/core"
      "github.com/pocketbase/pocketbase/tools/security"
  )

  // Create OTP
  otp := core.NewOTP(app)
  otp.SetCollectionRef(collection.Id)
  otp.SetRecordRef(record.Id)
  otp.SetPassword(security.RandomStringWithAlphabet(8, "1234567890"))
  err := app.Save(otp)

  // Send OTP email
  err = mails.SendRecordOTP(app, record, otp.Id, password)
  ```
</CodeGroup>

**Request validation:**

```go theme={null}
type createOTPForm struct {
    Email string `json:"email"` // Required, 1-255 chars, valid email format
}
```

### Rate limiting and abuse prevention

PocketBase implements several protections against OTP abuse:

1. **Maximum active OTPs**: Users can have a maximum of 10 non-expired OTPs. After that, the last issued OTP is reused:

```go theme={null}
// From apis/record_auth_otp_request.go:75
if totalRecent > 9 {
    otp = otps[0] // Reuse the last issued OTP
    e.App.Logger().Warn(
        "Too many OTP requests - reusing the last issued",
        "email", form.Email,
        "recordId", e.Record.Id,
    )
}
```

2. **Background email sending**: OTP emails are sent in the background to prevent timing attacks:

```go theme={null}
// From apis/record_auth_otp_request.go:103
routine.FireAndForget(func() {
    err = mails.SendRecordOTP(originalApp, e.Record, otp.Id, e.Password)
    if err != nil {
        originalApp.Logger().Error("Failed to send OTP email", "error", err)
    }
})
```

3. **Dummy responses**: Returns a fake `otpId` if the email doesn't exist to prevent enumeration:

```go theme={null}
// From apis/record_auth_otp_request.go:53
if e.Record == nil {
    e.JSON(http.StatusOK, map[string]string{
        "otpId": core.GenerateDefaultRandomId(),
    })
    return fmt.Errorf("missing or invalid auth record")
}
```

## Authenticate with OTP endpoint

**POST** `/api/collections/{collection}/auth-with-otp`

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST http://localhost:8090/api/collections/users/auth-with-otp \
    -H "Content-Type: application/json" \
    -d '{
      "otpId": "pb_otp_abc123xyz789",
      "password": "12345678"
    }'
  ```

  ```javascript JavaScript theme={null}
  const pb = new PocketBase('http://localhost:8090');

  const authData = await pb.collection('users').authWithOTP(
    otpId,
    '12345678'
  );

  console.log(authData.token);
  console.log(authData.record);
  ```

  ```go Go theme={null}
  // Validate OTP
  otp, err := app.FindOTPById(otpId)
  if err != nil {
      return err
  }

  if otp.HasExpired(collection.OTP.DurationTime()) {
      return errors.New("OTP is expired")
  }

  if !otp.ValidatePassword(password) {
      return errors.New("incorrect password")
  }

  // Get the auth record
  record, err := app.FindRecordById(otp.CollectionRef(), otp.RecordRef())
  ```
</CodeGroup>

**Request body:**

```go theme={null}
type authWithOTPForm struct {
    OTPId    string `json:"otpId"`    // Required, 1-255 characters
    Password string `json:"password"` // Required, 1-71 characters
}
```

### Validation and security

The OTP authentication endpoint performs several security checks:

```go theme={null}
// From apis/record_auth_with_otp.go:38
event.OTP, err = e.App.FindOTPById(form.OTPId)
if err != nil {
    return e.BadRequestError("Invalid or expired OTP", err)
}

if event.OTP.CollectionRef() != collection.Id {
    return e.BadRequestError("Invalid or expired OTP", 
        errors.New("the OTP is for a different collection"))
}

if event.OTP.HasExpired(collection.OTP.DurationTime()) {
    return e.BadRequestError("Invalid or expired OTP", 
        errors.New("the OTP is expired"))
}
```

**Extra rate limiting**: OTP validation has an additional rate limit to prevent brute force attacks:

```go theme={null}
// From apis/record_auth_with_otp.go:57
err = checkRateLimit(e, "@pb_otp_"+event.Record.Id, 
    core.RateLimitRule{MaxRequests: 5, Duration: 180})
if err != nil {
    return e.TooManyRequestsError(
        "Too many attempts, please try again later with a new OTP.", nil)
}
```

<Warning>
  Users are limited to 5 OTP validation attempts per 3 minutes per record. After exceeding the limit, they must request a new OTP.
</Warning>

## Email verification

When a user successfully authenticates with an OTP sent to their email, PocketBase automatically verifies their email if it matches:

```go theme={null}
// From apis/record_auth_with_otp.go:71
otpSentTo := e.OTP.SentTo()
if !e.Record.Verified() && otpSentTo != "" && e.Record.Email() == otpSentTo {
    e.Record.SetVerified(true)
    err = e.App.Save(e.Record)
    if err != nil {
        e.App.Logger().Error(
            "Failed to update record verified state after successful OTP validation",
            "error", err,
            "otpId", e.OTP.Id,
        )
    }
}
```

## OTP cleanup

After successful authentication, the OTP is automatically deleted:

```go theme={null}
// From apis/record_auth_with_otp.go:85
err = e.App.Delete(e.OTP)
if err != nil {
    e.App.Logger().Error("Failed to delete used OTP", "error", err)
}
```

## Email template customization

You can customize the OTP email template:

```go theme={null}
type EmailTemplate struct {
    Subject string `json:"subject"`
    Body    string `json:"body"`
}
```

**Available placeholders:**

* `{APP_NAME}` - Your application name
* `{APP_URL}` - Your application URL
* `{OTP}` - The one-time password
* `{OTP_ID}` - The OTP record ID

**Example template:**

```html theme={null}
Subject: Your login code for {APP_NAME}

Body:
Hello,

Your one-time login code is: {OTP}

This code will expire in 3 minutes.

If you didn't request this code, please ignore this email.

Thanks,
The {APP_NAME} Team
```

## OTP model

The OTP record is stored in the `_otps` system collection:

```go theme={null}
type OTP struct {
    *Record
}

// Core methods
func (o *OTP) CollectionRef() string
func (o *OTP) SetCollectionRef(collectionId string)
func (o *OTP) RecordRef() string
func (o *OTP) SetRecordRef(recordId string)
func (o *OTP) SentTo() string
func (o *OTP) SetSentTo(email string)
func (o *OTP) ValidatePassword(password string) bool
func (o *OTP) SetPassword(password string)
func (o *OTP) HasExpired(maxElapsed time.Duration) bool
```

## Integration with MFA

OTP can be used as one of the authentication methods for multi-factor authentication:

```go theme={null}
// From apis/record_auth_with_otp.go:90
return RecordAuthResponse(e.RequestEvent, e.Record, core.MFAMethodOTP, nil)
```

If MFA is enabled, users can authenticate first with password, then with OTP as the second factor.

## Use cases

### Passwordless authentication

OTP is ideal for applications that want to offer passwordless authentication:

```javascript theme={null}
// Request OTP
const { otpId } = await pb.collection('users').requestOTP('user@example.com');

// Prompt user for the code they received
const code = prompt('Enter the code from your email:');

// Authenticate
const authData = await pb.collection('users').authWithOTP(otpId, code);
```

### Magic links

You can implement magic link authentication by encoding the OTP in the email link:

```html theme={null}
Click here to login: https://yourdomain.com/auth?otpId={OTP_ID}&code={OTP}
```

### Account recovery

OTP can serve as an alternative recovery method when users forget their password.

## Security best practices

<Warning>
  OTP codes should be random and unpredictable. By default, PocketBase uses 8-digit numeric codes.
</Warning>

1. **Short expiration**: Keep OTP duration short (3-10 minutes) to minimize the attack window.

2. **One-time use**: OTPs are automatically deleted after successful authentication.

3. **Rate limiting**: Built-in rate limiting prevents brute force attacks on OTP validation.

4. **HTTPS only**: Always use HTTPS to prevent OTP interception.

5. **Consider longer codes**: For high-security applications, increase the OTP length beyond 8 characters.

## Custom OTP hooks

You can customize OTP behavior using event hooks:

```go theme={null}
// Customize OTP creation
app.OnRecordRequestOTPRequest().Bind(&hook.Handler[*core.RecordCreateOTPRequestEvent]{
    Func: func(e *core.RecordCreateOTPRequestEvent) error {
        // Use a custom OTP format (e.g., alphanumeric)
        e.Password = security.RandomString(10)
        
        // Add custom validation
        if e.Record.GetBool("otpDisabled") {
            return errors.New("OTP authentication is disabled for this account")
        }
        
        return e.Next()
    },
})

// Track OTP usage
app.OnRecordAuthWithOTPRequest().Bind(&hook.Handler[*core.RecordAuthWithOTPRequestEvent]{
    Func: func(e *core.RecordAuthWithOTPRequestEvent) error {
        log.Printf("OTP auth for user: %s", e.Record.Email())
        return e.Next()
    },
})
```

## Configuration example

```go theme={null}
collection, _ := app.FindCollectionByNameOrId("users")

// Enable OTP authentication
collection.OTP.Enabled = true
collection.OTP.Duration = 300 // 5 minutes
collection.OTP.Length = 10    // 10-digit codes

// Customize email template
collection.OTP.EmailTemplate.Subject = "Your login code"
collection.OTP.EmailTemplate.Body = `
Hi there,

Your verification code is: {OTP}

This code expires in 5 minutes.

Best regards,
Your App
`

app.Save(collection)
```

## Related topics

<CardGroup cols={2}>
  <Card title="Email/Password auth" icon="envelope" href="/auth/email-password">
    Traditional password authentication
  </Card>

  <Card title="OAuth2" icon="globe" href="/auth/oauth2">
    Social login providers
  </Card>

  <Card title="MFA" icon="shield-halved" href="/auth/mfa">
    Use OTP as second factor
  </Card>

  <Card title="Email templates" icon="envelope-open-text" href="/auth/email-password">
    Customize email templates
  </Card>
</CardGroup>
