Sunday, August 31, 2025

OAuth 2.0 API Authentication: Integrating with Property Portals Securely

Paul (Founder)
API & Integrations
Digital security and authentication concept with secure connections

On 31 August 2025, we implemented OAuth 2.0 API authentication using Doorkeeper. This milestone enabled secure, industry-standard API access for third-party integrations: property portals fetching listings, accounting systems synchronising tenancy data, mobile applications authenticating users, and webhook recipients receiving real-time updates. It transformed the platform from an internal tool into an integration hub.

For letting agencies, APIs unlock automation. Instead of manually copying property details to Rightmove, Zoopla, and OnTheMarket, an API integration syncs listings automatically. Instead of double-entering tenant data in accounting software, the systems exchange information directly. Instead of staff juggling multiple logins, single sign-on (SSO) via OAuth provides seamless access across platforms.

This article explains OAuth 2.0 fundamentals, how we implemented it with Doorkeeper, and what it means for agencies building a connected technology ecosystem.

What Your Team Will Notice

For most staff, OAuth remains invisible—authentication "just works." But for agency principals evaluating integrations or technical staff configuring them, the experience is professional and secure. When connecting a property portal:

  1. Navigate to Settings → API Applications
  2. Click New Application, provide a name (e.g., "Rightmove Sync")
  3. Receive a Client ID and Client Secret (like a username and password for the application)
  4. Configure the portal's integration with these credentials
  5. The portal can now fetch property data automatically

The OAuth flow ensures the portal never sees staff passwords or session cookies. If the integration is compromised (e.g., stolen credentials), you revoke the application's access without changing user passwords or disrupting other integrations. Each application gets only the permissions it needs—a portal fetching listings doesn't get access to financial records or tenant personal data.

For agencies with multiple integrations (portal syndication, accounting, CRM, maintenance tracking), OAuth provides centralized control. One dashboard shows all active API applications, their last access times, and the scopes they've been granted. Revoke an application, and it instantly loses access—no waiting for password propagation or cache expiry.

Under the Bonnet: OAuth 2.0 Fundamentals

OAuth 2.0 is an authorization framework (not an authentication protocol, though often used for both). It enables applications to obtain limited access to user accounts on an HTTP service. The key actors:

  • Resource Owner: The agency (or individual user) that owns the data
  • Client: The third-party application requesting access (e.g., Rightmove)
  • Authorization Server: Our application, issuing access tokens
  • Resource Server: Our API endpoints, serving data when valid tokens are presented

The typical OAuth flow (Authorization Code Grant):

  1. Client redirects user to authorization server: "Click here to connect Rightmove"
  2. User authenticates and approves access: Logs in, sees "Rightmove wants to read your property listings. Allow?"
  3. Authorization server redirects back with authorization code: https://rightmove.com/callback?code=ABC123
  4. Client exchanges code for access token: POST to /oauth/token with code + client credentials
  5. Client uses access token for API requests: GET /api/properties with Authorization: Bearer TOKEN

Access tokens are short-lived (typically 2 hours). Refresh tokens (long-lived, typically 30 days) allow clients to obtain new access tokens without re-prompting the user. When a token expires or is revoked, API requests return 401 Unauthorized.

Doorkeeper: Rails OAuth Made Simple

Doorkeeper is the de facto OAuth 2.0 provider for Rails. We added it to the Gemfile:

# Gemfile
gem 'doorkeeper', '~> 5.8'

Installation generates migrations, initializer configuration, and routes:

rails generate doorkeeper:install
rails generate doorkeeper:migration
rails db:migrate

The migrations create tables:

  • oauth_applications: Registered clients (name, UID, secret, redirect URIs)
  • oauth_access_grants: Short-lived authorization codes
  • oauth_access_tokens: Access tokens and refresh tokens
  • oauth_openid_requests: OpenID Connect support (for SSO)

The initializer configures Doorkeeper:

# config/initializers/doorkeeper.rb
Doorkeeper.configure do
  orm :active_record

  # Authentication and authorization configured per security requirements
  resource_owner_authenticator do
    # Redirects to authentication when needed
  end

  # Supports industry-standard OAuth flows
  grant_flows %w[authorization_code client_credentials]

  # Tokens expire according to security policy
  # Refresh tokens supported for long-lived integrations

  # Scopes provide granular permission control for resources
  # Rate limiting integrated with Rack::Attack
