getlocksmith 3.1.0

Official async Rust client for the Locksmith public authentication API (JWT, OAuth, magic links).
Documentation
# Locksmith Rust SDK (`getlocksmith`) and Linux PAM integration

This document is written for an AI or engineer implementing a **PAM module** that **delegates authentication** (effectively “overwriting” or bypassing local `/etc/shadow` checks for selected services) to **Locksmith’s public HTTP API**, using the official Rust crate **`getlocksmith`**.

---

## 1. Crate facts

| Item                    | Value                                                                                                             |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Crate name on crates.io | `getlocksmith` (not `locksmith`)                                                                                  |
| Source in monorepo      | `sdks/rust/`                                                                                                      |
| Default API base URL    | `https://getlocksmith.dev`                                                                                        |
| TLS                     | `reqwest` with **rustls** (no OpenSSL in the client itself)                                                       |
| Style                   | **Async** (`async fn` on all network methods)                                                                     |
| API key                 | Must start with `lsm_live_` (**production**) or `lsm_sbx_` (**Sandbox**). Environment is derived from the prefix. |

**`Cargo.toml` dependency:**

```toml
getlocksmith = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```

`LocksmithClient::new` returns `Result<Self, getlocksmith::Error>` and validates the API key prefix.

---

## 2. Error type (`getlocksmith::Error`)

Handle these variants when mapping to PAM return codes:

- **`Api { code, message, status }`** — Locksmith JSON error (`401` invalid credentials, `403` banned, `429` rate limit, etc.).
- **`Http(reqwest::Error)`** — Transport (DNS, timeout, TLS). Map to `PAM_AUTHERR` or `PAM_AUTHINFO_UNAVAIL` per policy.
- **`InvalidApiKey`** — configuration error; log and return `PAM_ABORT` / `PAM_SERVICE_ERR`.
- **`InvalidResponse`** — bad envelope; `PAM_SERVICE_ERR`.
- **`Jwt`** — local JWT decode failures in `verify_token`.

---

## 3. Types you will Deserialize from the API

All success bodies use `{ "data": … }`; the client unwraps `data` for you.

- **`UserBase`**`id`, `email`, `role` (legacy single role), `meta` (JSON map).
- **`SignInResult`**`user: UserLogin` + `AuthTokens` flattened (`access_token`, `refresh_token`, `expires_in`).
- **`UserMe`**`/api/auth/me`; includes `roles: Vec<String>`, `permissions: Vec<String>`, `email_verified`, `two_factor_enabled`, `passkey_count`, timestamps.
- **`TokenPayload`** — decoded JWT: `sub`, `email`, `role`, `roles`, `permissions`, `environment`, `meta`, `aud`, `iss`, `iat`, `exp`.

**RBAC helpers (in-memory, no network):**

- `token_has_role(&TokenPayload, &str)` — checks **`roles`** vector only (does not fall back to legacy `role` string; if you need both, also compare `payload.role`).
- `token_has_permission(&TokenPayload, &str)` — checks **`permissions`**.

---

## 4. `LocksmithClient` API reference

Assume `let client = LocksmithClient::new(api_key, Some("https://getlocksmith.dev"))?;` (second arg: optional base URL override; `None` uses default).

### 4.1 Session / password auth (primary for “replace Unix password”)

| Method                                       | HTTP                                         | Use in PAM                                                                                                                 |
| -------------------------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `sign_in(email, password).await`             | `POST /api/auth/login`                       | **Main path:** validate the password the user typed against Locksmith.                                                     |
| `sign_up(email, password, meta).await`       | `POST /api/auth/signup`                      | Usually **not** for PAM login; could be used in provisioning flows.                                                        |
| `sign_out(refresh_token).await`              | `POST /api/auth/logout`                      | Optional: revoke refresh after session if you stored one.                                                                  |
| `refresh(refresh_token).await`               | `POST /api/auth/refresh`                     | Renew tokens if you persist refresh in a secure store (unusual for classic PAM).                                           |
| `get_user(access_token).await`               | `GET /api/auth/me` + `Authorization: Bearer` | After `sign_in`, **refresh RBAC from DB** (`roles` / `permissions`). Good for enforcing `pam_has_permission` style checks. |
| `verify_token(access_token, public_key_pem)` | Local RS256                                  | **Offline** validation if user presents JWT; PEM is the project’s **public** key for the same environment as the API key.  |

