Security Review
When implementing or reviewing changes to Doorkeeper, use this skill to verify that changes do not introduce security vulnerabilities. Doorkeeper is an OAuth 2 provider library — security is the primary constraint.
Constant-Time Comparisons
All comparisons of secrets MUST use ActiveSupport::SecurityUtils.secure_compare or equivalent, never ==:
- Token values
- Client secrets
- PKCE code challenge verification
- Refresh tokens
- Any cryptographic digest comparison
Check: grep for == near variables named token, secret, code_challenge, refresh_token. If found, replace with secure_compare.
Reference: The codebase already uses secure_compare in lib/doorkeeper/secret_storing/base.rb. Follow that pattern.
Token Leakage Prevention
Tokens and secrets must never appear in:
- Log output (Rails parameter filtering should exclude them)
- Error response bodies (beyond what RFC 6749 requires)
- URL query parameters for sensitive values (except where RFC requires, e.g., authorization codes)
- Exception messages that might be captured by error reporters
Check: Doorkeeper registers Doorkeeper.setup_filter_parameters (in lib/doorkeeper.rb) which adds client_secret, authentication_token, access_token, and refresh_token to Rails.application.config.filter_parameters, plus code when the authorization_code grant flow is enabled. Verify any newly-introduced sensitive parameter is covered by that list.
Redirect URI Validation (RFC 6749 §3.1.2, RFC 8252 §7)
- Redirect URIs must be compared using exact string matching
- Exception: loopback redirects (127.0.0.1, [::1]) allow port variation per RFC 8252 §7.3
- Fragment components (
#) are forbidden - Relative URIs are forbidden
- The
urn:ietf:wg:oauth:2.0:oobredirect is special-cased
Reference: lib/doorkeeper/oauth/helpers/uri_checker.rb and lib/doorkeeper/orm/active_record/redirect_uri_validator.rb
Client Authentication (RFC 6749 §2.3)
- A client MUST NOT use more than one authentication method per request
- The codebase enforces this via
Doorkeeper::Request.validate_client_authentication!— ensure new auth methods participate in this check - Confidential clients must always authenticate; public clients use
none by_uid_and_secretmust be the resolution path — don't bypass it
Reference: lib/doorkeeper/client_authentication/ and lib/doorkeeper/request.rb
Grant Code Single-Use (RFC 6749 §4.1.2)
- Authorization codes MUST be single-use
- If a code is presented twice, all tokens issued from that code must be revoked
- Race conditions are handled via database locking (
lock!)
Reference: lib/doorkeeper/oauth/authorization_code_request.rb — InvalidGrantReuse handling
Scope Validation
- Requested scopes must be a subset of configured server scopes
- Per-application scopes further restrict what a client can request
scopes_by_grant_typecan restrict scopes per flow- Dynamic scopes (if enabled) must still match the configured delimiter pattern
Reference: lib/doorkeeper/oauth/helpers/scope_checker.rb
PKCE (RFC 7636)
- When
force_pkceis true,code_challengeis mandatory - S256 is the recommended (and often required) challenge method
- Code verifier comparison must use the correct transform (SHA256 + base64url for S256, plain for plain)
- Note: the codebase currently uses
==for PKCE comparison — this is a known issue
Input Validation
- All user-supplied parameters must be validated before use
grant_type,response_type,response_modemust match registered values- Token endpoint enforces
application/x-www-form-urlencodedcontent type (whenenforce_content_typeis enabled) - Multiple values for
scopein the same request must be rejected at the authorization endpoint
Revocation (RFC 7009)
- Token revocation should return 200 even for invalid tokens (to avoid information leakage)
- Revoking a refresh token should also revoke associated access tokens
- Client authentication is required for revocation (default behavior)
Patterns to Avoid
# BAD: String comparison on secrets
token == stored_token
# GOOD: Constant-time comparison
ActiveSupport::SecurityUtils.secure_compare(token, stored_token)
# BAD: Token in error message
raise "Invalid token: #{token}"
# GOOD: Generic error
raise Doorkeeper::Errors::InvalidToken
# BAD: Redirect URI with partial matching
uri.start_with?(registered_uri)
# GOOD: Exact match (already handled by URIChecker)
Doorkeeper::OAuth::Helpers::URIChecker.valid_for_authorization?(uri, registered_uris)
# BAD: Skipping client authentication
# Never bypass validate_client_authentication! for token-endpoint requests
# GOOD: All token requests go through client auth
# Request.client_authentication_method calls validate_client_authentication!(request) internally
Doorkeeper::Request.client_authentication_method(request)
After Making Changes
- Run security-related specs:
bundle exec rspec spec/requests/flows/ spec/lib/oauth/ - Check if Brakeman would flag the change:
gem install brakeman && brakeman --no-pager - Verify no token values appear in test output or logs