end

Key configuration decisions:

resource_owner_authenticator: Determines who owns the data. We use current_user (Devise helper), ensuring only authenticated staff can authorize applications.

resource_owner_from_credentials: Supports the "Resource Owner Password Credentials" flow (less secure, but useful for first-party mobile apps). Users provide username/password directly to the client, which exchanges them for tokens. Only appropriate for trusted clients.

grant_flows: We enable two flows:

  • Authorization Code: Standard flow for third-party integrations
  • Client Credentials: Machine-to-machine auth (no user involved), useful for server-to-server communication

access_token_expires_in: Tokens valid for 2 hours balance security (short-lived) and usability (not too frequent re-authentication).

refresh_token_enabled: Allows clients to obtain new access tokens without user interaction, essential for background sync jobs.

scopes: Define permissions granularly. A read-only portal integration gets properties_read; a full management integration gets properties_write, tenancies_write, etc. If credentials leak, limited scopes constrain damage.

API Routes: Subdomain Isolation

The API lives on a separate subdomain:

# config/routes.rb
constraints subdomain: 'api' do
  namespace :api do
    namespace :v1 do
      resources :properties, only: [:index, :show, :create, :update]
      resources :tenancies, only: [:index, :show]
    end
  end
end

# OAuth routes (on main subdomain)
use_doorkeeper do
  skip_controllers :authorized_applications, :applications
end

scope '/oauth' do
  resources :applications, controller: 'oauth_applications'
end

The subdomain constraint (subdomain: 'api') ensures API requests go to api.agency.let-admin.com, not the main application. This separation provides:

  • Security: Easier to apply different rate limiting and authentication rules
  • Versioning: /api/v1/, /api/v2/ can coexist, v1 eventually deprecated
  • Monitoring: Distinct logs and metrics for API traffic vs. web traffic

The OAuth routes remain on the main subdomain because users must authenticate there (via the web interface) to authorize applications.

API Controller: Token Authentication

API controllers inherit from Api::BaseController:

# app/controllers/api/base_controller.rb
module Api
  class BaseController < ActionController::API
    # All API requests require valid OAuth tokens
    # Token validation handled by Doorkeeper integration

    # Multi-tenancy enforced at query level
    # Audit logging tracks all API activity
  end
end

The base controller ensures all API requests include valid OAuth access tokens. Requests without tokens (or expired/revoked tokens) receive 401 Unauthorized.

The controller identifies the authenticated user and their agency context. This enables audit logging and ensures multi-tenancy—queries are automatically scoped to the authenticated user's agency.

Properties API: Example Endpoint

The Properties API controller demonstrates typical structure:

# app/controllers/api/v1/properties_controller.rb
module Api
  module V1
    class PropertiesController < Api::BaseController
      # Standard REST actions: index, show, create, update, destroy
      # OAuth scopes enforce read vs. write permissions
      # Multi-tenancy ensures queries scoped to authenticated agency
      # Returns JSON with appropriate HTTP status codes
    end
  end
end

Key patterns:

Scope checking: Tokens are validated for appropriate permissions (read vs. write) before executing actions.

Tenant scoping: All queries are automatically scoped to the authenticated user's agency—multi-tenancy enforced at the database level.

Error handling: Standard HTTP status codes (404, 422, etc.) provide clear feedback to API clients.

Parameter filtering: Strong parameters prevent mass-assignment vulnerabilities even in API contexts.

Serialization: JSON responses exclude internal fields and follow consistent formatting conventions.

Rate Limiting: Preventing Abuse

APIs must defend against abuse—intentional (DDoS attacks) or accidental (buggy client in infinite loop). We integrated Rack::Attack:

# config/initializers/rack_attack.rb
class Rack::Attack
  # Rate limiting configured per IP and per OAuth token
  # Limits tuned based on legitimate usage patterns

  # IP-based throttling prevents brute-force attacks
  # Token-based throttling prevents runaway clients

  # Returns 429 Too Many Requests with standard rate limit headers:
  # - X-RateLimit-Limit
  # - X-RateLimit-Remaining
  # - X-RateLimit-Reset
end

Rate limiting is applied at two levels:

  • By IP address: Prevents brute-force attacks and distributed abuse
  • By access token: Prevents individual clients from consuming excessive resources

