Skip to content

Authentication

The Book Corners API uses JWT (JSON Web Tokens) for authentication. Authenticated endpoints require a Bearer token in the Authorization header.

Token lifecycle

  1. Register or Login to receive an access/refresh token pair
  2. Use the access token for API requests (Authorization: Bearer <access>)
  3. When the access token expires, use the refresh token to get a new one
  4. 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
curl -X POST https://bookcorners.org/api/v1/auth/social \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "apple",
    "id_token": "eyJraWQiOiJBSURPUEsxIi...",
    "first_name": "Jane",
    "last_name": "Doe",
    "contributor_agreement_version": "1.0",
    "contributor_agreement_accepted": true
  }'
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
curl -X POST https://bookcorners.org/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "janedoe",
    "email": "jane@example.com",
    "password": "s3cure!Pass",
    "contributor_agreement_version": "1.0",
    "contributor_agreement_accepted": true
  }'
import requests

resp = requests.post(
    "https://bookcorners.org/api/v1/auth/register",
    json={
        "username": "janedoe",
        "email": "jane@example.com",
        "password": "s3cure!Pass",
        "contributor_agreement_version": "1.0",
        "contributor_agreement_accepted": True,
    },
)
print(resp.json())

Success (201 Created):

{
  "access": "eyJhbGciOiJIUzI1NiIs...",
  "refresh": "eyJhbGciOiJIUzI1NiIs..."
}

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
curl -X POST https://bookcorners.org/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "janedoe",
    "password": "s3cure!Pass"
  }'
curl -X POST https://bookcorners.org/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "jane@example.com",
    "password": "s3cure!Pass"
  }'
# 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):

{
  "access": "eyJhbGciOiJIUzI1NiIs...",
  "refresh": "eyJhbGciOiJIUzI1NiIs..."
}

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
curl -X POST https://bookcorners.org/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh": "eyJhbGciOiJIUzI1NiIs..."}'
resp = requests.post(
    "https://bookcorners.org/api/v1/auth/refresh",
    json={"refresh": tokens["refresh"]},
)
new_access = resp.json()["access"]

Success (200 OK):

{
  "access": "eyJhbGciOiJIUzI1NiIs..."
}

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)

curl https://bookcorners.org/api/v1/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
resp = requests.get(
    "https://bookcorners.org/api/v1/auth/me",
    headers={"Authorization": f"Bearer {access_token}"},
)
print(resp.json())

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
curl -X POST https://bookcorners.org/api/v1/auth/me/contributor-agreement \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "contributor_agreement_version": "1.0",
    "contributor_agreement_accepted": 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
curl -X POST https://bookcorners.org/api/v1/auth/devices \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "token": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "environment": "sandbox"
  }'
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)

curl -X DELETE \
  https://bookcorners.org/api/v1/auth/devices/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

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)
curl -X PATCH https://bookcorners.org/api/v1/auth/me/email \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"email": "new@example.com"}'
resp = requests.patch(
    "https://bookcorners.org/api/v1/auth/me/email",
    headers={"Authorization": f"Bearer {access_token}"},
    json={"email": "new@example.com"},
)
print(resp.json())

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)
curl -X PUT https://bookcorners.org/api/v1/auth/me/password \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{
    "current_password": "oldPass123!",
    "new_password": "newS3cure!Pass",
    "new_password_confirm": "newS3cure!Pass"
  }'
resp = requests.put(
    "https://bookcorners.org/api/v1/auth/me/password",
    headers={"Authorization": f"Bearer {access_token}"},
    json={
        "current_password": "oldPass123!",
        "new_password": "newS3cure!Pass",
        "new_password_confirm": "newS3cure!Pass",
    },
)
print(resp.json())

Success (200 OK):

{
  "message": "Password changed successfully."
}

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)
curl -X DELETE https://bookcorners.org/api/v1/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"password": "s3cure!Pass"}'
curl -X DELETE https://bookcorners.org/api/v1/auth/me \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Content-Type: application/json" \
  -d '{"confirm_text": "DELETE"}'
# 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):

{
  "message": "Account deleted successfully."
}

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)