# OAuth app

> Build an app that users authorize with their own accounts — client registration, authorization with PKCE, tokens, and refresh.

Every Solk installation runs an **OAuth 2.1 authorization server**. The same server issues tokens to MCP clients (Claude) and to apps that call the REST API on behalf of users. The supported flows are **authorization code + PKCE (S256)** and **refresh token**; the password flow and the client credentials flow are not supported.

| | URL |
|---|---|
| Discovery | `https://ornek.solk.app/.well-known/oauth-authorization-server` |
| Client registration | `POST https://ornek.solk.app/oauth/register` |
| Authorization | `GET https://ornek.solk.app/oauth/authorize` |
| Token | `POST https://ornek.solk.app/oauth/token` |
| Revocation | `POST https://ornek.solk.app/oauth/revoke` |

Each installation has its own authorization server: get the user's installation address (for example, `ornek.solk.app`) from the user and read the discovery document from that address.

:::steps
### Register the client

Register your app once ([Dynamic client registration](/rest-api/oauth/register), RFC 7591). Server-side apps that can keep a secret choose `client_secret_post`; desktop and mobile apps use `none` (public client).

```bash
curl https://ornek.solk.app/oauth/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Örnek Entegrasyon",
    "redirect_uris": ["https://uygulamaniz.com/oauth/callback"],
    "token_endpoint_auth_method": "none"
  }'
```

```json
{
  "client_id": "crm_Jq8w…",
  "client_id_issued_at": 1790939471,
  "client_name": "Örnek Entegrasyon",
  "redirect_uris": ["https://uygulamaniz.com/oauth/callback"],
  "token_endpoint_auth_method": "none",
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"]
}
```

Redirect URIs must use `https://` or a loopback address (`http://localhost`, `http://127.0.0.1`, `http://[::1]`). For loopback addresses, the port number is ignored during matching. Instead of client registration, you can also use **CIMD**: pass the `https://` URL of your client metadata document as the `client_id`.

### Generate PKCE values

For each authorization, generate a random `code_verifier` (43–128 characters) and a `code_challenge`, which is the base64url-encoded SHA-256 hash of the verifier:

```python
import secrets, hashlib, base64
verifier = secrets.token_urlsafe(48)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).decode().rstrip("=")
```

### Redirect the user

```text
https://ornek.solk.app/oauth/authorize
  ?response_type=code
  &client_id=crm_Jq8w…
  &redirect_uri=https%3A%2F%2Fuygulamaniz.com%2Foauth%2Fcallback
  &scope=crm.read%20crm.write
  &state=xyz123
  &code_challenge=E9Melhoa2Owv…
  &code_challenge_method=S256
```

If the user isn't signed in, the sign-in screen appears first. The consent screen shows your app's name, the domain of the redirect URI, and the requested scopes. If the user clicks **Allow** (*İzin ver*), the browser returns to:

```text
https://uygulamaniz.com/oauth/callback?code=Zx8…&state=xyz123
```

If the user declines, you get `?error=access_denied&state=xyz123`. Verify that the `state` value matches the one you sent.

### Exchange the code for tokens

The code is valid for 5 minutes and can be used **once**. The token endpoint expects a form body:

```bash
curl https://ornek.solk.app/oauth/token \
  -d grant_type=authorization_code \
  -d code=Zx8… \
  -d redirect_uri=https://uygulamaniz.com/oauth/callback \
  -d client_id=crm_Jq8w… \
  -d code_verifier=$VERIFIER
```

```json
{
  "access_token": "mcp_uWrPBo…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "mcpr_Nu5ue…",
  "scope": "crm.read crm.write"
}
```

### Call the API

```bash
curl https://ornek.solk.app/api/v1/me -H "Authorization: Bearer mcp_uWrPBo…"
```

The access token is valid for the REST API and the [MCP](/mcp/overview) endpoint. Without the `crm.write` scope, write requests return `403 insufficient_scope`.

### Refresh the token

The access token expires after 1 hour. Use the refresh token to get a new pair — every refresh returns a **new** refresh token and invalidates the old one:

```bash
curl https://ornek.solk.app/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=mcpr_Nu5ue… \
  -d client_id=crm_Jq8w…
```

Retrying with an old refresh token returns `400 invalid_grant`. A refresh token expires if it goes unused for 60 days; the user must then grant access again.
:::

## Grants and revocation

- When a user authorizes the same app a second time, the existing grant is updated (its scope is replaced by the scope of the new consent); each app has a single grant per user.
- In **Settings → App connections** (*Ayarlar → Uygulama bağlantıları*), users see your app, its last-used time, and its call count; **Remove access** (*Erişimi kaldır*) immediately revokes every token issued under that grant.
- If the user is deactivated, their tokens stop working.
- Your app can revoke a token itself by calling [`POST /oauth/revoke`](/rest-api/oauth/revoke).

## Errors

| Situation | Response |
|---|---|
| The code expired, was already used, belongs to a different client, or the `redirect_uri` doesn't match | `400 {"error": "invalid_grant"}` |
| PKCE verification failed | `400 {"error": "invalid_grant", "error_description": "PKCE doğrulanamadı."}` |
| The same code was used a second time | `400 invalid_grant` **and** every token issued under that grant is revoked |
| Unsupported grant type | `400 {"error": "unsupported_grant_type"}` |
| Too many requests | `429 {"error": "slow_down"}` |