# 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
| 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”)
| `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)
| `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)
| `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
| `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?;
}
```
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`