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

# Custom Challenge Types

> Implement custom challenge methods for Anubis

Anubis supports custom challenge types through the `challenge.Impl` interface. This allows you to create specialized bot detection mechanisms beyond the built-in proof-of-work, meta-refresh, and Preact challenges.

## Challenge Interface

All challenge implementations must satisfy the `challenge.Impl` interface:

```go theme={null}
type Impl interface {
	// Setup registers any additional routes with the Impl for assets or API routes.
	Setup(mux *http.ServeMux)

	// Issue a new challenge to the user, called by the Anubis.
	Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *IssueInput) (templ.Component, error)

	// Validate a challenge, making sure that it passes muster.
	Validate(r *http.Request, lg *slog.Logger, in *ValidateInput) error
}
```

Source: `lib/challenge/interface.go:59-68`

## Input Structures

### IssueInput

Provided when issuing a new challenge:

```go theme={null}
type IssueInput struct {
	Impressum *config.Impressum
	Rule      *policy.Bot
	Challenge *Challenge
	OGTags    map[string]string
	Store     store.Interface
}
```

### ValidateInput

Provided when validating a challenge response:

```go theme={null}
type ValidateInput struct {
	Rule      *policy.Bot
	Challenge *Challenge
	Store     store.Interface
}
```

### Challenge Metadata

```go theme={null}
type Challenge struct {
	IssuedAt       time.Time         `json:"issuedAt"`
	Metadata       map[string]string `json:"metadata"`
	ID             string            `json:"id"`
	Method         string            `json:"method"`
	RandomData     string            `json:"randomData"`
	PolicyRuleHash string            `json:"policyRuleHash,omitempty"`
	Difficulty     int               `json:"difficulty,omitempty"`
	Spent          bool              `json:"spent"`
}
```

Source: `lib/challenge/challenge.go:6-15`

## Implementation Example: Proof-of-Work

Here's how the built-in proof-of-work challenge is implemented:

```go theme={null}
package proofofwork

import (
	"crypto/subtle"
	"fmt"
	"log/slog"
	"net/http"
	"strconv"
	"strings"

	chall "github.com/TecharoHQ/anubis/lib/challenge"
	"github.com/TecharoHQ/anubis/lib/localization"
	"github.com/a-h/templ"
)

func init() {
	chall.Register("fast", &Impl{Algorithm: "fast")
	chall.Register("slow", &Impl{Algorithm: "slow")
}

type Impl struct {
	Algorithm string
}

func (i *Impl) Setup(mux *http.ServeMux) {}

func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
	loc := localization.GetLocalizer(r)
	return page(loc), nil
}

func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
	rule := in.Rule
	challenge := in.Challenge.RandomData

	nonceStr := r.FormValue("nonce")
	if nonceStr == "" {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
	}

	nonce, err := strconv.Atoi(nonceStr)
	if err != nil {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
	}

	response := r.FormValue("response")
	if response == "" {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
	}

	calcString := fmt.Sprintf("%s%d", challenge, nonce)
	calculated := internal.SHA256sum(calcString)

	if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
	}

	// compare the leading zeroes
	if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
		return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
	}

	return nil
}
```

Source: `lib/challenge/proofofwork/proofofwork.go`

## Implementation Example: Meta Refresh

The meta-refresh challenge validates timing constraints:

```go theme={null}
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *challenge.ValidateInput) error {
	wantTime := in.Challenge.IssuedAt.Add(time.Duration(in.Rule.Challenge.Difficulty) * 800 * time.Millisecond)

	if time.Now().Before(wantTime) {
		return challenge.NewError("validate", "insufficient time", 
			fmt.Errorf("%w: wanted user to wait until at least %s", challenge.ErrFailed, wantTime.Format(time.RFC3339)))
	}

	gotChallenge := r.FormValue("challenge")

	if subtle.ConstantTimeCompare([]byte(in.Challenge.RandomData), []byte(gotChallenge)) != 1 {
		return challenge.NewError("validate", "invalid response", 
			fmt.Errorf("%w: wanted response %s but got %s", challenge.ErrFailed, in.Challenge.RandomData, gotChallenge))
	}

	return nil
}
```

Source: `lib/challenge/metarefresh/metarefresh.go:51-64`

## Registration

Register your challenge implementation in an `init()` function:

```go theme={null}
func init() {
	challenge.Register("mycustom", &MyCustomImpl{)
}
```

The registry is thread-safe and uses `sync.RWMutex` for concurrent access.

Source: `lib/challenge/interface.go:15-32`

## Error Handling

Use the challenge error constructors for consistent error reporting:

```go theme={null}
var (
	ErrFailed        = errors.New("challenge: user failed challenge")
	ErrMissingField  = errors.New("challenge: missing field")
	ErrInvalidFormat = errors.New("challenge: field has invalid format")
)

func NewError(verb, publicReason string, privateReason error) *Error {
	return &Error{
		Verb:          verb,
		PublicReason:  publicReason,
		PrivateReason: privateReason,
		StatusCode:    http.StatusForbidden,
	}
}
```

Source: `lib/challenge/error.go:9-22`

## Best Practices

1. **Security-first**: Use constant-time comparison for secrets (`crypto/subtle.ConstantTimeCompare`)
2. **Difficulty scaling**: Honor `in.Rule.Challenge.Difficulty` from the policy configuration
3. **Localization**: Use `localization.GetLocalizer(r)` for internationalized UI
4. **Metrics**: Emit Prometheus metrics for observability (see `lib/challenge/metrics.go`)
5. **Structured logging**: Use the provided `*slog.Logger` for diagnostic output
6. **Templ components**: Return `templ.Component` for HTML rendering consistency

## Configuration

Once registered, reference your challenge in policy files:

```yaml theme={null}
bots:
  - name: custom-challenge-rule
    action: challenge
    expression:
      - path.startsWith('/protected')
    challenge:
      algorithm: mycustom
      difficulty: 5
```

## Available Challenge Methods

Query registered challenges at runtime:

```go theme={null}
methods := challenge.Methods()  // Returns []string of all registered challenge types
impl, ok := challenge.Get("fast")  // Get a specific implementation
```

Source: `lib/challenge/interface.go:27-42`