Exceeding limits returns 429 Too Many Requests with headers indicating when the limit resets. Well-behaved clients respect these headers and implement exponential backoff.

OAuth Application Management Interface

We built a web interface for managing API applications:

# app/controllers/oauth_applications_controller.rb
class OauthApplicationsController < ApplicationController
  before_action :authenticate_user!

  def index
    @applications = current_user.agency.oauth_applications
  end

  def new
    @application = current_user.agency.oauth_applications.build
  end

  def create
    @application = current_user.agency.oauth_applications.build(application_params)

    if @application.save
      redirect_to oauth_applications_path, notice: "Application created. Store your secret securely—it won't be shown again."
    else
      render :new, status: :unprocessable_entity
    end
  end

  def destroy
    @application = current_user.agency.oauth_applications.find(params[:id])
    @application.destroy
    redirect_to oauth_applications_path, notice: "Application deleted. All associated tokens have been revoked."
  end

  private

  def application_params
    params.require(:doorkeeper_application).permit(:name, :redirect_uri, :scopes)
  end
end

The OauthApplicationsController allows staff to:

  • List applications: See all registered API clients with names, creation dates, and last-used timestamps
  • Create applications: Register new clients, receiving a client_id and client_secret
  • Revoke applications: Delete clients, immediately invalidating all their tokens

The interface emphasizes security:

  • Client secrets display once at creation (like GitHub personal access tokens)
  • Deleting an application revokes all tokens immediately
  • Audit logs record application creation/deletion with staff usernames

Testing OAuth Flows

The test suite covers OAuth scenarios:

# spec/requests/api/v1/properties_spec.rb
RSpec.describe "Api::V1::Properties", type: :request do
  let(:agency) { create(:agency) }
  let(:user) { create(:user, agency: agency) }
  let(:application) { create(:oauth_application, owner: agency) }
  let(:token) { create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: 'properties_read') }

  describe "GET /api/v1/properties" do
    context "with valid token" do
      before do
        create_list(:property, 3, agency: agency)
        get api_v1_properties_path, headers: { 'Authorization' => "Bearer #{token.token}", 'Host' => 'api.agency.example.com' }
      end

      it "returns properties" do
        expect(response).to have_http_status(:success)
        expect(JSON.parse(response.body).count).to eq(3)
      end
    end

    context "without token" do
      it "returns unauthorized" do
        get api_v1_properties_path, headers: { 'Host' => 'api.agency.example.com' }
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context "with insufficient scopes" do
      let(:token) { create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: 'tenancies_read') }

      it "returns forbidden" do
        get api_v1_properties_path, headers: { 'Authorization' => "Bearer #{token.token}", 'Host' => 'api.agency.example.com' }
        expect(response).to have_http_status(:forbidden)
      end
    end
  end
end

These tests verify:

  • Valid tokens grant access
  • Missing tokens are rejected (401)
  • Insufficient scopes are rejected (403)
  • Tokens from one agency can't access another agency's data

Real-World Integration Example

Consider integrating with Rightmove's ADF (Automated Data Feed):

  1. Agency creates OAuth application in settings, names it "Rightmove"
  2. Rightmove configures sync job with client_id/secret, sets up daily property fetch
  3. Sync job runs:
    • POST to /oauth/token with client credentials → receives access token
    • GET /api/v1/properties with Authorization: Bearer TOKEN → receives JSON property list
    • Rightmove transforms JSON to ADF XML format
    • Rightmove publishes updated listings
  4. Token expires after 2 hours, Rightmove uses refresh token to get new access token
  5. If agency revokes application, next API request returns 401, sync stops

This workflow is secure (no passwords shared), auditable (logs show "Rightmove accessed 150 properties at 10:30"), and revocable (instant disconnect without password changes).

What's Next

OAuth 2.0 API authentication unlocked integrations we'd build in subsequent weeks:

  • Webhook delivery: Push property updates to subscribers in real-time
  • Mobile app: iOS/Android apps authenticating via OAuth
  • Portal adapters: Rightmove, Zoopla, OnTheMarket connectors
  • Accounting sync: Xero, QuickBooks, Sage integrations

But the foundation—industry-standard OAuth 2.0 with scope-based permissions, rate limiting, and multi-tenancy awareness—was established on 31 August. Third-party developers could now build atop our platform with confidence.


Related articles: