How API Authentication with Doorkeeper OAuth Works in Maybe Finance
The Maybe Finance API secures every endpoint using OAuth 2.0 access tokens issued by the Doorkeeper gem, validating requests in Api::V1::BaseController to establish the current user context.
The maybe-finance/maybe repository implements a stateless API authentication with Doorkeeper OAuth to protect financial data endpoints while maintaining compatibility with the traditional session-based web interface. This Rails application uses Doorkeeper to manage token issuance and validation, ensuring that every API request carries a valid Bearer token with appropriate scopes.
OAuth Token Issuance and Resource Owner Resolution
The authentication flow begins when clients request an access token from the Doorkeeper token endpoint.
Configuring the Resource Owner Authenticator
In config/initializers/doorkeeper.rb, the resource_owner_authenticator block defines how Doorkeeper maps an incoming request to a User record. The implementation extracts the session_token from cookies and looks up the corresponding user:
resource_owner_authenticator do
session_token = cookies.encrypted[:session_token]
User.find_by(id: session_token&.dig("user_id"))
end
This configuration allows Doorkeeper to issue tokens bound to specific users while leveraging the existing session infrastructure.
Token Validation in API Controllers
All API controllers inherit from Api::V1::BaseController, which implements custom token validation logic rather than using Doorkeeper's default before_action helpers.
Manual Token Lookup in BaseController
The controller extracts the Bearer token from the Authorization header and performs a manual database lookup:
token_string = request.authorization&.split(" ")&.last
access_token = Doorkeeper::AccessToken.by_token(token_string)
This approach provides fine-grained control over error handling and response formatting.
Scope Verification and Expiration Checks
After retrieving the token, the controller validates three conditions:
- Existence: The token must exist in the database
- Expiration: The token must not be expired (
token.expired?check) - Scope: The token must include at least the
readscope, orread_writefor mutation endpoints
If any check fails, the controller returns a JSON error response with HTTP 401 status.
Setting Up Current User Context
When validation succeeds, the controller establishes the application context for the request:
@_doorkeeper_token = access_token
Current.user = User.find(access_token.resource_owner_id)
Current.session = Session.new(user: Current.user)
This creates a temporary Session object and sets Current.user, allowing the rest of the application to rely on these globals regardless of whether the request came from the web UI or the API.
Error Handling for Invalid Tokens
The controller overrides doorkeeper_unauthorized_render_options to ensure API clients receive JSON responses rather than HTML redirects:
def doorkeeper_unauthorized_render_options(error: nil)
{
json: { error: "unauthorized", message: error.description },
status: :unauthorized
}
end
This method returns a 401 status with a descriptive error message when token validation fails.
Route Configuration
Doorkeeper routes are mounted in config/routes.rb using the use_doorkeeper method:
use_doorkeeper do
skip_controllers :authorizations, :applications, :authorized_applications
end
namespace :api do
namespace :v1 do
# API endpoints...
end
end
This configuration exposes the standard OAuth endpoints (/oauth/token, /oauth/revoke) while skipping the authorization flow controllers that are unnecessary for a first-party API.
Practical Examples
Obtaining an Access Token
Clients authenticate using the password grant flow to receive an access token:
curl -X POST https://api.maybe.finance/oauth/token \
-d "grant_type=password" \
-d "username=user@example.com" \
-d "password=secret123" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=read_write"
Calling a Protected Endpoint
Include the access token in the Authorization header for all API requests:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
https://api.maybe.finance/api/v1/accounts
Refreshing an Access Token
When the access token expires, use the refresh token to obtain a new one:
curl -X POST https://api.maybe.finance/oauth/token \
-d "grant_type=refresh_token" \
-d "refresh_token=YOUR_REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Key Implementation Files
| File | Purpose |
|---|---|
config/initializers/doorkeeper.rb |
Configures Doorkeeper, defines the resource_owner_authenticator block that maps session cookies to User records, and sets token expiration and scopes |
config/routes.rb |
Mounts Doorkeeper routes via use_doorkeeper and defines the API namespace |
app/controllers/api/v1/base_controller.rb |
Validates Bearer tokens manually, checks scopes and expiration, sets Current.user and Current.session, and returns JSON 401 errors |
app/models/user.rb |
The resource owner model associated with OAuth tokens via resource_owner_id |
app/models/api_key.rb |
Provides alternative API key authentication when OAuth tokens are not used |
Summary
- Doorkeeper Configuration: The
resource_owner_authenticatorinconfig/initializers/doorkeeper.rblinks OAuth tokens to users via session cookies. - Manual Token Validation:
Api::V1::BaseControllerperforms explicit Bearer token extraction and validation usingDoorkeeper::AccessToken.by_token. - Scope Enforcement: The API checks for
readorread_writescopes before allowing access to endpoints. - Context Initialization: Valid tokens trigger the creation of
Current.userandCurrent.sessionobjects for downstream application logic. - JSON Error Responses: Invalid or missing tokens return HTTP 401 with JSON error bodies rather than HTML redirects.
Frequently Asked Questions
How does Doorkeeper integrate with the existing User model?
Doorkeeper uses the resource_owner_authenticator block defined in config/initializers/doorkeeper.rb to resolve the user associated with a token request. This block extracts the encrypted session_token cookie and looks up the corresponding User record, allowing Doorkeeper to issue access tokens bound to specific users while leveraging the existing session infrastructure.
What OAuth scopes are available for API tokens?
The Maybe Finance API supports two primary scopes: read and read_write. The read scope grants access to GET endpoints for retrieving financial data, while read_write is required for POST, PUT, PATCH, and DELETE operations that modify data. The Api::V1::BaseController explicitly checks these scopes during token validation.
How does the API handle expired or invalid tokens?
When a request contains an expired, revoked, or malformed token, the Api::V1::BaseController returns an HTTP 401 Unauthorized response with a JSON error body. This behavior is implemented through the overridden doorkeeper_unauthorized_render_options method, which ensures API clients receive machine-readable error messages rather than HTML redirects.
Can API keys be used instead of OAuth tokens?
Yes, the application supports an alternative authentication method via API keys defined in app/models/api_key.rb. When a request does not include a valid OAuth Bearer token, the system can fall back to validating API keys, providing flexibility for service-to-service integrations or legacy client support alongside the primary Doorkeeper OAuth implementation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →