Getting Started
Authentication
OAuth 2.0 Authorization Code flow
Overview
LxBlog uses the OAuth 2.0 Authorization Code flow to authenticate third-party applications. This is the industry-standard protocol for delegated authorization — your application requests permission from a LxBlog user, and upon approval, receives tokens to make API calls on their behalf.
For single-page applications (SPAs) and mobile clients that cannot securely store a client secret, LxBlog supports the Proof Key for Code Exchange (PKCE) extension, which replaces the client secret with a dynamically generated code verifier.
Authorization flow
The OAuth 2.0 Authorization Code flow involves the following steps:
- Redirect to authorize — Your application redirects the user to the LxBlog authorization endpoint with the required parameters.
- User grants permission — The user reviews the requested scopes, selects which blog to authorize, and approves the request.
- Receive authorization code — LxBlog redirects back to your
redirect_uriwith a short-lived authorization code. - Exchange code for tokens — Your server exchanges the authorization code for an access token and refresh token.
- Use access token — Include the access token in the
Authorizationheader of API requests.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ │ 1. Redirect to /authorize │ │ │ │
│ Your │ ───────────────────────────> │ LxBlog │ │ LxBlog │
│ App │ │ Auth │ 2. User approves │ API │
│ │ 3. Redirect with code │ Server │ & selects blog │ │
│ │ <─────────────────────────── │ │ │ │
│ │ │ │ │ │
│ │ 4. Exchange code for tokens │ │ │ │
│ │ ───────────────────────────> │ │ │ │
│ │ ← Access + Refresh tokens │ │ │ │
│ │ │ │ │ │
│ │ 5. API request with token │ │ │ │
│ │ ─────────────────────────────────────────────────────────────> │ │
│ │ ← API response │ │ │ │
└──────────┘ └──────────┘ └──────────┘Step 1: Request authorization
Redirect the user to the LxBlog authorization endpoint with the following query parameters:
/api/oauth/authorizeRedirect the user to this URL to begin the authorization flow.
Query parameters
| Name | Type | Description |
|---|---|---|
client_id* | string | Your application's client ID. |
redirect_uri* | string | The URL to redirect to after authorization. Must match a registered redirect URI. |
response_type* | string | Must be "code" for the Authorization Code flow. |
scope* | string | Space-separated list of scopes your app requires. |
state | string | An opaque value used to prevent CSRF attacks. Recommended. |
code_challenge | string | PKCE code challenge. Required when using PKCE. |
code_challenge_method | string | Must be "S256" when using PKCE. |
https://lxblog.app/api/oauth/authorize?
client_id=your_client_id
&redirect_uri=https://yourapp.com/callback
&response_type=code
&scope=articles:read blog:read
&state=random_state_valueStep 4: Exchange code for tokens
/api/oauth/tokenExchange an authorization code for an access token and refresh token.
Request body (application/x-www-form-urlencoded)
| Name | Type | Description |
|---|---|---|
grant_type* | string | Must be "authorization_code". |
code* | string | The authorization code received from the callback. |
redirect_uri* | string | Must match the redirect_uri used in the authorization request. |
client_id* | string | Your application's client ID. |
client_secret | string | Your application's client secret. Not required when using PKCE. |
code_verifier | string | The PKCE code verifier. Required when a code_challenge was used. |
curl -X POST https://lxblog.app/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE_FROM_CALLBACK" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret"{
"access_token": "lxb_at_xxxxxxxxxxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "lxb_rt_xxxxxxxxxxxxxxxxxxxx",
"scope": "articles:read blog:read"
}Scopes
Scopes define the level of access your application requests. Users see the requested scopes during the authorization step and must approve them. Request only the scopes your application needs.
Available scopes
| Name | Type | Description |
|---|---|---|
articles:read | scope | List, get, and sync published articles. |
blog:read | scope | Read blog metadata such as name, description, and configuration. |
Token lifecycle
LxBlog issues two types of tokens, each with a distinct purpose and expiration:
- Access tokens (
lxb_at_prefix) — Used to authenticate API requests. Expire after 1 hour (3600 seconds). - Refresh tokens (
lxb_rt_prefix) — Used to obtain a new access token without re-prompting the user. Expire after 30 days.
Refresh token rotation
LxBlog enforces mandatory refresh token rotation. Each time you use a refresh token to obtain a new access token, the API returns a new refresh token and immediately invalidates the previous one. This limits the damage if a refresh token is ever compromised.
Always store the new refresh token. After each token refresh, you must persist the newly returned refresh token. If you lose it or continue using the old one, the user will need to re-authorize your application. Store refresh tokens securely — never expose them in client-side code, URLs, or logs.
/api/oauth/tokenRefresh an expired access token using a refresh token.
Request body
| Name | Type | Description |
|---|---|---|
grant_type* | string | Must be "refresh_token". |
refresh_token* | string | The current refresh token. |
client_id* | string | Your application's client ID. |
client_secret | string | Your application's client secret. Not required for PKCE clients. |
curl -X POST https://lxblog.app/api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=lxb_rt_current_refresh_token" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret"PKCE for public clients
If your application cannot securely store a client secret (such as a single-page application or mobile app), use the PKCE extension. Instead of a client secret, your app generates a random code_verifier and derives a code_challenge from it.
// 1. Generate a random code verifier
const codeVerifier = generateRandomString(128);
// 2. Derive the code challenge (SHA-256, base64url-encoded)
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await crypto.subtle.digest('SHA-256', data);
const codeChallenge = base64UrlEncode(digest);
// 3. Include in the authorization URL
const authUrl = new URL('https://lxblog.app/api/oauth/authorize');
authUrl.searchParams.set('client_id', 'your_client_id');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'articles:read');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
// 4. When exchanging the code, include the code_verifier
// instead of client_secretThe code_challenge_method must always be S256. LxBlog does not support the plain method, as it does not provide meaningful security benefits.
Token prefixes
All LxBlog tokens use prefixes to make them easy to identify and to help prevent accidental misuse:
lxb_at_— Access tokenslxb_rt_— Refresh tokens
These prefixes make it easier to identify token types in your code and allow secret scanning tools (such as GitHub's) to automatically detect and flag leaked credentials.
Error codes
The OAuth endpoints return standard error codes in the JSON response body when a request fails. The response will include an error field and an optional error_description field with additional details.
OAuth error codes
| Name | Type | Description |
|---|---|---|
invalid_grant | error | The authorization code has expired, has already been used, or the refresh token is invalid or expired. |
invalid_client | error | The client_id is not recognized, or the client_secret does not match. |
invalid_scope | error | One or more of the requested scopes are not valid or not available for this application. |
access_denied | error | The user denied the authorization request, or the application does not have permission to access the requested resource. |
{
"error": "invalid_grant",
"error_description": "The authorization code has expired."
}