# Sign in with OAuth by hand

> Discover the server, register a client, send a person through the Connect page with PKCE and trade the code for tokens, one curl request at a time.

Source: https://zorlya.com/guides/developers/sign-in-with-oauth-by-hand

Updated 2026-10-25.

To get a Zorlya access token by hand, discover the server, register a client, send a person through the Connect page with a PKCE challenge, and trade the code at the token endpoint. This guide runs each step with curl, so you see what every client does for you.

You need `curl`, `openssl`, a browser and an approved Zorlya account. A job seeker account needs the Pro plan, because AI client access is part of Pro.

Two hosts are involved. `https://mcp.zorlya.com` is the MCP server. `https://api.zorlya.com` is the authorization server (the issuer) for every Zorlya resource, and it serves the Connect page and the token endpoint.

**Only AI apps on Zorlya's list can connect.** The list holds Claude, ChatGPT, Claude Code and Cursor, each with the exact return addresses its vendor uses. Registration is refused for any other return address. A program of your own that is not on the list cannot get a client. The loopback address used below, `http://127.0.0.1:8976/callback`, is one the list allows for Claude Code (any port, the path `/callback`), so the registration works, and the Connect page shows the name **Claude Code**, because the name comes from the list and not from what you send. Use this only to learn the flow with your own account.

## 1. Discover the server

A client given only the MCP address finds everything else from two documents. Fetch both:

```bash
curl -s https://mcp.zorlya.com/.well-known/oauth-protected-resource
curl -s https://api.zorlya.com/.well-known/oauth-authorization-server
```

The first names the resource (`https://mcp.zorlya.com`) and its authorization server (`https://api.zorlya.com`). The second lists the endpoints:

| Field | What it holds |
| --- | --- |
| `authorization_endpoint` | Where you send the person: `/oauth/authorize` |
| `token_endpoint` | Where you trade a code or a refresh token: `/oauth/token` |
| `registration_endpoint` | Where a new client registers: `/oauth/register` |
| `revocation_endpoint` | Where you end a grant: `/oauth/revoke` |
| `scopes_supported` | `zorlya:read`, `zorlya:write` and `zorlya:delete` |
| `authorization_response_iss_parameter_supported` | `true`: the redirect back to you carries `iss=https://api.zorlya.com` |
| `code_challenge_methods_supported` | `S256` only |
| `token_endpoint_auth_methods_supported` | `none`: every client is public and PKCE is the proof |

You can also reach the first document from a refusal. A `POST` to `https://mcp.zorlya.com` with no token answers `401` with a `WWW-Authenticate` header that carries `resource_metadata=` pointing at it, and a `scope`. The same document is also served at `/.well-known/oauth-protected-resource/mcp`, for clients that derive that path from a resource written with `/mcp`. `api.zorlya.com` serves neither, and `mcp.zorlya.com` does not serve the authorization server document.

## 2. Register a client

Registration follows the dynamic client registration standard (RFC 7591), and Zorlya limits it to the list of AI apps. It grants nothing by itself.

```bash
curl -s -X POST https://api.zorlya.com/oauth/register \
  -H 'content-type: application/json' \
  -d '{
    "client_name": "My first client",
    "redirect_uris": ["http://127.0.0.1:8976/callback"],
    "token_endpoint_auth_method": "none",
    "scope": "zorlya:read"
  }'
```

The answer is `201` and includes a `client_id`. Keep it. Rules to know:

- **Redirect addresses** must all belong to one enabled entry on the list. Anything else, including a custom scheme like `myapp://callback`, plain `http` to a real host or a mix of two apps' addresses, is refused with `400 invalid_redirect_uri`.
- **The name** you send is ignored. The client is stored under the list entry's name and publisher.
- **Scope** is what the client may ask for later. Register `zorlya:read zorlya:write` if you will use the write tools.
- **Client authentication** is `none`. The other methods in the registration standard are accepted at registration, but every client is treated as public.

## 3. Make a PKCE pair

PKCE ties the code you receive to the client that asked for it. Make a random verifier, then its challenge:

```bash
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
```

The challenge is the SHA-256 of the verifier, base64url encoded with no padding. The server accepts only the `S256` method, so `plain` is refused.

