Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/oauth-scope-authorization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/backend': minor
'@clerk/nextjs': minor
'@clerk/shared': minor
---

OAuth access-token auth objects now support `has({ oauth_scope: 'scope' })` to authorize against the exact scopes granted in the token.
26 changes: 26 additions & 0 deletions examples/oauth-scope-hono/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Clerk resource server
CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
CLERK_API_URL=http://localhost:8002
CLERK_API_VERSION=v1
RESOURCE_SERVER_HOST=127.0.0.1
RESOURCE_SERVER_PORT=8787
RESOURCE_SERVER_URL=http://127.0.0.1:8787/protected
OAUTH_REQUIRED_SCOPE=profile

# OAuth client
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
OAUTH_CLIENT_AUTH_METHOD=client_secret_post
OAUTH_REDIRECT_URI=http://127.0.0.1:8788/callback
OAUTH_SCOPE=profile email

# Set both direct endpoints, or leave them blank and use issuer discovery.
OAUTH_AUTHORIZE_URL=
OAUTH_TOKEN_URL=
OAUTH_ISSUER_URL=
OAUTH_DISCOVERY_URL=

OAUTH_OPEN_BROWSER=true
OAUTH_CALLBACK_TIMEOUT_MS=300000
OAUTH_EXPECT_JWT_ACCESS_TOKEN=true
1 change: 1 addition & 0 deletions examples/oauth-scope-hono/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
93 changes: 93 additions & 0 deletions examples/oauth-scope-hono/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# OAuth scope authorization with Hono

This example runs two local programs:

- a Hono resource server protected by Clerk's `clerkMiddleware()`;
- a PKCE OAuth client that opens Clerk's authorization page, receives the code on a loopback callback, exchanges it for an access token, and calls the protected route.

The protected route accepts only `oauth_token` and authorizes with:

```ts
auth.has({ oauth_scope: process.env.OAUTH_REQUIRED_SCOPE || 'profile' });
```

## Configure the example app

1. Create an OAuth application. Configure its redirect URI as `http://127.0.0.1:8788/callback` and allow at least `profile`.
1. Copy this instance's publishable key, secret key, and the OAuth application's client ID and client secret.
1. Copy the OAuth application's **Authorize URL** and **Token URL**.

Then install and configure the example:

```sh
# From the javascript repository root, install the SDK build dependencies first.
pnpm install --frozen-lockfile

cd examples/oauth-scope-hono
cp .env.example .env
pnpm install --frozen-lockfile
```

Fill in at least:

```dotenv
CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
OAUTH_CLIENT_ID=client_...
OAUTH_CLIENT_SECRET=...
OAUTH_AUTHORIZE_URL=https://.../oauth/authorize
OAUTH_TOKEN_URL=https://.../oauth/token
```

The example defaults `OAUTH_EXPECT_JWT_ACCESS_TOKEN=true`. New local instances default `oauth_jwt_access_tokens` to enabled; confirm that setting at `GET /v1/instance/oauth_application_settings` if the client reports an opaque token. Set the expectation to `false` only when intentionally testing opaque access tokens.

For a public client, leave `OAUTH_CLIENT_SECRET` blank and set `OAUTH_CLIENT_AUTH_METHOD=none`. PKCE is used in every mode. Confidential Clerk OAuth applications normally use `client_secret_post`.

Instead of direct endpoint URLs, you can leave `OAUTH_AUTHORIZE_URL` and `OAUTH_TOKEN_URL` blank and set `OAUTH_ISSUER_URL`. The client will use RFC 8414 authorization-server discovery. Set `OAUTH_DISCOVERY_URL` when the stack exposes metadata at a compatibility or proxy-specific URL.

## Run

Build the changed local SDK packages once:

```sh
pnpm run sdk:build
```

Start the resource server:

```sh
pnpm run server
```

In another terminal, run the OAuth client:

```sh
pnpm run client
```

The client prints the authorization URL and opens it in the default browser. Sign in and approve consent. A successful run ends with a `200` response showing the granted scopes and subject.

Useful client flags:

```sh
pnpm client -- --no-open
pnpm client -- --print-token
```

`--no-open` only prints the URL. `--print-token` prints the access token and should only be used for local debugging.

## Verify denial

- Set `OAUTH_REQUIRED_SCOPE` to a scope not present in `OAUTH_SCOPE`, restart the server, and run the client again. Expect `403`.
- Remove the bearer token or send a non-OAuth token to `RESOURCE_SERVER_URL`. Expect `401`.
- Scope matching is exact and case-sensitive.

## Contributing

After SDK changes, rerun `pnpm sdk:build`. Validate the example itself with `pnpm typecheck`.

In case local hostnames aren't resolving:

```sh
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
```
28 changes: 28 additions & 0 deletions examples/oauth-scope-hono/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@clerk/example-oauth-scope-hono",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"client": "tsx src/client.ts",
"sdk:build": "pnpm --dir ../.. --filter @clerk/hono... build",
"server": "tsx src/server.ts",
"server:watch": "tsx watch src/server.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@clerk/hono": "link:../../packages/hono",
"@hono/node-server": "1.19.17",
"dotenv": "^16.5.0",
"hono": "4.12.27"
},
"devDependencies": {
"@types/node": "^22.19.17",
"tsx": "^4.20.6",
"typescript": "^6.0.3"
},
"packageManager": "pnpm@10.33.0",
"engines": {
"node": ">=24.15.0"
}
}
Loading
Loading