Authentication¶
The Book Corners API uses JWT (JSON Web Tokens) for authentication. Authenticated endpoints require a Bearer token in the Authorization header.
Token lifecycle¶
- Register or Login to receive an access/refresh token pair
- Use the access token for API requests (
Authorization: Bearer <access>) - When the access token expires, use the refresh token to get a new one
- When the refresh token expires, log in again
Access tokens are short-lived (5 minutes). Refresh tokens last 365 days, so clients that store the refresh token securely only need to ask the user to log in again after the refresh token expires or after an explicit logout.
Endpoints¶
The Contributor Agreement fields below are additive during the compatibility rollout. API credential registration and native social-account creation continue accepting legacy requests that omit them while CONTRIBUTOR_AGREEMENT_REGISTRATION_REQUIRED=false (the default). Valid explicit acceptance is recorded immediately. Enforcement must be enabled only after supported web and app clients have shipped the new flows.
Web password registration now requires explicit acceptance of the current agreement. Google and Apple buttons on the registration page preserve that signup intent through OAuth and record acceptance only when a new account is created. Social OAuth started from the login page continues to log in existing users without prompting or recording acceptance; a new identity is directed to the registration page instead. This web behavior does not enable API or native-app registration enforcement.
Social Login¶
POST /api/v1/auth/social
Exchange a native Apple or Google identity token for a JWT token pair. Designed for iOS/Android apps that authenticate via native SDKs (Sign in with Apple, Google Sign-In).
On first sign-in, a new user account records Contributor Agreement acceptance when the request explicitly sends the current version and true. During the default compatibility rollout, legacy requests without acceptance still create an unaccepted account; after enforcement is deliberately enabled, missing, false, or stale acceptance is rejected. If the email matches an existing account, the social identity is linked without requiring or recording acceptance. Subsequent logins return tokens for the existing user. The response includes account_created so clients can distinguish these paths.
Auth required: No
| Field | Type | Required | Description |
|---|---|---|---|
provider |
string | Yes | Social provider: "apple" or "google" |
id_token |
string | Yes | Identity token JWT from the native SDK (min 20 characters) |
first_name |
string | No | First name (Apple only provides on first sign-in, max 150 characters) |
last_name |
string | No | Last name (Apple only provides on first sign-in, max 150 characters) |
contributor_agreement_version |
string | No during rollout; required after activation | Exact current version, currently "1.0" |
contributor_agreement_accepted |
boolean | No during rollout; required after activation | JSON boolean true; strings and numbers are not accepted |
let body: [String: Any] = [
"provider": "apple",
"id_token": identityToken,
"first_name": fullName?.givenName ?? "",
"last_name": fullName?.familyName ?? "",
"contributor_agreement_version": "1.0",
"contributor_agreement_accepted": true
]
var request = URLRequest(url: URL(string: "https://bookcorners.org/api/v1/auth/social")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
Success (200 OK):
{
"access": "eyJhbGciOiJIUzI1NiIs...",
"refresh": "eyJhbGciOiJIUzI1NiIs...",
"account_created": true
}
account_created is false when an existing linked identity is logged in or a verified social email is linked to an existing account.
Errors:
| Status | Message |
|---|---|
400 |
"Unsupported provider. Use 'apple' or 'google'." |
400 |
"Invalid identity token." |
400 |
"Contributor agreement acceptance is required." when enforcement is enabled |
400 |
"Contributor agreement acceptance must be true." when enforcement is enabled |
400 |
"Contributor agreement version is not current." when enforcement is enabled |
429 |
"Too many social login attempts. Please try again later." |
Register¶
POST /api/v1/auth/register
Create a new user account and receive a token pair. The agreement fields are optional while backward-compatible rollout mode is active. A valid current acceptance is recorded when supplied; an omitted, false, or stale value creates an unaccepted account until enforcement is explicitly enabled.
Auth required: No
| Field | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | Unique username (3–150 characters) |
email |
string | Yes | Valid email address |
password |
string | Yes | Password (8–128 characters, validated against Django password policies) |
contributor_agreement_version |
string | No during rollout; required after activation | Exact current version, currently "1.0" |
contributor_agreement_accepted |
boolean | No during rollout; required after activation | JSON boolean true; strings and numbers are not accepted |
Success (201 Created):
Errors:
| Status | Message |
|---|---|
400 |
"Username already exists." |
400 |
"Email already exists." |
400 |
"Provide a valid email address." |
400 |
Password policy violation message |
400 |
"Contributor agreement acceptance is required." when enforcement is enabled |
400 |
"Contributor agreement acceptance must be true." when enforcement is enabled |
400 |
"Contributor agreement version is not current." when enforcement is enabled |
422 |
Agreement value is present but not a JSON boolean |
429 |
"Too many registration attempts. Please try again later." |
Login¶
POST /api/v1/auth/login
Authenticate with credentials and receive a token pair.
The username field accepts either a username or an email address. When an email
is provided, the server resolves it to the corresponding account (case-insensitive lookup).
This matches the web login flow.
Auth required: No
| Field | Type | Required | Description |
|---|---|---|---|
username |
string | Yes | Username or email address |
password |
string | Yes | Account password |
# Login by username
resp = requests.post(
"https://bookcorners.org/api/v1/auth/login",
json={"username": "janedoe", "password": "s3cure!Pass"},
)
tokens = resp.json()
# Login by email (same field, same endpoint)
resp = requests.post(
"https://bookcorners.org/api/v1/auth/login",
json={"username": "jane@example.com", "password": "s3cure!Pass"},
)
tokens = resp.json()
Success (200 OK):
Errors:
| Status | Message |
|---|---|
401 |
"Invalid credentials." |
429 |
"Too many login attempts. Please try again later." |
Refresh¶
POST /api/v1/auth/refresh
Exchange a valid refresh token for a new access token.
Auth required: No
| Field | Type | Required | Description |
|---|---|---|---|
refresh |
string | Yes | Refresh token from login or registration |
Success (200 OK):
Errors:
| Status | Message |
|---|---|
401 |
"Invalid or expired refresh token." |
429 |
"Too many refresh attempts. Please try again later." |
Me¶
GET /api/v1/auth/me
Return the profile of the currently authenticated user.
Auth required: Yes (Bearer token)
Success (200 OK):
{
"id": 1,
"username": "janedoe",
"email": "jane@example.com",
"is_social_only": false,
"is_staff": false,
"contributor_agreement": {
"current_version": "1.0",
"agreement_url": "https://bookcorners.org/contributor-agreement/1.0/en/",
"is_current": true
}
}
| Field | Type | Description |
|---|---|---|
id |
integer | Unique user identifier |
username |
string | Username |
email |
string | Email address |
is_social_only |
boolean | true when the account uses social login only (Apple/Google) and has no local password. Email and password change endpoints are unavailable for these accounts. |
is_staff |
boolean | true when the account can access staff-only moderation endpoints. |
contributor_agreement.current_version |
string | Current deployed agreement version. |
contributor_agreement.agreement_url |
string | Absolute URL for the immutable current agreement copy. |
contributor_agreement.is_current |
boolean | Whether this user accepted the current version. Missing or older acceptance returns false. |
Errors:
| Status | Message |
|---|---|
401 |
Unauthorized (missing or invalid token) |
Accept Contributor Agreement¶
POST /api/v1/auth/me/contributor-agreement
Record explicit acceptance of the current Contributor Agreement for the authenticated user. Existing users and users created through an older flow can use this endpoint. Repeating the same request is idempotent and does not change the original timestamp or channel.
The endpoint accepts only the exact current version and the JSON boolean true. The server derives the user, timestamp, and audit channel; fields such as user_id, accepted_at, and channel are never accepted from the client.
Auth required: Yes (Bearer token)
| Field | Type | Required | Description |
|---|---|---|---|
contributor_agreement_version |
string | Yes | Exact current version, currently "1.0" |
contributor_agreement_accepted |
boolean | Yes | Must be the JSON boolean true |
Success (200 OK):
{
"current_version": "1.0",
"agreement_url": "https://bookcorners.org/contributor-agreement/1.0/en/",
"is_current": true
}
Errors:
| Status | Message |
|---|---|
400 |
"Contributor agreement acceptance must be true." |
400 |
"Contributor agreement version is not current." |
401 |
Unauthorized (missing or invalid token) |
422 |
Missing or non-boolean agreement fields |
429 |
"Too many requests. Please try again later." |
Register Device Token¶
POST /api/v1/auth/devices
Register an APNs device token for the authenticated user. iOS clients should call this after login, after receiving push permission, and whenever iOS returns a new device token.
If the same token is already registered, the API reassigns it to the current user and marks it active. This supports reinstall, account switching, and token refresh flows.
Auth required: Yes (Bearer token)
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | APNs device token from iOS |
environment |
string | Yes | sandbox for development tokens, or production for distributed builds |
let body = [
"token": deviceTokenHex,
"environment": isDebugBuild ? "sandbox" : "production"
]
var request = URLRequest(url: URL(string: "https://bookcorners.org/api/v1/auth/devices")!)
request.httpMethod = "POST"
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONSerialization.data(withJSONObject: body)
Success (201 Created):
{
"token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"environment": "sandbox",
"is_active": true
}
Errors:
| Status | Message |
|---|---|
400 |
"Device token is required." |
401 |
Unauthorized (missing or invalid token) |
422 |
Validation error for unsupported environment |
429 |
"Too many requests. Please try again later." |
Unregister Device Token¶
DELETE /api/v1/auth/devices/{token}
Remove a device token for the authenticated user. iOS clients should call this on logout before discarding the JWT. The endpoint is idempotent and returns 204 No Content even when the token is already absent.
Auth required: Yes (Bearer token)
Success (204 No Content)
Errors:
| Status | Message |
|---|---|
401 |
Unauthorized (missing or invalid token) |
429 |
"Too many requests. Please try again later." |
Change Email¶
PATCH /api/v1/auth/me/email
Update the authenticated user's email address. The new email must be a valid, unique address.
Not available for social-only accounts (Apple/Google sign-in without a local password). Returns 403.
Auth required: Yes (Bearer token)
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | New email address (3–254 characters) |
Success (200 OK):
{
"id": 1,
"username": "janedoe",
"email": "new@example.com",
"is_social_only": false,
"is_staff": false,
"contributor_agreement": {
"current_version": "1.0",
"agreement_url": "https://bookcorners.org/contributor-agreement/1.0/en/",
"is_current": true
}
}
Errors:
| Status | Message |
|---|---|
400 |
"Provide a valid email address." |
400 |
"This is already your current email address." |
400 |
"Email already exists." |
401 |
Unauthorized (missing or invalid token) |
403 |
"Social login accounts cannot change their email address." |
Change Password¶
PUT /api/v1/auth/me/password
Change the authenticated user's password. Requires the current password for verification.
Not available for social-only accounts (Apple/Google sign-in without a local password). Returns 403.
Auth required: Yes (Bearer token)
| Field | Type | Required | Description |
|---|---|---|---|
current_password |
string | Yes | Current account password |
new_password |
string | Yes | New password (8–128 characters, validated against Django password policies) |
new_password_confirm |
string | Yes | New password confirmation (must match new_password) |
Success (200 OK):
Errors:
| Status | Message |
|---|---|
400 |
"Current password is incorrect." |
400 |
"New passwords do not match." |
400 |
Password policy violation message |
401 |
Unauthorized (missing or invalid token) |
403 |
"Social login accounts cannot change their password." |
Delete Account¶
DELETE /api/v1/auth/me
Permanently delete the authenticated user's account. This action is irreversible. Submitted libraries, reports, and photos are preserved with their author unlinked.
Regular users must provide their current password. Social-only users (no local password) must set confirm to true instead.
Auth required: Yes (Bearer token)
| Field | Type | Required | Description |
|---|---|---|---|
password |
string | Conditional | Current account password (required for non-social accounts) |
confirm_text |
string | Conditional | Must be "DELETE" (required for social-only accounts that have no password) |
# Regular account
resp = requests.delete(
"https://bookcorners.org/api/v1/auth/me",
headers={"Authorization": f"Bearer {access_token}"},
json={"password": "s3cure!Pass"},
)
# Social-only account
resp = requests.delete(
"https://bookcorners.org/api/v1/auth/me",
headers={"Authorization": f"Bearer {access_token}"},
json={"confirm_text": "DELETE"},
)
Success (200 OK):
Errors:
| Status | Message |
|---|---|
400 |
"Incorrect password." |
400 |
"Password is required." |
400 |
"Send confirm_text set to 'DELETE' to delete your account." |
401 |
Unauthorized (missing or invalid token) |