## 4. Send the person to the Connect page

Build the address and open it in a browser. The `resource` parameter (RFC 8707) names the server the token is for. Leave it out and you get the MCP server. The only other resource Zorlya defines, `https://api.zorlya.com`, is switched off and answers `invalid_target`.

```bash
CLIENT_ID=paste-your-client-id
echo "https://api.zorlya.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A8976%2Fcallback&code_challenge=$CHALLENGE&code_challenge_method=S256&scope=zorlya%3Aread&resource=https%3A%2F%2Fmcp.zorlya.com&state=any-random-text"
```

The Connect page opens. It names the client and its publisher, lists the Zorlya accounts you can use (job seeker, hiring, support, staff) and offers the permission levels, with Read only selected. Choose an account and a level. The browser then goes to that account's own sign-in page, which shows what you are approving. Sign in with your password or a passkey. Google and GitHub also work for Read only. If you chose a level that changes data, Zorlya asks for the password or a passkey even when you are already signed in. The request waits for 10 minutes, works once and is tied to the browser that started it, so use one browser and allow cookies. You also get an email about the connection.

The browser then goes to your redirect address with three values in the query: `code`, your `state` and `iss`. Nothing is listening on port 8976, so the browser shows a connection error. That is expected. Copy `code` from the address bar. Check that `state` is the text you sent, which is how a real client knows the answer belongs to its request, and that `iss` is `https://api.zorlya.com`, which is how it knows the answer came from the server it asked.

If the person cancels, the redirect carries `error=access_denied` instead of a code. A bad scope, a missing `code_challenge` or a `resource` Zorlya does not serve reaches you the same way, as `error` and `error_description` in the query of your redirect address, with your `state` and `iss`. An unknown `client_id` or a `redirect_uri` that does not match exactly is different: the server shows an error page on `api.zorlya.com` and does not redirect, because it cannot trust the address.

## 5. Trade the code for tokens

The code works once and lasts 60 seconds, so do this now:

```bash
curl -s -X POST https://api.zorlya.com/oauth/token \
  -d grant_type=authorization_code \
  -d "code=PASTE_THE_CODE" \
  -d "client_id=$CLIENT_ID" \
  --data-urlencode "redirect_uri=http://127.0.0.1:8976/callback" \
  --data-urlencode "code_verifier=$VERIFIER" \
  --data-urlencode "resource=https://mcp.zorlya.com"
```

The `resource` must be the one the code was issued for. The server keeps the access token's audience fixed to it.

The answer:

```json
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "zorlya:read",
  "refresh_token": "…"
}
```

The access token lasts one hour and the refresh token 30 days. Send the access token on every call to `https://mcp.zorlya.com` as `Authorization: Bearer …`. A token for another audience is refused with `401`. [Make your first calls](/guides/developers/make-your-first-calls) does that next.

## When a step fails

| You see | Cause |
| --- | --- |
| `400 invalid_redirect_uri` at registration | The address does not belong to an AI app on Zorlya's list |
| An error page at authorize, no redirect | The `client_id` is unknown or the `redirect_uri` differs from the registered one |
| `error=invalid_target` on your redirect address | The `resource` is not `https://mcp.zorlya.com` |
| "That request could not be verified" on the Connect page | Cookies are blocked, or the page was opened in another browser than the one that started it. Start again |
| `error=invalid_request` on your redirect address | A missing `code_challenge`, or a `code_challenge_method` other than `S256`. The error comes back in the query, not as a page |
| `invalid_grant`, "incorrect code_verifier" | The verifier is not the one that made the challenge |
| `invalid_grant`, "has already been used" | The code was traded twice. The second attempt also ends the tokens the first one issued |
| `invalid_grant`, "has expired" | More than 60 seconds passed between the redirect and the exchange |
| `invalid_request`, "redirect_uri did not match" | The exchange sent a different `redirect_uri` from the authorize step |

## What we checked

The first version of this guide was run against a local copy of the server on 2026-10-17. On 2026-10-18 it was rewritten for the new hosts, the list of AI apps and the Connect page from the server's source and its tests. The commands were not run again after the rewrite and none was run against the production server.
