# SteelERP auth.md

Audience: AI agents, assistants (Claude, ChatGPT and other MCP or A2A clients) and developers acting for a person who wants information about SteelERP or wants SteelERP to contact them.

SteelERP has three agent-facing interfaces:

| Interface | Endpoint | Authentication |
|---|---|---|
| Demo request API | `POST https://steelerp.ir/api/waitlist` | none |
| A2A agent (JSON-RPC 2.0, A2A 0.3) | `POST https://steelerp.ir/api/steelerp/a2a` | none |
| MCP server (Streamable HTTP) | `POST https://steelerp.ir/api/steelerp/mcp` | OAuth 2.1 bearer token, scope `facts` or `demo` |

## Public endpoint: no registration, no credentials

`POST https://steelerp.ir/api/waitlist` (described in [openapi.json](https://steelerp.ir/openapi.json) and the [API catalog](https://steelerp.ir/.well-known/api-catalog)) needs no account, API key or OAuth token.

- Send a request only when the user explicitly asked for a demo or the sample PDF, with their own name and phone number.
- Limit: 5 requests per client per hour (HTTP 429 after that).

The A2A agent ([agent card](https://steelerp.ir/.well-known/agent-card.json)) is public in the same way. Its `request-demo` skill applies the same validation and limit and forwards to the same API. Its other skills, `product-facts` and `estimate-disorder-cost`, only read public information.

## MCP server: discovery

The MCP server ([server card](https://steelerp.ir/.well-known/mcp/server-card.json)) is an OAuth 2.1 protected resource. A request without a valid token gets `401` with:

```
WWW-Authenticate: Bearer resource_metadata="https://steelerp.ir/.well-known/oauth-protected-resource/api/steelerp/mcp", scope="demo"
```

1. Read the [protected resource metadata](https://steelerp.ir/.well-known/oauth-protected-resource/api/steelerp/mcp) (RFC 9728): resource `https://steelerp.ir/api/steelerp/mcp`, authorization server `https://steelerp.ir`.
2. Read the [authorization server metadata](https://steelerp.ir/.well-known/oauth-authorization-server) (RFC 8414). Its `agent_auth` block lists the agent registration surface described below.

There are two ways to get a token. Both end in the same consent page, where a person enters and approves their own contact details. No assistant can file a demo request in a person's name without that step.

| Way | For | Start at |
|---|---|---|
| A. OAuth authorization code + PKCE | MCP clients that can open a browser and receive a redirect | `POST https://steelerp.ir/api/steelerp/oauth/register` (RFC 7591) |
| B. auth.md anonymous registration + claim | agents without a redirect URI (CLI, server-side, chat) | `POST https://steelerp.ir/api/steelerp/oauth/agent` |

## A. Authorization code flow (RFC 7591, RFC 6749, PKCE)

1. Register at `POST https://steelerp.ir/api/steelerp/oauth/register` (dynamic client registration). Only public clients are accepted: `token_endpoint_auth_method` is `none`. Redirect URIs must be `https`, or `http` on `localhost`/`127.0.0.1`, and they are matched exactly.
2. Send the person to `GET https://steelerp.ir/api/steelerp/oauth/authorize` with `response_type=code`, `client_id`, `redirect_uri`, `state`, `scope=demo`, PKCE `code_challenge` with `code_challenge_method=S256` (required), and optionally `resource=https://steelerp.ir/api/steelerp/mcp`.
3. Exchange the code at `POST https://steelerp.ir/api/steelerp/oauth/token` (`grant_type=authorization_code` with `client_id`, `redirect_uri` and `code_verifier`). Codes are single-use and expire after 10 minutes.

You get an access token (1 hour, scope `demo`) and a refresh token (see [Using and renewing tokens](#using-and-renewing-tokens)).

## B. Agent registration (auth.md, anonymous identity)

SteelERP supports one auth.md identity type: `anonymous`. It does not accept ID-JAG identity assertions, verified-email registration or `service_auth`, because SteelERP has no user accounts to bind them to. Those requests get `issuer_not_enabled`, `verified_email_not_enabled` or `service_auth_not_enabled`.

### B1. Register

```http
POST https://steelerp.ir/api/steelerp/oauth/agent
Content-Type: application/json

{ "type": "anonymous", "client_name": "My assistant" }
```

`client_name` is optional; the person sees it on the consent page. `requested_credential_type` may be omitted or `access_token`. Limit: 10 registrations per address per hour.

Response (200):

```json
{
  "registration_id": "...",
  "registration_type": "anonymous",
  "client_id": "...",
  "client_name": "My assistant",
  "credential_type": "access_token",
  "credential": "<access token>",
  "token_type": "Bearer",
  "credential_expires": "2026-11-02T13:00:00Z",
  "scopes": ["facts"],
  "claim_url": "https://steelerp.ir/api/steelerp/oauth/agent/claim",
  "claim_token": "clm_...",
  "claim_token_expires": "2026-11-03T12:00:00Z",
  "post_claim_scopes": ["demo"]
}
```

The `credential` works at once on the MCP server with scope `facts` (1 hour, no refresh token): `search_product_facts` and `estimate_disorder_cost`. `request_demo` is not listed and refuses until a person claims the agent. Keep `client_id` for refresh and revocation. `claim_token` is returned once and lasts 24 hours; keep it in memory only.

### B2. Claim: a person takes over the agent

Do this only when the person wants SteelERP to be able to contact them.

1. Start a claim attempt:

   ```http
   POST https://steelerp.ir/api/steelerp/oauth/agent/claim
   Content-Type: application/json

   { "claim_token": "clm_..." }
   ```

   Response: `{"registration_id", "status": "initiated", "expires_at", "claim_attempt": {"user_code": "123456", "verification_uri": "https://steelerp.ir/api/steelerp/oauth/agent/verify?attempt=...", "expires_in": 600, "interval": 5}}`. No email is needed: SteelERP has no accounts and sends no email.

2. Give the person `verification_uri` and `user_code` in one message, for example: «این پیوند را باز کنید و کد ۱۲۳۴۵۶ را در آن وارد کنید». The code goes into the page, not back to you.
3. On that page (Persian) the person sees your `client_name`, types the code, their full name, mobile number and (optionally) company, then approves or denies. Five wrong codes end the attempt.
4. Poll the token endpoint every `interval` seconds:

   ```http
   POST https://steelerp.ir/api/steelerp/oauth/token
   Content-Type: application/x-www-form-urlencoded

   grant_type=urn:workos:agent-auth:grant-type:claim&claim_token=clm_...
   ```

   - `authorization_pending`: the person has not finished yet; keep polling.
   - `slow_down`: you polled faster than `interval`; wait longer.
   - `expired_token`: no live attempt (the 10-minute code expired, or you did not start one). Call step 1 again with the same `claim_token` for a new code.
   - `access_denied`: the person denied. The claim is over; the `facts` credential still works until it expires.
   - `invalid_grant`: unknown, used or revoked `claim_token`. Register again.
   - Success: a standard token response with scope `demo`, plus `client_id`. The claim token is used up.

The claim also ends the pre-claim `facts` credential: use the new access token.

Errors at `/oauth/agent/claim`: `invalid_claim_token` (wrong token, or its registration was revoked), `claimed_or_in_flight` (already claimed or denied), `claim_expired` (HTTP 410, the 24 hours have passed: register again).

## Using and renewing tokens

- Send `Authorization: Bearer <access_token>` to `POST https://steelerp.ir/api/steelerp/mcp`.
- Access tokens last 1 hour. `demo` grants (ways A and B after the claim) also get a refresh token that lasts 30 days and is rotated on every use: `POST https://steelerp.ir/api/steelerp/oauth/token` with `grant_type=refresh_token`, `refresh_token` and `client_id`. Presenting an old refresh token again revokes the grant.
- A `facts` credential has no refresh token. When it expires, register again.
- On `401` with a token that worked before, refresh once; if that fails, start again.

## Revocation

`POST https://steelerp.ir/api/steelerp/oauth/revoke` (RFC 7009, form-encoded) with `token` (access or refresh token) and your `client_id`. Revoking either token ends the whole grant, including an unfinished claim. The answer is `200` whether or not the token was valid. The person can also ask you to disconnect; do it with this call.

SteelERP has no provider-pushed revocation (`revocation_uri`, Security Event Tokens), because it accepts no ID-JAGs.

## Consent: a person stays in the loop

Both consent pages show the agent's name and what it may do. The person types their own full name, mobile number and (optionally) company, then approves or denies. In way A, denying returns `error=access_denied` to the client.

An assistant can file a demo request only after a person has entered and approved their contact details on that page. The `request_demo` tool always uses those approved details. The assistant cannot change them. SteelERP does not verify the phone number on the page; the sales team calls it.

## Scopes

- `facts` (anonymous agents before a claim): `search_product_facts` searches the official product facts ([llms-full.txt](https://steelerp.ir/llms-full.txt)); `estimate_disorder_cost` runs the calculator formula of steelerp.ir.
- `demo` (after a person approves): the two tools above, plus `request_demo`: one request (`want`: `demo` or `sample`, optional `note`) to the sales team in the person's name, at most twice a day per grant.

Neither scope gives access to any SteelERP customer system or data.

## The SteelERP product itself

Cloud and on-premise installs of SteelERP use per-company user accounts created by the customer's administrator. Those accounts are not available for self-service agent registration.

## Contact

Questions about integration: info@fanpino.com · https://steelerp.ir/contact/