### 4.2 Magic link / password reset (optional for PAM)

| Method                                               | Notes                                                            |
| ---------------------------------------------------- | ---------------------------------------------------------------- |
| `send_magic_link(email, create_if_not_exists).await` | No API key in browser verify path for Magic Link **verify** URL. |
| `verify_magic_link(token, project_id).await`         | `GET` without `X-API-Key`; needs `project` id.                   |
| `send_password_reset` / `update_password`            | Not typical inside PAM `authenticate`.                           |

### 4.3 OAuth / OIDC (usually not for tty login)

- `initiate_oauth`, `exchange_oauth_code`, `complete_oidc_grant` — backend flows; ill-suited to classic `login(1)` unless you build a browser/cookie bridge.

### 4.4 RBAC admin (API key; not for every login)

These call `/api/auth/rbac/*` with `X-API-Key`. Use from **admin** tools, not from every `pam_sm_authenticate`:

- `rbac_list_roles`, `rbac_get_role`, `rbac_create_role`, `rbac_update_role`, `rbac_delete_role`, `rbac_set_role_permissions`
- `rbac_list_permissions`, `rbac_get_permission`, `rbac_create_permission`, `rbac_update_permission`, `rbac_delete_permission`
- `rbac_get_user_roles`, `rbac_assign_role`, `rbac_revoke_role`, `rbac_set_user_roles`

RBAC **create/update** bodies are `serde_json::Value`; align field names with the HTTP API (`camelCase` in JSON).

---

## 5. What “overwrite logins” means in PAM terms

You are **not** replacing the kernel. You are providing a **PAM module** (`.so`) that:

1. Runs inside **sshd**, **login**, **sudo**, **su**, etc., when the stack includes your module.
2. In `pam_sm_authenticate`, reads the **Linux username** and **password** (or other auth token) via PAM conversation.
3. Maps username → Locksmith **email** or uses username-as-email (`format!("{}@yourdomain", user)`).
4. Calls **`LocksmithClient::sign_in`** (async → see §6). On **Ok** → return **`PAM_SUCCESS`** so later modules may skip Unix password (`pam_unix` configured as `sufficient` below your module, or your module `required` and unix `disabled` for that service).
5. Optionally calls **`get_user`** with `access_token` to enforce **`token_has_permission`** before success (e.g. only `system:ssh`).

**Important:** If another module already verified the user (e.g. pubkey-only SSH), your module may not run password checks; design the **pam.d** snippet per service (`sshd`, `login`, etc.).

---

## 6. Async inside PAM: required pattern

PAM callbacks are **synchronous**. The Rust client is **async**.

Use a **Tokio** (or other) runtime inside the authenticate hook, for example:

```rust
// Pseudocode inside pam_sm_authenticate
use tokio::runtime::Runtime;

fn authenticate_with_locksmith(email: &str, password: &str) -> Result<SignInResult, getlocksmith::Error> {
    let api_key = std::env::var("LOCKSMITH_API_KEY") // Prefer reading from file in real code
        .map_err(|_| getlocksmith::Error::InvalidApiKey)?;
    let client = LocksmithClient::new(api_key, None)?;
    let rt = Runtime::new().map_err(|e| getlocksmith::Error::InvalidResponse(e.to_string()))?;
    rt.block_on(async { client.sign_in(email, password).await })
}
```

- Prefer **`Runtime::new()`** per auth (simple; small overhead) or a **lazy static** runtime (`once_cell`) if profiling shows it matters.
- **Do not** block the SSH main thread for minutes; set **`reqwest`** timeouts on a custom client if you extend the SDK, or accept defaults and document failover.

