Tutorial¶
This tutorial will guide you through integrating django-indieweb into your Django project to add IndieAuth authentication, Micropub content creation, and Webmention support.
Note
Micropub ships with an in-memory handler for quick testing; configure INDIEWEB_MICROPUB_HANDLER
to store content in your own models.
Prerequisites¶
Django project up and running
Python 3.10 or higher
Basic understanding of OAuth-like flows
Installation¶
Install the package:
pip install django-indieweb
Add
indiewebto yourINSTALLED_APPS:INSTALLED_APPS = [ ... 'indieweb', ]
Include the URLs in your project’s
urls.py:from django.urls import path, include urlpatterns = [ ... path('indieweb/', include('indieweb.urls', namespace='indieweb')), ]
Run migrations:
python manage.py migrate
Basic Setup¶
After installation, you’ll have these endpoints available:
/indieweb/auth/- IndieAuth authorization (consent)/indieweb/token/- Token exchange/indieweb/micropub/- Micropub content creation (uses configured handler)/indieweb/webmention/- Webmention receive endpoint (Link rel=”webmention” is advertised)/indieweb/webmention/<pk>/- Webmention status lookup
Implementing IndieAuth Login¶
Here’s how to implement IndieAuth login in your application:
Client-Side Implementation¶
Create a login form:
<form id="indieauth-form"> <input type="url" name="me" placeholder="https://yoursite.com" required> <button type="submit">Sign in with IndieAuth</button> </form>
Handle the login flow:
document.getElementById('indieauth-form').addEventListener('submit', (e) => { e.preventDefault(); const me = e.target.me.value; const client_id = window.location.origin; const redirect_uri = window.location.origin + '/auth/callback'; const state = Math.random().toString(36).substring(2, 15); const scope = 'create'; // Store state for verification sessionStorage.setItem('indieauth_state', state); // Redirect to authorization endpoint const authUrl = new URL('/indieweb/auth/', window.location.origin); authUrl.searchParams.append('me', me); authUrl.searchParams.append('client_id', client_id); authUrl.searchParams.append('redirect_uri', redirect_uri); authUrl.searchParams.append('state', state); authUrl.searchParams.append('scope', scope); window.location.href = authUrl.toString(); });
Handle the callback:
// On your callback page const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); const state = urlParams.get('state'); const me = urlParams.get('me'); // Verify state if (state !== sessionStorage.getItem('indieauth_state')) { alert('Invalid state parameter'); return; } // Exchange code for token fetch('/indieweb/token/', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ code: code, client_id: window.location.origin, redirect_uri: window.location.origin + '/auth/callback', me: me, scope: 'create' }) }) .then(response => response.text()) .then(data => { const params = new URLSearchParams(data); const access_token = params.get('access_token'); // Store the token securely localStorage.setItem('micropub_token', access_token); localStorage.setItem('micropub_me', params.get('me')); });
Using the Micropub Endpoint¶
Once you have an access token, you can make requests to the Micropub endpoint:
Verifying Token¶
fetch('/indieweb/micropub/', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('micropub_token')
}
})
.then(response => response.text())
.then(data => {
console.log('Token valid for:', new URLSearchParams(data).get('me'));
});
Creating a Post¶
fetch('/indieweb/micropub/', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('micropub_token'),
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
h: 'entry',
content: 'Hello from Micropub!',
category: 'test,micropub'
})
})
.then(response => {
if (response.status === 201) {
console.log('Post created! Location:', response.headers.get('Location'));
}
});
Extending the Micropub Endpoint¶
To persist content, configure your own handler:
# settings.py
INDIEWEB_MICROPUB_HANDLER = "myapp.micropub_handler.BlogPostMicropubHandler"
See Micropub Implementation Guide for a full handler example (create, retrieve, update, delete stubs). The default in-memory handler implements source queries, update, delete, and undelete in process memory only — entries do not persist across restarts. Configure your own handler for durable storage.
# myapp/micropub_handler.py
from indieweb.handlers import MicropubContentHandler, MicropubEntry
from myapp.models import BlogPost
class BlogPostMicropubHandler(MicropubContentHandler):
def create_entry(self, properties, user):
post = BlogPost.objects.create(
author=user,
content=properties.get("content", [""])[0],
)
return MicropubEntry(url=post.get_absolute_url(), properties=properties)
def update_entry(self, url, updates, user):
raise NotImplementedError("Update not yet implemented")
def delete_entry(self, url, user):
raise NotImplementedError("Delete not yet implemented")
def undelete_entry(self, url, user):
raise NotImplementedError("Undelete not supported")
def get_entry(self, url, user):
return None
Then include the bundled URLconf:
# urls.py
from django.urls import include, path
urlpatterns = [
path("indieweb/", include("indieweb.urls")),
# ... other patterns
]
Server-Side Token Validation¶
For server-side applications, you might want to validate tokens:
from indieweb.models import Token
def validate_token(request):
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if auth_header.startswith('Bearer '):
key = auth_header.split()[1]
try:
token = Token.objects.get(key=key)
if not token.owner.is_active:
return None
if token.is_expired():
return None
return token
except Token.DoesNotExist:
pass
return None
Security Considerations¶
Always verify the state parameter to prevent CSRF attacks
Use HTTPS in production for all endpoints
Store tokens securely - consider using session storage instead of localStorage
Tune token lifetime via the
INDIEWEB_TOKEN_EXPIRES_INsetting (default 86400 seconds); see ConfigurationValidate redirect_uri matches registered client applications
Debugging Tips¶
Check Django logs for authentication failures
Verify all required parameters are present
Ensure user is logged in before authorization
Check that authorization codes are used within 60 seconds
Verify scope requirements match token permissions
Common Issues¶
- “Missing parameter” error
Ensure all required parameters are included in the request
- 401 Unauthorized on token endpoint
Authorization code may have expired (60 second timeout)
Code may have already been used
Parameters don’t match original auth request
- 403 Forbidden on micropub endpoint
Token doesn’t have the scope required for the requested operation. Create requires
createor the legacy aliaspost; source and update requireupdate; delete requiresdelete; undelete requiresundelete.- Redirect loops
Check that login redirect URLs are properly configured in Django settings
Adding User Profiles with H-Cards¶
H-cards allow you to add rich profile information for your users:
Create a profile for a user:
from django.contrib.auth import get_user_model from indieweb.models import Profile User = get_user_model() user = User.objects.get(username="alice") Profile.objects.create( user=user, name="Alice Johnson", photo_url="https://example.com/alice.jpg", url="https://example.com/alice", h_card={ "name": ["Alice Johnson"], "photo": ["https://example.com/alice.jpg"], "url": ["https://example.com/alice"], "email": ["alice@example.com"], "note": ["Web developer and blogger"] } )
Display the h-card on your homepage:
{# templates/home.html #} {% load indieweb_tags %} <\!DOCTYPE html> <html> <head> <title>{{ user.username }} - Homepage</title> {% webmention_endpoint_link %} </head> <body> <header> {% h_card user %} </header> <main> <\!-- Your content here --> </main> </body> </html>
The h-card will render with proper microformats2 markup, making your profile machine-readable for other IndieWeb tools and services.
See H-Cards for more details on h-card properties and customization.