Skip to main content

OAuth Authorization Server

Willow exposes a full OAuth 2.0 / OpenID Connect authorization server that MCP clients use to authenticate users. This page documents every endpoint, the discovery metadata, and the end-to-end flow.

All OAuth endpoints live on the Connect host for your organization:

https://<org>.connect.your-domain.com

Discovery

Before calling any endpoint, MCP clients can discover the server configuration through standard metadata documents.

GET /.well-known/oauth-authorization-server

Returns RFC 8414 OAuth Authorization Server Metadata, which is used by MCP clients implementing the MCP OAuth spec.

curl https://<org>.connect.your-domain.com/.well-known/oauth-authorization-server
{
"issuer": "https://<org>.connect.your-domain.com",
"authorization_endpoint": "https://<org>.connect.your-domain.com/authorize",
"token_endpoint": "https://<org>.connect.your-domain.com/token",
"registration_endpoint": "https://<org>.connect.your-domain.com/register",
"revocation_endpoint": "https://<org>.connect.your-domain.com/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["openid", "email", "profile"]
}

GET /.well-known/openid-configuration

Returns OpenID Connect Discovery metadata. Identical to the OAuth metadata above, with the addition of the userinfo_endpoint.

curl https://<org>.connect.your-domain.com/.well-known/openid-configuration
{
"issuer": "https://<org>.connect.your-domain.com",
"authorization_endpoint": "https://<org>.connect.your-domain.com/authorize",
"token_endpoint": "https://<org>.connect.your-domain.com/token",
"userinfo_endpoint": "https://<org>.connect.your-domain.com/userinfo",
"registration_endpoint": "https://<org>.connect.your-domain.com/register",
"revocation_endpoint": "https://<org>.connect.your-domain.com/revoke",
"scopes_supported": ["openid", "email", "profile"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"code_challenge_methods_supported": ["S256"],
"subject_types_supported": ["public"]
}

GET /.well-known/oauth-protected-resource

Returns RFC 9728 Protected Resource Metadata. MCP clients use this to identify which authorization server protects a given resource.

curl https://<org>.connect.your-domain.com/.well-known/oauth-protected-resource
{
"resource": "https://<org>.connect.your-domain.com",
"authorization_servers": ["https://<org>.connect.your-domain.com"],
"scopes_supported": ["openid", "email", "profile"],
"resource_name": "MCP-S",
"resource_documentation": "https://<org>.connect.your-domain.com"
}

Endpoints

1. Dynamic Client Registration

POST /register

MCP clients register themselves before starting the authorization flow. Willow implements RFC 7591 Dynamic Client Registration.

Request Body

{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:3000/callback"]
}
FieldTypeRequiredDescription
client_namestringNoHuman-readable name for the client
redirect_urisstring[]YesAllowed redirect URIs after authorization
client_idstringNoCustom client ID (auto-generated if omitted)
client_secretstringNoCustom client secret (auto-generated if omitted)

Response — 200 OK

{
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"client_secret": "s3cr3t-v4lu3-h3r3",
"redirect_uris": ["http://localhost:3000/callback"],
"grant_types": ["authorization_code"],
"client_name": "My MCP Client"
}

2. Authorization

GET /authorize

Starts the authorization code flow. The MCP client redirects the user's browser to this endpoint.

Query Parameters

ParameterRequiredDescription
client_idYesThe registered client ID
redirect_uriYesMust match one of the client's registered redirect URIs
stateNoOpaque value for CSRF protection, returned unchanged in the callback
code_challengeNoPKCE code challenge (recommended, S256 method)
nonceNoValue included in the id_token for replay protection

Flow

  1. Willow validates the client_id and redirect_uri against the registered client.
  2. The user is redirected to the organization's sign-in page (SSO, default auth, etc.).
  3. If the client is not a recognized MCP client in the organization, a consent screen is shown.
  4. After successful authentication, the user is redirected back to redirect_uri with a code and state:
http://localhost:3000/callback?code=abc123...&state=xyz

Errors

If redirect_uri is missing or doesn't match, the user is shown an error page instead of being redirected.


3. Token Exchange

POST /token

Exchanges an authorization code for tokens, or refreshes an expired access token. The request body must be sent as application/x-www-form-urlencoded.

Grant Type: authorization_code

FieldRequiredDescription
grant_typeYesMust be authorization_code
client_idYesThe registered client ID (can also be sent via Basic auth header)
codeYesThe authorization code from the /authorize callback
redirect_uriYesMust match the URI used in the authorization request
code_verifierConditionalRequired if code_challenge was used during authorization (PKCE)
curl -X POST https://<org>.connect.your-domain.com/token \
-d "grant_type=authorization_code" \
-d "client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-d "code=abc123..." \
-d "redirect_uri=http://localhost:3000/callback" \
-d "code_verifier=your-pkce-verifier"

Grant Type: refresh_token

FieldRequiredDescription
grant_typeYesMust be refresh_token
client_idYesThe registered client ID
refresh_tokenYesThe refresh token from a previous token response
curl -X POST https://<org>.connect.your-domain.com/token \
-d "grant_type=refresh_token" \
-d "client_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890" \
-d "refresh_token=def456..."

Response — 200 OK

{
"access_token": "a1b2c3d4e5f6...",
"token_type": "Bearer",
"access_token_expired_at": 1719878400000,
"scope": "openid email profile",
"refresh_token": "f6e5d4c3b2a1...",
"expires_in": 86400000,
"id_token": "eyJhbGciOiJIUzI1NiIs..."
}
FieldDescription
access_tokenBearer token for authenticating API and MCP requests
token_typeAlways Bearer
access_token_expired_atUnix timestamp (ms) when the token expires
scopeGranted scopes
refresh_tokenUse to obtain a new access token when the current one expires
expires_inToken lifetime in milliseconds
id_tokenJWT containing the nonce claim (only present if nonce was provided during authorization)
Token Expiration

The default token lifetime is 24 hours. Organizations can configure a custom expiration in Admin Settings → Security.

Errors

StatusErrorDescription
400invalid_requestMissing required parameters
400invalid_grantInvalid authorization code or refresh token
400invalid_clientUnknown client ID

4. UserInfo

GET /userinfo
POST /userinfo

Returns identity claims about the authenticated user, following the OpenID Connect UserInfo specification.

Authentication

Include the access token from the /token endpoint as a Bearer token:

curl -H "Authorization: Bearer <access_token>" \
https://<org>.connect.your-domain.com/userinfo

Response — 200 OK

{
"sub": "user-id-123",
"email": "[email protected]",
"name": "John Doe"
}

Claims by Scope

ScopeClaims
openidsub (user ID)
emailemail
profilename

Errors — 401 Unauthorized

{
"error": "invalid_token",
"error_description": "Access token has expired"
}
ScenarioError Description
No Authorization headerMissing access token
Token not foundToken not found
Token expiredAccess token has expired
User not foundUser not found

5. Token Revocation

POST /revoke

Revokes an access token, following RFC 7009. The request body must be sent as application/x-www-form-urlencoded.

curl -X POST https://<org>.connect.your-domain.com/revoke \
-d "token=a1b2c3d4e5f6..."
FieldRequiredDescription
tokenYesThe access token to revoke
token_type_hintNoHint about the token type (e.g. access_token)

Response

  • 200 OK — Token revoked successfully (also returned if the token was already invalid, per RFC 7009).
  • 400 Bad Request — Missing token parameter.

Full Authorization Flow

Here's the complete flow an MCP client follows to authenticate a user:

  1. Register — The client registers itself via POST /register to get a client_id.
  2. Authorize — The client redirects the user to /authorize with PKCE. The user signs in through their organization's auth provider.
  3. Token — The client exchanges the authorization code for an access token via POST /token.
  4. UserInfo — The client calls /userinfo to identify the user.
  5. Refresh — When the access token expires, the client uses the refresh token to get a new one.
  6. Revoke — When the user signs out, the client revokes the token via POST /revoke.

Supported Scopes

ScopeDescription
openidRequired for OpenID Connect. Returns the sub claim.
emailReturns the user's email address.
profileReturns the user's display name.

Security Considerations

  • PKCE is recommended — Use code_challenge (S256) during authorization and code_verifier during token exchange to prevent authorization code interception.
  • Localhost redirect URIs — Following RFC 8252 §7.3, loopback redirects match on scheme, host, and path only — any port is allowed.
  • Known-client enforcement — Organizations can enable "Allow only authenticated clients" in security settings. When enabled, only MCP clients explicitly configured in the organization are allowed; unrecognized clients are rejected (localhost is always exempt).
  • Token revocation is idempotent — Revoking an already-revoked or unknown token returns 200 OK to prevent token scanning.