Client ID Metadata Document (CIMD) support for Doorkeeper.
Warning
Pre-release. Nothing is implemented yet. The gem currently contains only its module skeleton. Everything below describes the intended behavior and API, and both will change before 0.1.0 ships. Do not use this in production.
Normally an OAuth client has to be registered with your provider before it can ask for an authorization code — someone creates a record, copies out a client_id and client_secret, and pastes them into the client. That handshake does not scale to an open network of clients, and it is the reason a new app cannot simply start talking to your server.
CIMD removes the pre-registration step. The client uses an HTTPS URL as its client_id, and that URL serves a JSON document describing the client:
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example App",
"client_uri": "https://app.example.com",
"logo_uri": "https://app.example.com/logo.png",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read write",
"token_endpoint_auth_method": "none"
}The provider fetches that document during the authorization request and treats it as the client's registration for the life of that request. The approach is specified in the IETF draft OAuth Client ID Metadata Document and is used in the wild by IndieAuth and AT Protocol / Bluesky.
This gem adds that flow to a Doorkeeper-backed provider: fetching the document, validating it, caching it, and handing Doorkeeper something that behaves like an application record.
| Ruby | >= 3.2 |
| Doorkeeper | >= 5.6, < 6 |
Your application must already be set up as a Doorkeeper OAuth provider.
Not published to RubyGems yet. Install from git:
# Gemfile
gem "doorkeeper-cimd", github: "andrewmcodes/doorkeeper-cimd"bundle install# config/initializers/doorkeeper_cimd.rb
Doorkeeper::Cimd.configure do
# Restrict which origins may serve a metadata document.
# Omit to allow any HTTPS origin (see Security below before you do).
allowed_hosts %w[app.example.com]
# How long a fetched document is reused before it is re-fetched.
cache_ttl 1.hour
# Where fetched documents are stored. Defaults to Rails.cache.
cache Rails.cache
# Limits on the outbound fetch itself.
fetch_timeout 5.seconds
max_response_size 100.kilobytes
max_redirects 0
endEvery option has a usable default; an empty configure block is valid.
With the initializer in place, an authorization request whose client_id is an HTTPS URL resolves through CIMD. Every other client_id continues to resolve against your existing oauth_applications table, so registered clients are unaffected.
Before a document is accepted, the gem checks that:
- The
client_idinside the document is byte-for-byte the URL it was fetched from. - The URL is HTTPS, and its host is in
allowed_hostswhen that option is set. - The
redirect_uriin the authorization request appears inredirect_uris. - The requested
response_typeandgrant_typeappear in the document. - The requested scopes are a subset of
scope.
A document that fails any of these is rejected and the authorization request fails with invalid_client.
client = Doorkeeper::Cimd.resolve("https://app.example.com/oauth/client-metadata.json")
client.client_id # => "https://app.example.com/oauth/client-metadata.json"
client.client_name # => "Example App"
client.redirect_uris # => ["https://app.example.com/oauth/callback"]
client.scopes # => ["read", "write"]resolve raises Doorkeeper::Cimd::Error when the document cannot be fetched, is not valid JSON, or fails validation. Pass raise_on_error: false to get nil instead:
client = Doorkeeper::Cimd.resolve("https://app.example.com/oauth/client-metadata.json", raise_on_error: false)CIMD clients are unverified by definition — anyone can publish a document claiming any client_name. Show users the client_id host, not just the display name:
<p>
<strong><%= @client.client_name %></strong>
(<%= URI(@client.client_id).host %>)
is requesting access to your account.
</p>Resolving a CIMD client means your server makes an outbound HTTP request to a URL an untrusted party supplied. Treat it accordingly:
- Server-side request forgery. Requests to private and link-local address ranges are refused, and redirects are not followed by default (
max_redirects 0). Setallowed_hostsif you can enumerate the clients you expect. - Resource exhaustion.
fetch_timeoutandmax_response_sizebound every fetch. Documents are cached forcache_ttlso a busy client does not mean a fetch per request. - Client impersonation. The
client_nameandlogo_uriin a document are attacker-controlled strings. Never present them to a user as verified, and always show theclient_idorigin alongside them. - Public clients. CIMD clients hold no secret, so they are public clients. Require PKCE for them (
force_pkcein your Doorkeeper config).
- Document fetching, validation, and caching
- Doorkeeper integration for the authorization code grant
- Refresh token grant
- SSRF protections and configurable host allowlists
- Test suite and RBS signatures for the public API
- First release to RubyGems
bin/setup # install dependencies
bundle exec rake # specs + Standard lint (what CI runs)
bin/console # IRB with the gem loadedLint autofix is bundle exec standardrb --fix. Keep the RBS signatures in sig/ in sync with the public API.
To release: bump VERSION in lib/doorkeeper/cimd/version.rb, update CHANGELOG.md, then bundle exec rake release.
Bug reports and pull requests are welcome at https://github.com/andrewmcodes/doorkeeper-cimd. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
Available as open source under the terms of the MIT License.