# Login

Sign in an existing user with username and password and receive an access token, a refresh token and the user profile.

## Overview

The endpoint checks the username and password and returns `accessToken`, `refreshToken`, `expiresIn` and `user`. Send the access token as `Authorization: Bearer <accessToken>` on protected routes, and exchange the refresh token at [Refresh Token](https://docs.aureahub.com/docs/auth-refresh.md) when the access token expires.

Where the user is looked up:

- If you send `tenantApiKey` in the body, only users of that tenant are searched. A key that does not match an active tenant returns `401` *Invalid tenant API key*.
- Otherwise the username is matched against platform-admin accounts first, then against users of the tenant identified by the `x-tenant-api-key` signing header.

The issued tokens are scoped to the user's own tenant. When you pass `createWallet`, the response can also contain `wallet` (`id`, `chain`, `address`, `isPrimary`, `createdAt`). Accounts created through [Google](https://docs.aureahub.com/docs/auth-google.md) or [Apple](https://docs.aureahub.com/docs/auth-apple.md) sign-in have no password and receive `401` *This account uses social sign-in*.

> ⚠️ This endpoint requires **tenant request signing**: send `x-tenant-api-key`, `x-timestamp` and `x-signature` as described in [Authentication](https://docs.aureahub.com/docs/authentication.md). It does not take a bearer token, and it is rate-limited to **20 requests per minute per client IP**.

## Endpoint

### `POST /v1/auth/login`

Checks username and password and returns an access token, a refresh token and the user. Requires tenant HMAC signing headers.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `username` | string | yes | 3–100 characters: letters, digits, underscore or hyphen |
| `password` | string | yes | The account password |
| `createWallet` | object | no | { chain }. If the user has no wallet on that chain, one is created (not on tenants that use MPC wallets). If they already have one, their primary wallet on that chain is returned in wallet. |
| `tenantApiKey` | string | no | Search for the user in this tenant instead (at least 32 characters) |

**Responses**

`200` OK

```json
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "expiresIn": 900,
  "user": {
    "id": "<user-id>",
    "username": "alice",
    "email": "alice@example.com",
    "qrCode": "<qr-code>",
    "role": "user",
    "createdAt": "2026-09-01T10:00:00.000Z"
  }
}
```

`401` Unauthorized

```json
{ "error": "UnauthorizedError", "message": "Invalid credentials" }
```

`404` Unknown API Key

```json
{ "error": "NotFoundError", "message": "Invalid API key" }
```

`400` Bad Request

```json
{ "error": "Bad Request", "message": "Request validation failed" }
```

`422` Validation Error

```json
{
  "statusCode": 422,
  "error": "Validation Error",
  "message": "Request validation failed",
  "details": [
    { "path": "username", "message": "Username can only contain letters, numbers, underscores, and hyphens" }
  ]
}
```

`429` Too Many Requests

```json
{ "statusCode": 429, "error": "Error", "message": "Rate limit exceeded, retry in 1 minute" }
```

**Example request**

```bash
curl -X POST https://api.aureahub.com/v1/auth/login \
  -H "Content-Type: application/json" \
  -H "x-tenant-api-key: <your-tenant-api-key>" \
  -H "x-timestamp: <unix-seconds>" \
  -H "x-signature: <hex-hmac-sha256>" \
  -d '{"username":"alice","password":"SecurePass123"}'
```

## Errors

On this endpoint, `400`, `401` and `404` bodies contain only `error` and `message`; `422` and `429` use the full envelope described in [Error Handling](https://docs.aureahub.com/docs/errors.md).

| Status | message | When |
| --- | --- | --- |
| 400 | `Request validation failed` | A required field is missing, username is shorter than 3 or longer than 100 characters, password is empty, or tenantApiKey is shorter than 32 characters. |
| 401 | `Invalid credentials` | The username does not exist or the password is wrong. |
| 401 | `This account uses social sign-in` | The account was created with Google or Apple and has no password. |
| 401 | `Account is not active` | The password is correct but the account is not active. |
| 401 | `Invalid tenant API key` | The tenantApiKey body field does not match an active tenant. |
| 401 | `Missing required HMAC headers: x-tenant-api-key, x-timestamp, x-signature` | Signature problem. Other messages: Invalid x-timestamp header, Request expired, Invalid signature, Replay detected. See Authentication. |
| 404 | `Invalid API key` | The x-tenant-api-key header does not match an active tenant. |
| 422 | `Request validation failed` | username contains characters other than letters, digits, underscore or hyphen, or createWallet is sent without chain. details lists each problem. |
| 429 | `Rate limit exceeded, retry in 1 minute` | More than 20 requests in one minute from the same client IP. See the retry-after header. |

## Implementation

The tenant signature must cover the exact bytes you send, so build the body string once and use it for both the signature and the request:

```javascript
import { createHash, createHmac } from 'node:crypto';

const API = 'https://api.aureahub.com';

// Tenant request signing: see Authentication → HMAC Request Signing
function signedHeaders(method, pathWithQuery, body = '') {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const bodyHash = createHash('sha256').update(body).digest('hex');
  const signature = createHmac('sha256', process.env.AUREA_TENANT_API_SECRET)
    .update(`${method}\n${pathWithQuery}\n${timestamp}\n${bodyHash}`)
    .digest('hex');
  return {
    'x-tenant-api-key': process.env.AUREA_TENANT_API_KEY,
    'x-timestamp': timestamp,
    'x-signature': signature,
  };
}

export async function login(username, password) {
  const path = '/v1/auth/login';
  const body = JSON.stringify({ username, password });

  const res = await fetch(API + path, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...signedHeaders('POST', path, body) },
    body, // exactly the string that was signed
  });
  const data = await res.json();

  if (!res.ok) {
    throw new Error(`${res.status}: ${data.message ?? data.error}`);
  }

  const { accessToken, refreshToken, expiresIn, user } = data;
  return { accessToken, refreshToken, expiresIn, user };
}
```

## Staying Signed In

`expiresIn` is returned as `900` seconds (15 minutes), which matches the default access-token lifetime. The lifetime itself is a server setting, so read the token's `exp` claim if you need the exact expiry. Refresh shortly before it passes, or refresh once when a protected call returns `401`. See [Refresh Token](https://docs.aureahub.com/docs/auth-refresh.md).

```javascript
function scheduleRefresh(expiresInSeconds, refresh) {
  // refresh one minute before the access token expires
  const delayMs = Math.max(expiresInSeconds - 60, 0) * 1000;
  return setTimeout(refresh, delayMs);
}
```

---

Web version: https://docs.aureahub.com/#auth-login
