Authentication
Contents
Authentication#
For: developers
OAuth 2.0 client credentials flow, JWT tokens, scopes, and rate limiting for OpenSPP API V2.
Authentication Flow#
OpenSPP API V2 uses OAuth 2.0 Client Credentials flow for machine-to-machine authentication.
┌────────┐ ┌────────────┐
│ Client │──(1) POST /oauth/token──────►│ OpenSPP │
│ │ client_id, client_secret │ OAuth │
│ │◄─(2) access_token────────────│ Server │
│ │ └────────────┘
│ │──(3) GET /api/v2/spp/...────►┌────────────┐
│ │ Authorization: Bearer │ OpenSPP │
│ │◄─(4) Response────────────────│ API │
└────────┘ └────────────┘
Prerequisites#
Before you can authenticate:
Your application must be registered in OpenSPP
You must have a Client ID and Client Secret
Your client must have authorized scopes
Contact your OpenSPP administrator to register your application.
Obtaining an Access Token#
Request#
The token endpoint supports three authentication methods:
Method 1: HTTP Basic Auth (recommended per RFC 6749)
POST /api/v2/spp/oauth/token
Authorization: Basic base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
Method 2: Form-encoded body
POST /api/v2/spp/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=ministry-of-agriculture&client_secret=your-secret-key-here
Method 3: JSON body
POST /api/v2/spp/oauth/token
Content-Type: application/json
{
"grant_type": "client_credentials",
"client_id": "ministry-of-agriculture",
"client_secret": "your-secret-key-here"
}
Response#
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "individual:read individual:search group:read"
}
Field |
Type |
Description |
|---|---|---|
|
string |
JWT token for API requests |
|
string |
Always "Bearer" |
|
integer |
Token lifetime in seconds (default: 86400 = 24 hours) |
|
string |
Space-separated list of granted scopes |
Example: curl#
curl -X POST https://{your-domain}/api/v2/spp/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "ministry-of-agriculture",
"client_secret": "your-secret-key-here"
}'
Example: Python (requests)#
import requests
def get_access_token(client_id, client_secret, base_url):
"""Obtain an OAuth 2.0 access token."""
response = requests.post(
f"{base_url}/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": client_id,
"client_secret": client_secret
}
)
response.raise_for_status()
return response.json()["access_token"]
# Usage
token = get_access_token(
client_id="ministry-of-agriculture",
client_secret="your-secret-key-here",
base_url="https://{your-domain}/api/v2/spp"
)
print(f"Token: {token}")
Example: JavaScript (fetch)#
async function getAccessToken(clientId, clientSecret, baseUrl) {
const response = await fetch(`${baseUrl}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret
})
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.statusText}`);
}
const data = await response.json();
return data.access_token;
}
// Usage
const token = await getAccessToken(
'ministry-of-agriculture',
'your-secret-key-here',
'https://{your-domain}/api/v2/spp'
);
console.log('Token:', token);
Using the Access Token#
Include the token in the Authorization header of all API requests:
GET /api/v2/spp/Individual/urn:gov:ph:psa:national-id|PH-123456789
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Example: curl#
curl https://{your-domain}/api/v2/spp/Individual/urn:gov:ph:psa:national-id|PH-123456789 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Example: Python#
import requests
def get_individual(identifier, token, base_url):
"""Fetch an individual using an access token."""
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(
f"{base_url}/Individual/{identifier}",
headers=headers
)
response.raise_for_status()
return response.json()
# Usage
individual = get_individual(
identifier="urn:gov:ph:psa:national-id|PH-123456789",
token=token,
base_url="https://{your-domain}/api/v2/spp"
)
print(individual)
Example: JavaScript#
async function getIndividual(identifier, token, baseUrl) {
const response = await fetch(`${baseUrl}/Individual/${identifier}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}
return await response.json();
}
// Usage
const individual = await getIndividual(
'urn:gov:ph:psa:national-id|PH-123456789',
token,
'https://{your-domain}/api/v2/spp'
);
console.log(individual);
Token Management#
Token Expiration#
Access tokens expire after 24 hours (86400 seconds) by default. The lifetime is configurable per deployment. You must request a new token when the current one expires.
Note
The client credentials flow does not use refresh tokens. Instead, request a new token before the current one expires.
Token Renewal Strategy#
Implement a proactive renewal strategy that requests a new token before the current one expires:
import requests
from datetime import datetime, timedelta
class TokenManager:
"""Manage OAuth 2.0 access tokens with automatic refresh."""
def __init__(self, client_id, client_secret, base_url):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token = None
self.expires_at = None
def get_token(self):
"""Get a valid access token, requesting a new one if necessary."""
if self.token is None or self._is_expired():
self._request_new_token()
return self.token
def _is_expired(self):
"""Check if token is expired or about to expire (5 min buffer)."""
if self.expires_at is None:
return True
return datetime.now() >= self.expires_at - timedelta(minutes=5)
def _request_new_token(self):
"""Request a new access token."""
response = requests.post(
f"{self.base_url}/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
)
response.raise_for_status()
data = response.json()
self.token = data["access_token"]
expires_in = data["expires_in"]
self.expires_at = datetime.now() + timedelta(seconds=expires_in)
# Usage
token_manager = TokenManager(
client_id="ministry-of-agriculture",
client_secret="your-secret-key-here",
base_url="https://{your-domain}/api/v2/spp"
)
# Always get fresh token
token = token_manager.get_token()
Handling 401 Unauthorized#
If you receive a 401 response, your token has expired or is invalid:
def api_request_with_retry(url, token_manager):
"""Make API request with automatic token refresh on 401."""
token = token_manager.get_token()
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
if response.status_code == 401:
# Token expired, force refresh
token_manager._request_new_token()
token = token_manager.get_token()
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
Scopes#
Scopes define what your API client can access. Scopes follow the resource:action format.
Available Actions:
Action |
Description |
|---|---|
|
Read a single resource by identifier |
|
Search/list resources with filters |
|
Create a new resource |
|
Update an existing resource |
|
Delete a resource |
|
All of the above (wildcard) |
Available Resources and Required Scopes:
Resource |
Read single |
List/search |
Create |
Update |
Delete |
|---|---|---|---|---|---|
|
|
|
|
|
not supported |
|
|
|
|
|
not supported |
|
|
|
not supported |
not supported |
not supported |
|
|
|
|
|
not supported |
|
|
not applicable ² |
not via this resource |
not via this resource |
|
¹ Individual and Group search endpoints currently accept only :read; granting :search alone will produce 403. This will likely change in a future release to also accept :search.
² Consent records are read individually only. Listing all consents is not supported.
Your administrator configures which scopes your client receives. Scopes can also include field-level restrictions to limit which fields are returned.
Note
The identifier resource appears in the API client UI but is not currently checked by any endpoint. Granting identifier:* scopes has no effect.
Checking Your Scopes#
The token response includes granted scopes:
{
"access_token": "...",
"scope": "individual:read individual:search group:read"
}
Scope Errors#
If you attempt an operation without the required scope:
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"detail": "Required scope: individual:create. Granted: individual:read"
}
Security Best Practices#
Protect Your Client Secret#
Never expose your client secret in:
Public repositories (GitHub, GitLab, etc.)
Client-side code (JavaScript in browsers)
Log files or error messages
Environment variables in CI/CD without encryption
Store secrets securely:
# ✅ Good: Read from environment variable
import os
client_secret = os.environ["OPENSPP_CLIENT_SECRET"]
# ✅ Good: Use secrets management service
from aws_secretsmanager import get_secret
client_secret = get_secret("openspp/client_secret")
# ❌ Bad: Hardcoded
client_secret = "abc123secret" # Never do this!
Rotate Credentials Regularly#
Contact your administrator to rotate your client secret periodically (e.g., every 90 days).
Use HTTPS Only#
Always use HTTPS endpoints in production. Never send credentials or tokens over HTTP.
# ✅ Good
base_url = "https://{your-domain}/api/v2/spp"
# ❌ Bad (development only)
base_url = "http://localhost:8069/api/v2/spp"
Limit Token Lifetime#
Tokens expire after 24 hours by default. This limits the exposure window if a token is compromised.
Monitor for Unauthorized Usage#
Check API logs regularly for:
Failed authentication attempts
Access from unexpected IP addresses
Unusual request patterns
JWT Token Details#
Access tokens are JSON Web Tokens (JWT) signed with HS256:
Field |
Value |
|---|---|
Algorithm |
HS256 (HMAC-SHA256) |
Issuer ( |
|
Audience ( |
|
Default lifetime |
24 hours (configurable) |
JWT Payload:
{
"iss": "openspp-api-v2",
"sub": "ministry-of-agriculture",
"aud": "openspp",
"exp": 1732867200,
"iat": 1732780800,
"client_id": "ministry-of-agriculture",
"scopes": ["individual:read", "individual:search", "group:read"]
}
Rate Limiting#
Bucket |
Per Minute |
Per Day |
Notes |
|---|---|---|---|
|
5 |
50 |
Hard-coded brute-force protection |
Authenticated API (per client) |
60 (default) |
10,000 (default) |
Configurable per client via |
Unauthenticated API (per IP fallback) |
30 |
5,000 |
Hard-coded fallback when no client is identified |
When rate-limited, responses include Retry-After and X-RateLimit-* headers. See Error Handling for details.
Error Responses#
401 Unauthorized#
Returned when:
The access token is missing, malformed, or expired
Client credentials in the token request are invalid
{
"detail": "Invalid client credentials"
}
Solution: Verify your client ID and secret. For expired access tokens, request a new one.
400 Bad Request#
Returned when the token request itself is malformed — e.g., unsupported grant_type or unparseable body:
{
"detail": "Unsupported grant_type. Only 'client_credentials' is supported."
}
Solution: Check your request body. Only grant_type=client_credentials is supported.
403 Forbidden#
Valid token, but insufficient permissions:
{
"detail": "Missing required scope 'individual:create'"
}
Solution: Request additional scopes from your administrator.
Common mistakes#
Getting "invalid_client" error?
Double-check your client ID and secret. Ensure there are no extra spaces or line breaks when copying credentials. The token endpoint supports HTTP Basic Auth, form-encoded body, and JSON body — try a different method if one isn't working.
Token request hangs or times out?
Check your network connection and firewall rules. Ensure you can reach the OAuth server endpoint.
Getting 401 on all requests despite valid token?
Your API client may be disabled. Contact your administrator to verify your client is active.
Scopes in token don't match what you expected?
Scopes are configured server-side. Contact your administrator to update your client's scope configuration.
Complete Example#
import requests
from datetime import datetime, timedelta
class OpenSPPClient:
"""Complete OpenSPP API client with authentication."""
def __init__(self, client_id, client_secret, base_url):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url
self.token = None
self.expires_at = None
def _get_token(self):
"""Get a valid access token."""
if self.token is None or self._is_expired():
self._request_new_token()
return self.token
def _is_expired(self):
"""Check if token needs refresh."""
if self.expires_at is None:
return True
return datetime.now() >= self.expires_at - timedelta(minutes=5)
def _request_new_token(self):
"""Request new access token."""
response = requests.post(
f"{self.base_url}/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
)
response.raise_for_status()
data = response.json()
self.token = data["access_token"]
self.expires_at = datetime.now() + timedelta(seconds=data["expires_in"])
def _request(self, method, path, **kwargs):
"""Make authenticated API request."""
token = self._get_token()
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {token}"
response = requests.request(
method,
f"{self.base_url}/{path}",
headers=headers,
**kwargs
)
# Retry once on 401
if response.status_code == 401:
self._request_new_token()
token = self._get_token()
headers["Authorization"] = f"Bearer {token}"
response = requests.request(
method,
f"{self.base_url}/{path}",
headers=headers,
**kwargs
)
response.raise_for_status()
return response.json()
def get_individual(self, identifier):
"""Fetch an individual by identifier."""
return self._request("GET", f"Individual/{identifier}")
def search_individuals(self, **params):
"""Search for individuals."""
return self._request("GET", "Individual", params=params)
def create_individual(self, data):
"""Create a new individual."""
return self._request("POST", "Individual", json=data)
# Usage
client = OpenSPPClient(
client_id="ministry-of-agriculture",
client_secret="your-secret-key-here",
base_url="https://{your-domain}/api/v2/spp"
)
# Fetch individual
individual = client.get_individual("urn:gov:ph:psa:national-id|PH-123456789")
print(individual)
# Search
results = client.search_individuals(name="Santos")
print(f"Found {results['total']} individuals")
What's next#
API Resources - Learn about available API resources
Search and Filtering - Advanced search and filtering
Consent Management - Understanding consent-based access control
Error Handling - Complete error handling guide
See also#
OAuth 2.0 RFC 6749 - OAuth standard
JWT RFC 7519 - JSON Web Tokens
openspp.org