Skip to content

No Authentication Model

The Danish Parliament API implements a unique "no authentication required" model that prioritizes democratic transparency over access control. This document explains the security implications, benefits, and best practices for working with an open government API.

Authentication Architecture

No Authentication Required

The API requires absolutely no authentication:

# These requests work identically - no credentials needed
curl "https://oda.ft.dk/api/Sag?$top=5"
curl -H "Authorization: Bearer invalid-token" "https://oda.ft.dk/api/Sag?$top=5"
curl -H "X-API-Key: anything" "https://oda.ft.dk/api/Sag?$top=5"

# All return identical results with HTTP 200

Tested Authentication Methods

Based on comprehensive testing (Phase 15), the following authentication methods were tested and confirmed to be unnecessary:

  • ✅ No Bearer tokens required
  • ✅ No API keys needed
  • ✅ No basic authentication
  • ✅ No OAuth flows
  • ✅ No session cookies
  • ✅ No IP whitelisting
  • ✅ No geographic restrictions
  • ✅ No user agent filtering

Security Implications

Benefits of Open Access

1. Democratic Transparency

# Anyone can access parliamentary data instantly
import requests

def get_voting_transparency(politician_name):
    """Get voting records without authentication barriers"""
    response = requests.get(
        "https://oda.ft.dk/api/Aktør",
        params={
            "$filter": f"navn eq '{politician_name}'",
            "$expand": "Stemme($expand=Afstemning)"
        }
    )
    return response.json()

# No API keys to manage, no registration required
voting_data = get_voting_transparency("Frank Aaen")

2. Reduced Complexity

// Simple TypeScript client - no auth layer needed
class DanishParliamentAPI {
    private baseURL = 'https://oda.ft.dk/api';

    // No authentication setup required
    async getCases(limit: number = 100): Promise<any> {
        const response = await fetch(`${this.baseURL}/Sag?$top=${limit}`);
        return response.json();
    }

    // No token refresh, no credential management
    async getVotingRecords(caseId: number): Promise<any> {
        const response = await fetch(
            `${this.baseURL}/Afstemning?$filter=sagid eq ${caseId}&$expand=Stemme`
        );
        return response.json();
    }
}

3. Universal Access

// Works directly in browser without CORS issues
fetch('https://oda.ft.dk/api/Sag?$top=10')
    .then(response => response.json())
    .then(data => {
        console.log('Parliamentary cases:', data.value);
        // No authentication errors to handle
    });

Security Considerations

1. Read-Only Protection

The API implements strict read-only access:

# All write operations properly rejected
curl -X POST "https://oda.ft.dk/api/Sag" -d "{\"titel\": \"test\"}" \
     -H "Content-Type: application/json"
# Returns: HTTP 501 Not Implemented

curl -X PUT "https://oda.ft.dk/api/Sag(12345)" -d "{\"titel\": \"modified\"}" \
     -H "Content-Type: application/json"  
# Returns: HTTP 501 Not Implemented

curl -X DELETE "https://oda.ft.dk/api/Sag(12345)"
# Returns: HTTP 501 Not Implemented

Error response for write attempts:

{
    "odata.error": {
        "code": "501",
        "message": {
            "lang": "en-US",
            "value": "Unsupported functionality"
        }
    }
}

2. Data Integrity Assurance

Without authentication, data integrity is maintained through:

def verify_data_integrity():
    """Verify API data cannot be modified"""
    test_cases = [
        {'method': 'POST', 'expected': 501},
        {'method': 'PUT', 'expected': 501}, 
        {'method': 'DELETE', 'expected': 501},
        {'method': 'PATCH', 'expected': 501}
    ]

    for test in test_cases:
        response = requests.request(
            test['method'], 
            "https://oda.ft.dk/api/Sag",
            json={"test": "data"}
        )
        assert response.status_code == test['expected']
        print(f"{test['method']} properly rejected: {response.status_code}")

verify_data_integrity()

Implementation Best Practices

Client-Side Security

Even without authentication, implement security best practices:

