Error Reference

This page documents every error response you may encounter when integrating with UniAuth. Understanding error formats, status codes, and troubleshooting steps will help you build robust integrations that handle edge cases gracefully.

Error Response Formats

UniAuth uses two distinct error response formats depending on the API surface. All error responses include a Content-Type: application/json header.

Authentication and User APIs

Endpoints under /api/auth/* and /api/user/* return errors in the following format:

{
  "success": false,
  "message": "Invalid email or password"
}

The success field is always false for error responses, and the message field contains a human-readable description of what went wrong. Some endpoints may include additional fields such as field (indicating which input failed validation) or remaining (for lockout timing).

OAuth2 and OIDC APIs

Endpoints under /api/oauth/* follow the OAuth 2.0 error response format defined in RFC 6749 Section 5.2:

{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired"
}

The error field is a machine-readable error code from a predefined set (see the OAuth2 Error Codes table below). The error_description field provides additional context for developers. Your application should branch on the error code, not the description text, as descriptions may change.

HTTP Status Codes

The following table summarizes the HTTP status codes returned by UniAuth APIs and their typical causes:

CodeMeaningCommon Causes
200SuccessRequest processed successfully
201CreatedUser registered, OAuth client created, resource created
400Bad RequestMissing required parameters, validation failure, malformed request body, invalid token format
401UnauthorizedInvalid credentials, expired session or access token, failed 2FA verification, missing authorization header
403ForbiddenEmail not verified, insufficient role permissions, CORS policy blocked the request, admin-only endpoint
404Not FoundUser does not exist, session not found, OAuth client not found, invalid endpoint path
409ConflictDuplicate email address, duplicate username, OAuth client name already taken
401Unauthorized (Lockout)Account temporarily locked due to too many failed login attempts (returns generic "Invalid credentials" to prevent account enumeration)
429Too Many RequestsRate limit exceeded for this endpoint and IP address
500Server ErrorInternal server error, database unavailable, unexpected failure

OAuth2 Error Codes

The following error codes may be returned by OAuth2 endpoints ( /api/oauth/token, /api/oauth/authorize, /api/oauth/revoke, /api/oauth/introspect):

Error CodeHTTP StatusDescription
invalid_client401Client authentication failed. The client ID is unknown, the client secret is incorrect, or the client is not authorized for the requested grant type.
invalid_grant400The authorization code, refresh token, or resource owner credentials are invalid, expired, revoked, or were issued to a different client. Also returned for PKCE code verifier mismatches.
invalid_request400The request is missing a required parameter, includes an unsupported parameter, or is otherwise malformed.
invalid_scope400The requested scope is invalid, unknown, or not permitted for this client. Supported scopes: openid, profile, email, phone, address.
unsupported_grant_type400The grant type is not supported. UniAuth supports authorization_code, refresh_token, client_credentials, and urn:ietf:params:oauth:grant-type:device_code.
unauthorized_client403The client is not authorized to use the requested grant type, or the redirect URI does not match any registered for this client.
access_denied403The user denied the consent request, or the resource owner did not grant the requested permissions.
authorization_pending400Device flow only. The user has not yet completed the authorization on the verification page. Continue polling.
slow_down400Device flow only. You are polling too frequently. Increase your polling interval by 5 seconds.
expired_token400Device flow only. The device code has expired. The user must restart the device authorization flow.

Rate Limit Errors (429)

When you exceed the rate limit for an endpoint, UniAuth returns a 429 Too Many Requests response. The response includes a Retry-After header indicating how many seconds to wait before making another request.

HTTP/1.1 429 Too Many Requests
Retry-After: 900
Content-Type: application/json

{
  "success": false,
  "message": "Too many requests. Please try again later."
}

Rate limits are enforced per IP address within a sliding window. Different endpoints have different limits. Sensitive endpoints like login and registration have stricter limits to prevent brute-force attacks. See the Rate Limits page for the full table of per-endpoint limits.

Tip: Always check for the Retry-After header when you receive a 429 response. Use it to schedule your next retry rather than using a fixed delay.

Account Lockout Behavior

UniAuth implements progressive account lockout to protect against brute-force password attacks. After multiple consecutive failed login attempts, the account is temporarily locked. The lockout duration increases with each threshold:

Failed AttemptsLockout Duration
5 attempts1 minute
10 attempts5 minutes
15 attempts15 minutes
20+ attempts1 hour

Note: The thresholds above are based on the default maxLoginAttempts setting of 20. These thresholds are proportional to the configured value, so administrators who change the maximum will see the lockout tiers scale accordingly.

When an account is locked, the login endpoint returns a 401 Unauthorized response with a generic invalid credentials message. This is intentional — returning a distinct status code or lockout details would allow attackers to enumerate which accounts exist.

HTTP/1.1 401 Unauthorized
Content-Type: application/json

{
  "success": false,
  "message": "Invalid credentials"
}

The response is deliberately identical to a normal failed login to prevent account enumeration. The lockout counter resets automatically upon a successful login. Users can also bypass the lockout by authenticating with a passkey, as passkey authentication does not rely on the password.

Note: Administrators can unlock a user account manually from the Admin Panel by navigating to the user's profile and clicking Unlock Account.

Troubleshooting Common Errors

The following table covers the most frequently encountered errors during integration and how to resolve them:

ErrorCauseSolution
redirect_uri mismatchThe redirect URI in your authorization request does not match any URI registered for your application.Ensure the redirect URI in your request exactly matches one registered in your app settings at Developer Console. The comparison is exact — trailing slashes, ports, and protocols must all match.
CORS blockedYour frontend is making a cross-origin request to an endpoint that does not allow CORS.Only OAuth endpoints (token, userinfo, revoke, introspect, end-session) and SCIM endpoints allow cross-origin requests. Auth endpoints like login and register must be called from the same origin or from your backend server.
invalid_grant (expired code)The authorization code has expired before it was exchanged for tokens.Authorization codes expire after 10 minutes and can only be used once. Redirect the user to start the authorization flow again. Ensure your callback handler exchanges the code immediately upon receiving it.
invalid_grant (PKCE mismatch)The code verifier does not match the code challenge sent during authorization.Ensure you are sending the same code_verifier that was used to generate the code_challenge in the authorization request. The verifier must be stored between the authorization redirect and the token exchange.
Email not verified (403)The user has not verified their email address.The user must click the verification link sent to their email during registration. You can request a new verification email via the account settings. OAuth flows will not complete until the email is verified.
invalid_clientClient credentials are incorrect or the client has been deactivated.Verify your client ID and client secret in the Developer Console. If you recently rotated credentials, make sure your application is using the new values.
Duplicate email (409)A user with this email address already exists.Each email address can only be associated with one account. The user should sign in with their existing account, or use a different email address to register. If they forgot their password, direct them to the password reset flow.
Password too weak (400)The chosen password does not meet strength requirements.UniAuth enforces password strength scoring on a 0–4 scale. Passwords must score at least "fair" (2/4). Encourage users to use longer passwords with a mix of characters. Passwords containing the user's email or name are penalized.
Token expired (401)The access token has expired.Access tokens are valid for 1 hour. Use your refresh token to obtain a new access token via the token endpoint. If the refresh token is also expired (30 days), the user must re-authenticate.

Error Handling Best Practices

  • Always check the HTTP status code first. Use the status code to determine the category of error before parsing the response body.
  • Branch on error codes, not messages. For OAuth responses, use the error field for control flow. Error descriptions are informational and may change.
  • Implement retry logic for transient errors. Status codes 429 and 500 are candidates for retry with exponential backoff. Do not retry 400, 401, 403, or 409 errors as they indicate a problem with the request itself.
  • Log error responses for debugging. Include the full response body, status code, and request details in your application logs. This is invaluable when diagnosing integration issues.
  • Show user-friendly messages. Never display raw error responses to end users. Map error codes to human-friendly messages in your application's language.
  • Handle token refresh proactively. Rather than waiting for a 401 error, check the token expiration time and refresh proactively before it expires.

Example: Robust Error Handling

async function callUniAuthAPI(url, options) {
  const response = await fetch(url, options);

  if (response.ok) {
    return await response.json();
  }

  const body = await response.json().catch(() => ({}));

  switch (response.status) {
    case 401:
      // Token expired — attempt refresh
      if (body.error === "invalid_grant" || body.message?.includes("expired")) {
        const refreshed = await refreshAccessToken();
        if (refreshed) {
          return callUniAuthAPI(url, {
            ...options,
            headers: { ...options.headers, Authorization: `Bearer ${refreshed.access_token}` }
          });
        }
      }
      // Redirect to login
      window.location.href = "/login";
      break;

    case 429:
      // Rate limited — wait and retry
      const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10);
      await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      return callUniAuthAPI(url, options);

    case 423:
      // Account locked (may be returned by admin APIs)
      throw new Error("Account locked. Please try again later.");

    default:
      throw new Error(body.message || body.error_description || "An unexpected error occurred");
  }
}