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:
| Code | Meaning | Common Causes |
|---|---|---|
200 | Success | Request processed successfully |
201 | Created | User registered, OAuth client created, resource created |
400 | Bad Request | Missing required parameters, validation failure, malformed request body, invalid token format |
401 | Unauthorized | Invalid credentials, expired session or access token, failed 2FA verification, missing authorization header |
403 | Forbidden | Email not verified, insufficient role permissions, CORS policy blocked the request, admin-only endpoint |
404 | Not Found | User does not exist, session not found, OAuth client not found, invalid endpoint path |
409 | Conflict | Duplicate email address, duplicate username, OAuth client name already taken |
401 | Unauthorized (Lockout) | Account temporarily locked due to too many failed login attempts (returns generic "Invalid credentials" to prevent account enumeration) |
429 | Too Many Requests | Rate limit exceeded for this endpoint and IP address |
500 | Server Error | Internal 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 Code | HTTP Status | Description |
|---|---|---|
invalid_client | 401 | Client authentication failed. The client ID is unknown, the client secret is incorrect, or the client is not authorized for the requested grant type. |
invalid_grant | 400 | The 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_request | 400 | The request is missing a required parameter, includes an unsupported parameter, or is otherwise malformed. |
invalid_scope | 400 | The requested scope is invalid, unknown, or not permitted for this client. Supported scopes: openid, profile, email, phone, address. |
unsupported_grant_type | 400 | The grant type is not supported. UniAuth supports authorization_code, refresh_token, client_credentials, and urn:ietf:params:oauth:grant-type:device_code. |
unauthorized_client | 403 | The client is not authorized to use the requested grant type, or the redirect URI does not match any registered for this client. |
access_denied | 403 | The user denied the consent request, or the resource owner did not grant the requested permissions. |
authorization_pending | 400 | Device flow only. The user has not yet completed the authorization on the verification page. Continue polling. |
slow_down | 400 | Device flow only. You are polling too frequently. Increase your polling interval by 5 seconds. |
expired_token | 400 | Device 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 Attempts | Lockout Duration |
|---|---|
| 5 attempts | 1 minute |
| 10 attempts | 5 minutes |
| 15 attempts | 15 minutes |
| 20+ attempts | 1 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:
| Error | Cause | Solution |
|---|---|---|
redirect_uri mismatch | The 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 blocked | Your 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_client | Client 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
errorfield 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");
}
}Related documentation: Rate Limits · Token Reference · Security Guide · API Reference