class SecureNoAuthClient:
    def __init__(self):
        self.base_url = "https://oda.ft.dk/api"
        self.session = requests.Session()
        self._setup_security()

    def _setup_security(self):
        """Setup security despite no authentication"""
        # Enforce HTTPS only
        self.session.hooks['response'] = self._verify_https

        # Set security headers
        self.session.headers.update({
            'User-Agent': 'SecureDanishParliamentClient/1.0',
            'Accept': 'application/json',
            'Cache-Control': 'no-cache'
        })

        # Verify TLS
        self.session.verify = True

    def _verify_https(self, response, *args, **kwargs):
        """Ensure responses come over HTTPS"""
        if not response.url.startswith('https://'):
            raise SecurityError("Response was not served over HTTPS")
        return response

    def safe_request(self, endpoint, params=None, max_size=50*1024*1024):
        """Make request with safety checks"""
        response = self.session.get(
            f"{self.base_url}/{endpoint}",
            params=params,
            stream=True  # For size checking
        )

        # Check response size
        content_length = response.headers.get('content-length')
        if content_length and int(content_length) > max_size:
            raise ValueError(f"Response too large: {content_length} bytes")

        return response.json()

Input Validation

Without authentication, validate all inputs carefully:

import re
from urllib.parse import quote

class InputValidator:
    SAFE_FIELD_PATTERN = re.compile(r'^[a-zA-Z][a-zA-Z0-9_]*$')

    @staticmethod
    def validate_odata_field(field_name):
        """Validate OData field names"""
        if not InputValidator.SAFE_FIELD_PATTERN.match(field_name):
            raise ValueError(f"Invalid field name: {field_name}")
        return field_name

    @staticmethod
    def sanitize_filter_value(value):
        """Sanitize OData filter values"""
        # Escape single quotes
        sanitized = str(value).replace("'", "''")
        return quote(sanitized)

    @staticmethod
    def build_safe_filter(field, operator, value):
        """Build safe OData filter"""
        safe_field = InputValidator.validate_odata_field(field)
        safe_value = InputValidator.sanitize_filter_value(value)
        return f"{safe_field} {operator} '{safe_value}'"

# Usage
safe_filter = InputValidator.build_safe_filter('titel', 'eq', "Climate'test")
print(safe_filter)  # titel eq 'Climate''test'

Rate Limiting (Client-Side)

Implement responsible usage patterns:

import time
from collections import deque
from datetime import datetime, timedelta

class RateLimiter:
    def __init__(self, requests_per_minute=60):
        self.requests_per_minute = requests_per_minute
        self.request_times = deque()

    def acquire(self):
        """Acquire permission to make request"""
        now = datetime.now()

        # Remove requests older than 1 minute
        while (self.request_times and 
               now - self.request_times[0] > timedelta(minutes=1)):
            self.request_times.popleft()

        # Check if we've hit the limit
        if len(self.request_times) >= self.requests_per_minute:
            sleep_time = 60 - (now - self.request_times[0]).total_seconds()
            if sleep_time > 0:
                time.sleep(sleep_time)

        self.request_times.append(now)

class ResponsibleClient:
    def __init__(self):
        self.rate_limiter = RateLimiter(requests_per_minute=30)  # Conservative
        self.base_url = "https://oda.ft.dk/api"

    def make_request(self, endpoint, params=None):
        """Make rate-limited request"""
        self.rate_limiter.acquire()

        response = requests.get(f"{self.base_url}/{endpoint}", params=params)
        return response.json()

Access Control Through Design

Functional Limitations

The API implements access control through functional design rather than authentication:

def analyze_access_model():
    """Analyze what's accessible vs. restricted"""

    accessible_features = [
        "Read all parliamentary data",
        "Query historical records (1952-2026)", 
        "Access voting records",
        "Download public documents",
        "View biographical information",
        "Monitor real-time updates"
    ]

    restricted_features = [
        "Modify any data (HTTP 501)",
        "Create new records (HTTP 501)", 
        "Delete records (HTTP 501)",
        "Administrative functions (not exposed)",
        "Private communications (not in dataset)"
    ]

    return {
        "accessible": accessible_features,
        "restricted": restricted_features,
        "access_model": "Read-only transparency"
    }

Data Sensitivity Handling

Handle sensitive data appropriately despite open access:

class SensitiveDataHandler:
    SENSITIVE_FIELDS = ['biografi', 'telefon', 'email', 'adresse']

    def filter_sensitive_data(self, actor_data, include_sensitive=False):
        """Filter out sensitive biographical information"""
        if include_sensitive:
            return actor_data

        return {
            key: value for key, value in actor_data.items()
            if not any(sensitive in key.lower() for sensitive in self.SENSITIVE_FIELDS)
        }

    def get_public_actor_info(self, actor_name):
        """Get only public-facing actor information"""
        response = requests.get(
            "https://oda.ft.dk/api/Aktør",
            params={
                "$filter": f"navn eq '{actor_name}'",
                "$select": "id,navn,typeid,startdato,slutdato"
            }
        )

        return response.json()