---

## 7. Configuration and secrets (production checklist)

| Secret / config        | Where                                                           | Notes                                                                                        |
| ---------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `LOCKSMITH_API_KEY`    | Root-only file, e.g. `/etc/security/locksmith.conf` mode `0600` | Production vs Sandbox strictly from key prefix.                                              |
| Base URL               | Same file                                                       | Default `https://getlocksmith.dev`; self-host if applicable.                                 |
| Email mapping          | Config                                                          | `username_map = email` or `suffix = "@corp.example"`                                         |
| Project public key PEM | Local file                                                      | Only if using **`verify_token`** offline. Must match environment (live vs sandbox keypair).  |
| Rate limits            | Locksmith docs                                                  | Brute-force still hits API; optionally cache failures locally (careful with timing attacks). |

Never log passwords or refresh tokens. Avoid writing access tokens to world-readable files.

---

## 8. Suggested PAM return code mapping

| Outcome                              | PAM return                             |
| ------------------------------------ | -------------------------------------- |
| `sign_in` Ok                         | `PAM_SUCCESS`                          |
| API `invalid_credentials` / 401 auth | `PAM_AUTH_ERR`                         |
| `user_banned` / 403                  | `PAM_PERM_DENIED`                      |
| `rate_limited` / 429                 | `PAM_AUTHINFO_UNAVAIL` or retry policy |
| Network / 5xx                        | `PAM_AUTHINFO_UNAVAIL`                 |
| Missing API key / misconfig          | `PAM_ABORT`                            |
| `get_user` Ok but missing permission | `PAM_PERM_DENIED`                      |

---

## 9. Optional: enforcing RBAC on login

After successful `sign_in`:

```rust
let me = client.get_user(&tokens.access_token).await?;
if !me.permissions.iter().any(|p| p == "host:ssh") {
    return Err(/* map to PAM_PERM_DENIED */);
}
```

Or decode JWT without extra RTT (must trust token age / revocation policy):

```rust
let claims = LocksmithClient::verify_token(&tokens.access_token, PEM)?;
if !getlocksmith::token_has_permission(&claims, "host:ssh") {
    return Err(/* … */);
}
```

Remember: **`token_has_role` ignores `claims.role`** — compare `claims.role` manually if you still use the legacy field.

---

## 10. Example `/etc/pam.d/sshd` sketch (illustrative only)

```
# Locksmith first; if success, skip unix password.
auth sufficient pam_locksmith.so
auth required pam_unix.so try_first_pass nullok
```

Or **replace** unix for SSH:

```
auth required pam_locksmith.so
account required pam_unix.so
session required pam_unix.so
```

Exact file paths and module name depend on your install prefix (`/usr/lib/security/pam_locksmith.so`).

---

## 11. Limitations and non-goals

- **2FA / TOTP / passkeys:** Locksmith may return `401` or different flows; a tty PAM module may need extra prompts or a different auth stack.
- **Account management** (`pam_sm_acct_mgmt`): Locksmith does not replace NSS; Linux still needs **`passwd`/`getpwnam`** entries (local users, LDAP, `nss_systemd`, etc.). This doc covers **authentication** only unless you add a **`nss_locksmith`**-style resolver (out of scope here).
- **Session** / **password** PAM hooks: `pam_sm_setcred`, `pam_sm_chauthtok` are separate; “change password” might call Locksmith’s password update API with a reset token, not PAM’s usual `chauthtok`.

---

## 12. Quick reference: minimal end-to-end login

1. PAM prompts for user + password.
2. Build email from username per config.
3. `Runtime::block_on(client.sign_in(email, password))`.
4. On success: optionally `get_user` or `verify_token` for RBAC.
5. Return `PAM_SUCCESS` so OpenSSH/login continues.
6. Do **not** store long-lived tokens in the session unless you have a clear threat model (SSH generally does not need Locksmith JWT after auth).

---

For HTTP semantics and error codes, cross-check Locksmith’s public API documentation at `https://getlocksmith.dev/docs/api`