Security Considerations

Copy Markdown View Source

This document outlines known security considerations and recommendations for applications using KeenAuth.

Addressed Issues

1. Open Redirect Prevention (FIXED)

KeenAuth now validates all redirect URLs through KeenAuth.Helpers.RedirectValidator.

Default behavior: Only relative URLs (starting with /) are allowed.

Custom validation: Configure a callback for custom validation logic:

config :keen_auth,
  redirect_validator: &MyApp.Auth.validate_redirect/2

The callback receives (url, conn) and returns {:ok, url} or :error:

# Allow specific domains
def validate_redirect(url, _conn) do
  uri = URI.parse(url)
  allowed = ["myapp.com", "app.myapp.com", nil]  # nil = relative URL
  if uri.host in allowed, do: {:ok, url}, else: :error
end

# Database-backed allowlist
def validate_redirect(url, _conn) do
  uri = URI.parse(url)
  if AllowedDomains.exists?(uri.host), do: {:ok, url}, else: :error
end

2. Input Length Limits (FIXED)

KeenAuth now validates input lengths:

InputMax Length
Redirect URL2048 bytes
Provider name64 bytes

Provider names are also validated to only contain alphanumeric characters, hyphens, and underscores.

3. Rate Limiting (Application Responsibility)

Location: OAuth endpoints, Email authentication

KeenAuth does not include built-in rate limiting because:

  • Applications may use infrastructure-level rate limiting (nginx, CloudFlare, AWS WAF)
  • Different applications have different rate limiting requirements
  • Rate limiting libraries (Hammer, PlugAttack) are easy to integrate

Note: Email authentication includes timing attack mitigation via Process.sleep(Enum.random(100..300//10)).

Recommended Rate Limits

EndpointRouteLimitWindow
OAuth start/auth/:provider/new10-201 minute
OAuth callback/auth/:provider/callback10-201 minute
Sign out/auth/:provider/delete101 minute
Email login/auth/email51 minute

Implementation with Hammer

Add Hammer to your dependencies:

# mix.exs
{:hammer, "~> 6.1"}

Create a rate limiting plug:

defmodule MyAppWeb.Plugs.AuthRateLimiter do
  @behaviour Plug

  import Plug.Conn

  # 10 requests per minute per IP
  @limit 10
  @window_ms 60_000

  def init(opts), do: opts

  def call(conn, _opts) do
    case check_rate(conn) do
      {:allow, _count} ->
        conn

      {:deny, _limit} ->
        conn
        |> put_resp_content_type("text/plain")
        |> send_resp(429, "Too many requests. Please try again later.")
        |> halt()
    end
  end

  defp check_rate(conn) do
    ip = conn.remote_ip |> :inet.ntoa() |> to_string()
    Hammer.check_rate("auth:#{ip}", @window_ms, @limit)
  end
end

Add the plug to your authentication pipeline:

# router.ex
pipeline :authentication do
  plug MyAppWeb.Plugs.AuthRateLimiter  # Add rate limiting
  plug KeenAuth.Plug, otp_app: :my_app
end

scope "/auth" do
  pipe_through [:browser, :authentication]
  KeenAuth.authentication_routes()
end

Implementation with PlugAttack

# mix.exs
{:plug_attack, "~> 0.4"}

# lib/my_app_web/plugs/auth_rate_limiter.ex
defmodule MyAppWeb.Plugs.AuthRateLimiter do
  use PlugAttack

  rule "auth rate limit", conn do
    if String.starts_with?(conn.request_path, "/auth") do
      throttle(conn.remote_ip, period: 60_000, limit: 10)
    end
  end
end

Infrastructure-Level Rate Limiting

If you prefer infrastructure-level rate limiting:

Nginx:

limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/m;

location /auth {
    limit_req zone=auth burst=5 nodelay;
    # ... proxy config
}

CloudFlare: Use Rate Limiting Rules in the dashboard to limit /auth/* endpoints.

4. Session Storage Size (LOW)

Location: lib/storage/session.ex

Issue: OAuth tokens (access_token, id_token, refresh_token) are stored directly in session. Some providers return large tokens (especially with many claims/groups), which could:

  • Exceed cookie size limits (4KB)
  • Cause session storage issues

Recommendation:

  • Use server-side session storage (ETS, Redis, database)
  • Or implement a custom Storage that only stores essential data

5. JWT Configuration (LOW)

Location: lib/token/jwt.ex

Issue: The JWT module uses bare use Joken.Config without explicit configuration. Applications must configure Joken properly.

Recommendation: Ensure your application configures Joken with:

config :joken, default_signer: [
  signer_alg: "HS256",  # or RS256 for asymmetric
  key_octet: "your-secret-key-at-least-256-bits"
]

6. Provider Name Handling (LOW)

Location: lib/helpers/binary.ex

Issue: Uses String.to_existing_atom/1 which will raise ArgumentError if the atom doesn't exist. While this prevents atom table exhaustion attacks, it could cause unhandled crashes.

Note: This is generally safe as provider names should be pre-defined in configuration.

Security Best Practices for Implementers

  1. Always validate redirect URLs - Implement allowlist validation in your Processor
  2. Use HTTPS only - Ensure all OAuth redirect URIs use HTTPS
  3. Implement rate limiting - Protect authentication endpoints from abuse
  4. Use server-side sessions - Avoid cookie size limitations and token exposure
  5. Validate OAuth state - The library uses Assent which handles state validation
  6. Keep dependencies updated - Regularly update Assent and other dependencies
  7. Log authentication events - Implement audit logging in your Processor
  8. Set secure cookie flags - Ensure session cookies have secure: true, http_only: true, same_site: :lax

Reporting Security Issues

If you discover a security vulnerability, please report it by:

  1. Opening a private security advisory on GitHub
  2. Or emailing the maintainers directly

Please do not open public issues for security vulnerabilities.