Monitoring and Logging

Security Event Logging

import logging
from datetime import datetime

class NoAuthSecurityLogger:
    def __init__(self):
        self.logger = logging.getLogger('danish_parliament_security')
        self.logger.setLevel(logging.INFO)

        handler = logging.FileHandler('parliament_api_security.log')
        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - %(message)s'
        )
        handler.setFormatter(formatter)
        self.logger.addHandler(handler)

    def log_data_access(self, endpoint, params, response_size):
        """Log data access for auditing"""
        self.logger.info(
            f"DATA_ACCESS: endpoint={endpoint}, "
            f"params={params}, response_size={response_size}"
        )

    def log_large_request(self, endpoint, record_count):
        """Log large data requests"""
        if record_count > 1000:
            self.logger.warning(
                f"LARGE_REQUEST: endpoint={endpoint}, "
                f"records={record_count}"
            )

    def log_sensitive_access(self, data_type, actor_name=None):
        """Log access to sensitive information"""
        self.logger.info(
            f"SENSITIVE_ACCESS: type={data_type}, "
            f"actor={actor_name or 'N/A'}"
        )

# Usage
security_logger = NoAuthSecurityLogger()

def monitored_api_request(endpoint, params=None):
    response = requests.get(f"https://oda.ft.dk/api/{endpoint}", params=params)
    data = response.json()

    # Log the access
    record_count = len(data.get('value', []))
    security_logger.log_data_access(endpoint, params, len(response.text))
    security_logger.log_large_request(endpoint, record_count)

    return data

Advantages and Trade-offs

Advantages of No Authentication

  1. Immediate Access: No registration or approval process
  2. Reduced Complexity: No credential management
  3. Universal Compatibility: Works with any HTTP client
  4. Democratic Values: Reflects open government principles
  5. No Rate Limits: (Though client-side throttling recommended)
  6. CORS Friendly: Direct browser access possible

Trade-offs Accepted

  1. No Usage Analytics: Cannot track individual API usage
  2. No Abuse Prevention: Relies on infrastructure-level protection
  3. No Customization: Cannot provide user-specific features
  4. No Private Data: Cannot serve non-public information

Comparison with Authenticated APIs

# Danish Parliament API - No Auth
response = requests.get("https://oda.ft.dk/api/Sag?$top=100")
# Immediate access, no setup required

# Typical Government API - With Auth
headers = {"Authorization": f"Bearer {get_token()}"}
response = requests.get("https://other-api.gov/data", headers=headers)
# Requires registration, token management, renewal, etc.

Security Checklist

Pre-Production Checklist

  • HTTPS Only: All requests use HTTPS
  • Input Validation: All user inputs validated
  • Rate Limiting: Client-side throttling implemented
  • Error Handling: Proper error response handling
  • Logging: Access logging configured
  • Data Filtering: Sensitive data handling implemented
  • Size Limits: Large response size limits configured
  • TLS Verification: Certificate validation enabled

Operational Monitoring

  • Request Patterns: Monitor for unusual access patterns
  • Response Sizes: Track large data extractions
  • Error Rates: Monitor API error responses
  • Performance: Track response times and availability

Integration Examples

Production Application Security

class ProductionParliamentClient:
    def __init__(self):
        self.client = SecureNoAuthClient()
        self.rate_limiter = RateLimiter()
        self.security_logger = NoAuthSecurityLogger()
        self.sensitive_handler = SensitiveDataHandler()

    def get_politician_public_data(self, name):
        """Get politician data with security controls"""
        try:
            # Rate limit
            self.rate_limiter.acquire()

            # Secure request
            data = self.client.safe_request(
                'Aktør',
                params={
                    '$filter': f"navn eq '{name}'",
                    '$select': 'id,navn,typeid,startdato,slutdato'
                }
            )

            # Log access
            self.security_logger.log_data_access('Aktør', {'name': name}, len(str(data)))

            # Filter sensitive data
            return self.sensitive_handler.filter_sensitive_data(data)

        except Exception as e:
            self.security_logger.logger.error(f"Request failed: {e}")
            raise

The Danish Parliament API's no-authentication model represents a bold commitment to government transparency. While it requires careful client-side security implementation, it removes barriers to democratic data access and serves as a model for open government APIs worldwide.