OAuth 2.0 for the GlassFrog API
How a client gets a person's approval to call the GlassFrog API v5, including the MCP endpoint, with OAuth 2.0. For the endpoints themselves, see the API v5 reference.
Overview
GlassFrog is an OAuth 2.0 authorization server for its own API. It follows the OAuth 2.1 profile: the authorization-code grant with PKCE (S256 only), public clients registered dynamically, short-lived access tokens and rotating refresh tokens. There are no implicit, password or client-credentials grants.
- Discover the authorization server from the metadata documents.
- Register the client once and keep its client ID.
- Send the person to the authorization endpoint with a PKCE code challenge. They sign in, choose an organization and choose what the client may access.
- Exchange the returned code and the code verifier for an access token and a refresh token.
- Call the API with the access token as a Bearer credential, and refresh it when it expires.
Connecting Grok
Grok's custom connectors don't register a client, so GlassFrog keeps one for Grok. In Grok, add a custom connector with these settings.
| Server URL |
https://fr.glassfrog.com/api/v5/mcp
|
|---|---|
| Client ID |
grok
|
| Client secret | Leave blank. Grok authenticates with PKCE alone. |
| Token auth method | none (PKCE only) |
Grok then sends you to GlassFrog to sign in and choose what it may access, as described in What the person approves.
Discovery
The authorization server lives at the root of the host that serves the API, outside /api/v5. Discover it rather than hard-coding it. Both documents are public JSON, and every URL in them is built from the host you asked.
GET https://fr.glassfrog.com/.well-known/oauth-protected-resource/api/v5(RFC 9728, path form) describes the API as the protected resource and names the authorization server; the MCP endpoint has its own document athttps://fr.glassfrog.com/.well-known/oauth-protected-resource/api/v5/mcp. A401from the API points at the document for the endpoint that answered, in theresource_metadataparameter of itsWWW-Authenticatechallenge. Both name the same authorization server, and a token works on both.GET https://fr.glassfrog.com/.well-known/oauth-authorization-server(RFC 8414) lists the endpoints below and the supported grant types, response types, PKCE methods and scopes.
| Endpoint | Purpose |
|---|---|
GET, POST https://fr.glassfrog.com/oauth/authorize
|
Authorization endpoint, where the person approves the client |
POST https://fr.glassfrog.com/oauth/token
|
Token endpoint, for the authorization-code and refresh-token grants |
POST https://fr.glassfrog.com/oauth/revoke
|
Token revocation (RFC 7009) |
POST https://fr.glassfrog.com/oauth/register
|
Dynamic client registration (RFC 7591) |
POST https://fr.glassfrog.com/api/v5/mcp
|
The MCP endpoint, a protected resource like the rest of API v5 |
Client registration
Register with POST https://fr.glassfrog.com/oauth/register and a JSON body (RFC 7591). Registration is open and needs no credential, and it grants nothing: a registered client can do nothing until a person approves it.
POST /oauth/register HTTP/1.1
Host: fr.glassfrog.com
Content-Type: application/json
{"client_name": "Example Client",
"redirect_uris": ["https://client.example.com/oauth/callback"]}
HTTP/1.1 201 Created
Content-Type: application/json
{"client_id": "CLIENT_ID", "client_id_issued_at": 1790000000,
"client_name": "Example Client",
"redirect_uris": ["https://client.example.com/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"], "scope": "api"}
Every registered client is public. The response carries a client_id and no secret, and the client authenticates at the token endpoint with PKCE alone (token_endpoint_auth_method: none). A requested authentication method or scope is replaced by what is registered, and the response says so.
Redirect URIs
- Absolute
httpsURIs only. Custom schemes andurn:ietf:wg:oauth:2.0:oobare refused. - A host is required; user information and fragments are refused.
- Loopback, private and link-local hosts are refused, including
localhost,*.localhostand*.local. - Between one and 5 redirect URIs, each at most 2,000 characters.
- The
redirect_urisent at authorization must match a registered one exactly.
client_name is optional, at most 100 characters, and may not contain markup or control characters. The consent screen shows it as reported by the client, next to the redirect host; without one, the redirect host stands in.
grant_types may list only authorization_code and refresh_token, and response_types only code. Leave them out to get exactly those.
A refused registration answers 400 with an RFC 7591 error body: error is invalid_redirect_uri or invalid_client_metadata, with an error_description.
A client that nobody has approved 14 days after registering is deleted. Register again if that happens.
What the person approves
GlassFrog always shows the consent screen; a client is never approved silently. Only a person signed in as themselves can approve a client. On the consent screen the person:
- Chooses one organization, from those where AI integration is enabled. The tokens act in that organization only.
-
Chooses what the client may access, its grant set:
- Everything I can access: the client acts as the person, with everything they can see and do in the organization.
- Only selected roles: the client acts only through the roles the person picks, each with read or change access, and optionally on their personal items (tensions, actions, projects and notes assigned to them personally).
- Everything in this organization: the client acts as an organization admin. Offered to organization admins only.
A token's authority is that grant set, not its OAuth scope. Permissions follow the person's membership in the organization, narrowed to the grant set. Limiting a client to selected roles narrows what it can change and which circle-private items it can read; anything every member of the organization can see stays readable.
Tokens
POST https://fr.glassfrog.com/oauth/token takes a form-encoded body. A public client sends its client_id in the body and no secret.
Exchanging the code
Send the code, the same redirect_uri and the code_verifier behind the code challenge.
POST /oauth/token HTTP/1.1
Host: fr.glassfrog.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=https%3A%2F%2Fclient.example.com%2Foauth%2Fcallback&client_id=CLIENT_ID&code_verifier=CODE_VERIFIER
HTTP/1.1 200 OK
Content-Type: application/json
{"access_token": "gfo_ACCESS_TOKEN", "token_type": "Bearer", "expires_in": 1800,
"refresh_token": "gfr_REFRESH_TOKEN", "scope": "api", "created_at": 1790000000}
Access tokens start with gfo_ and expire after 30 minutes; refresh tokens start with gfr_. Treat both as opaque secrets.
Refreshing
Refresh with grant_type=refresh_token. Refresh tokens rotate: every refresh returns a new refresh token, and the one you sent stops working at once. Presenting a used refresh token again is treated as theft and revokes every token issued under that approval, so the person has to approve the client again. Keep a single copy of the current refresh token and replace it as soon as a refresh succeeds.
POST /oauth/token HTTP/1.1 Host: fr.glassfrog.com Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=gfr_REFRESH_TOKEN&client_id=CLIENT_ID
Calling the API
Send the access token in the Authorization header on any API v5 endpoint, including the MCP endpoint at https://fr.glassfrog.com/api/v5/mcp:
POST /api/v5/mcp HTTP/1.1 Host: fr.glassfrog.com Authorization: Bearer gfo_ACCESS_TOKEN
OAuth access tokens are accepted only as Authorization: Bearer. An OAuth token in X-Auth-Token is rejected, a gfr_ refresh token is never a valid bearer credential, and API v3 and v4 do not accept OAuth tokens.
A request without a valid credential answers 401 with an RFC 6750 challenge whose resource_metadata points at the protected-resource document. error="invalid_token" is added when a credential was sent and rejected, such as an expired access token: refresh, then retry.
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer realm="GlassFrog", error="invalid_token", resource_metadata="https://fr.glassfrog.com/.well-known/oauth-protected-resource/api/v5"
OAuth requests count toward the organization's API rate limit, like requests made with API keys.
Scopes
There is one scope, api. It is the default, so a client may leave scope out; any other value is refused. What a token may do is decided by the grant set the person chose, never by the scope.
Revocation
A client revokes a token with POST https://fr.glassfrog.com/oauth/revoke (RFC 7009): a form-encoded token, an optional token_type_hint, and its client_id. It answers 200 whether or not the token was still valid. A client can revoke only its own tokens; a request with an unknown client ID, or for a token that belongs to another client, gets 403.
POST /oauth/revoke HTTP/1.1 Host: fr.glassfrog.com Content-Type: application/x-www-form-urlencoded token=gfr_REFRESH_TOKEN&token_type_hint=refresh_token&client_id=CLIENT_ID
A person can revoke a client at any time under Connected apps on the API tab of their profile in the organization.
Every token issued under an approval also ends when:
- the person leaves or is removed from the organization;
- the person's account is deactivated;
- GlassFrog disables the client;
- a used refresh token is presented again.
After that the API answers 401 and a refresh fails with invalid_grant. Start a new authorization request; the person has to approve the client again.
Rate limits
The OAuth endpoints are rate-limited per client IP address and answer 429 with a Retry-After header over the limit:
/oauth/authorize: 150 requests per 10 minutes./oauth/tokenand/oauth/revoke: 300 requests per 10 minutes, together./oauth/register: 20 registration requests per hour, refused ones included.