# auth.md This file describes the ways an agent can get access to Judt for its user. Judt is a platform for building full stack web apps via the workerd (not Cloudflare) runtime. An App is a web app or a site and can have databases, queues, Durable Objects and more. ## What you can do With access, you can make, change, preview, and publish the Apps of your user through the Judt HTTP API, MCP server, or CLI. You can start the sign-in yourself, without a key or a registered client. Your user approves your access, and there is no access without this approval. ## Ways to get access | What the agent has | Way to get access | What the agent gets | What the person does | |---|---|---|---| | MCP client with OAuth | Add `https://api.judt.ai/mcp` as a server, with no `Authorization` header. The client does the sign-in. | Scoped token, kept by the client | Signs in and approves | | A shell | The Judt CLI: `judt auth login --device`. | Scoped token, saved and refreshed by the CLI | Opens a link and approves | | HTTP requests only | [Registration with an email address](https://judt.ai/auth.md#registration-with-an-email-address). | Scoped token, stored and renewed by the agent | Opens a link, types a code, and approves | | Only a browser | The form at https://judt.ai/signup, or the WebMCP tool `sign_up`. | The full session of the person, in that browser only | Follows the emailed link, or gives the agent the emailed code | The order of the rows is Judt's order of preference. The first row that an agent's tools allow leaves that agent the least to do. The first three rows give a token for one organization, with the scopes that the person allows. The person can disconnect it at https://judt.ai/connections. The person does not need an account first: the sign-in page of these rows also makes one. This command installs the CLI, whose source is at https://github.com/judt-ai/cli: ```sh curl -fsSL https://judt.ai/install | sh ``` `judt auth login --device` prints a link and a code to stderr at once, then waits until the person approves, so it can run in the background. A person can also make an API key at https://judt.ai/api-keys for scripts and CI. Personal Agents that use the Personal Agent Protocol start at https://judt.ai/.well-known/poppy.json. ## Registration with an email address This way needs only the email address of your user. The registration has these steps, in this sequence: 1. Registration. 2. Approval: your user gets a link from you, signs in to Judt, and approves your access. 3. Tokens. 4. Requests to the API and the MCP server. 5. New access tokens. This file uses these hosts: - `https://judt.ai`: the authorization server. Your user signs in here. - `https://api.judt.ai`: the HTTP API. It accepts your access token. - `https://api.judt.ai/mcp`: the MCP server. It accepts your access token. The server metadata (see [Discovery](#discovery)) is the source of truth. If the metadata and this file are different, the metadata is correct. ### Step 1: Registration Registration needs the email address of the Judt account of your user. If your user does not have an account, it is the email address that they want to use for Judt. ```http POST https://judt.ai/api/auth/agent/identity Content-Type: application/json { "type": "service_auth", "login_hint": "USER_EMAIL" } ``` `USER_EMAIL` is the email address of your user. The response is similar to this example: ```json { "registration_id": "reg_...", "registration_type": "service_auth", "claim_url": "https://judt.ai/api/auth/agent/identity/claim", "claim_token": "clm_...", "claim_token_expires": "2026-10-28T16:00:00.000Z", "post_claim_scopes": ["apps:read", "apps:write"], "claim": { "user_code": "WDJBMJHT", "verification_uri": "https://judt.ai/device", "verification_uri_complete": "https://judt.ai/device?user_code=WDJBMJHT", "expires_in": 600, "interval": 5 } } ``` The `claim_token` is a secret for you only, and you need it until step 3 is complete. The server sends it only one time. Judt has these scopes: | Scope | Permission | |---|---| | `apps:read` | See the Apps of your user, with their files, deployments, and logs. | | `apps:write` | Change the Apps of your user: edit files, run commands, deploy, and manage secrets. | If the request is not correct, the server sends `400 Bad Request` with `{ "error": "...", "message": "..." }`: | Error | Meaning | |---|---| | `invalid_login_hint` | `login_hint` is not an email address. | | `anonymous_not_enabled` | Judt does not accept the `anonymous` type. It accepts `service_auth`. | | `issuer_not_enabled` | Judt does not accept the `identity_assertion` type. It accepts `service_auth`. | ### Step 2: Approval Your user approves your access with `claim.verification_uri_complete` and `claim.user_code`. One message from you can give them both. For example: > To connect me to Judt, open https://judt.ai/device?user_code=WDJBMJHT. Make sure that the page shows the code **WDJBMJHT**. On the Judt site, your user: 1. Opens the link. The link fills in the code. 2. Signs in as the email address from step 1. If your user is not signed in, Judt sends a 6-digit code to that address, and your user types it on the page. If your user does not have a Judt account, the same code makes the account. Judt makes an organization for each new account. 3. Makes sure that the page shows the same code as your message. 4. Selects an organization, if they are a member of more than one. Your access is to this organization only. 5. Selects the scopes to allow, and approves. Only the account with the email address from step 1 can approve the code. The code expires after `expires_in` seconds (10 minutes). Judt sends the 6-digit code only when your user asks for it on the page. The 6-digit code is for the Judt site only, so this flow does not need it from your user. If your user opens `claim.verification_uri` without the code, the page asks for `claim.user_code`. ### Step 3: Tokens The token endpoint gives your tokens after your user approves. You can send this request after each `interval` seconds: ```http POST https://judt.ai/api/auth/oauth2/token Content-Type: application/x-www-form-urlencoded grant_type=urn:workos:agent-auth:grant-type:claim &claim_token=CLAIM_TOKEN &resource=https://api.judt.ai &resource=https://api.judt.ai/mcp ``` `CLAIM_TOKEN` is the `claim_token` from step 1. Each server accepts only a token for that server, and there is one `resource` parameter for each server. With the two `resource` parameters in the example, the token works on the API and on the MCP server. Without a `resource` parameter, the token works only on the API. Before your user approves, the server sends `400 Bad Request` with one of these errors: | Error | Meaning | What you can do | |---|---|---| | `authorization_pending` | Your user did not approve yet. | Send the request again after `interval` seconds. | | `slow_down` | The requests were too frequent. | Send the request again with 5 seconds added to `interval`. | | `access_denied` | Your user refused your access. | This registration cannot give access. You can tell your user the result. | | `expired_token` | The code or the registration expired. | Get a new code (below). If that fails, a new registration (step 1) starts again. | A new code comes from the claim endpoint, with the `claim_token` and the email address from step 1: ```http POST https://judt.ai/api/auth/agent/identity/claim Content-Type: application/json { "claim_token": "CLAIM_TOKEN", "email": "USER_EMAIL" } ``` The response has a `claim_attempt` object with a new `user_code` and `verification_uri_complete`. Your user approves with the new link (step 2), and the request of step 3 can then succeed. If the server sends `410 Gone` with `claim_expired`, the registration expired after 24 hours, and a new registration (step 1) starts again. When your user approves, the response is similar to this example: ```json { "access_token": "...", "token_type": "Bearer", "expires_in": 3600, "scope": "apps:read apps:write", "identity_assertion": "...", "assertion_expires": "2026-11-26T16:00:00.000Z" } ``` The `identity_assertion` is a secret. It gets you new access tokens in step 5. The `claim_token` is now used, so you no longer need it. Your user can allow fewer scopes than you requested. The `scope` field shows the scopes that you have. ### Step 4: Requests The access token goes in each request as a bearer token. For example, this request lists the Apps with the API: ```http GET https://api.judt.ai/api/apps Authorization: Bearer ACCESS_TOKEN ``` The OpenAPI description of the API is at https://api.judt.ai/api/openapi.json. This request lists the tools of the MCP server: ```http POST https://api.judt.ai/mcp Authorization: Bearer ACCESS_TOKEN Content-Type: application/json Accept: application/json, text/event-stream { "jsonrpc": "2.0", "id": 1, "method": "tools/list" } ``` `ACCESS_TOKEN` is the `access_token` from step 3. If the token is expired or revoked, the server sends `401 Unauthorized` with `error="invalid_token"`. Step 5 gets a new access token. If a request needs a scope that you do not have, the API sends `403 Forbidden` with the error code `insufficient_scope`, and the tool call fails with the same error code. A new scope needs a new approval from your user, through a new registration (step 1). ### Step 5: New access tokens The access token is valid for one hour. Before it expires, or after a `401`, the identity assertion gets you a new one from the token endpoint: ```http POST https://judt.ai/api/auth/oauth2/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer &assertion=IDENTITY_ASSERTION &resource=https://api.judt.ai &resource=https://api.judt.ai/mcp ``` `IDENTITY_ASSERTION` is the `identity_assertion` from step 3. You can use the same identity assertion again for each new access token. It is valid until `assertion_expires` (30 days). Using it does not make it valid for longer. If the server sends `invalid_grant`, your user disconnected you or the identity assertion expired. A new registration (step 1) starts again. ### Discovery The steps above name each endpoint, and the metadata gives the same endpoints. A request to the MCP server or the API without a token gets `401 Unauthorized`. The `WWW-Authenticate` header gives the URL of the protected resource metadata. For example, from the MCP server: ```http HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer resource_metadata="https://api.judt.ai/.well-known/oauth-protected-resource/mcp", scope="apps:read apps:write" ``` The protected resource metadata ([RFC 9728](https://www.rfc-editor.org/rfc/rfc9728)) names the authorization server. The metadata of the API is at `https://api.judt.ai/.well-known/oauth-protected-resource`. ```http GET https://api.judt.ai/.well-known/oauth-protected-resource/mcp ``` ```json { "resource": "https://api.judt.ai/mcp", "authorization_servers": ["https://judt.ai"], "scopes_supported": ["apps:read", "apps:write"], "bearer_methods_supported": ["header"], "resource_name": "Judt" } ``` The authorization server metadata ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) is at the first URL in `authorization_servers`: ```http GET https://judt.ai/.well-known/oauth-authorization-server ``` These metadata fields apply to this flow: - `agent_auth.identity_endpoint`: registers you (step 1). - `agent_auth.claim_endpoint`: gives a new code, if your code expires (step 3). - `token_endpoint`: gives your tokens (steps 3 and 5). - `revocation_endpoint`: stops your access. Judt accepts only the `service_auth` registration type (`agent_auth.identity_types_supported`). ## Stopping access Your user can disconnect you at any time, from **Connections** at https://judt.ai/connections. After this, your tokens and your identity assertion do not work. You can also stop your access yourself, by revoking the identity assertion: ```http POST https://judt.ai/api/auth/oauth2/revoke Content-Type: application/x-www-form-urlencoded token=IDENTITY_ASSERTION ``` After this, your access tokens also stop working. ## Sign-up in a browser If you control a browser for your user, you can fill in the sign-up form at `https://judt.ai/signup` for them. The form has two fields: name and work email. Judt sends a link and a 6-digit code to that email address. The account exists only after your user follows the link, or gives you the code to enter on the page, which also signs in your browser. The link and the code stay with your user until they choose to use them. The result is the session of your user in that browser, with all of their permissions. It is not a token for the API or the MCP server. ## Other way: OAuth device flow The authorization server metadata also has `registration_endpoint` and `device_authorization_endpoint`, for agents that already use dynamic client registration ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) and the device authorization grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)) as a public client. The scope is `apps:read apps:write offline_access`, with one `resource` parameter for each server. The [authentication page of the documentation](https://docs.judt.ai/manage/authentication#oauth-device-flow) describes the requests. The [Judt documentation](https://docs.judt.ai) has more information about Judt.