A mobile app should not pretend to be a browser. It needs an authentication flow designed for API clients: submit credentials over HTTPS, receive a short-lived token, store it in the platform’s secure storage, and attach it to each protected request.
JSON API User Plus 5.0.4 adds that flow to WordPress through the userplus controller. It is JWT-only: the old cookie endpoints are intentionally absent. The result is one predictable session model for iOS, Android, Flutter, React Native, PWAs, and headless frontends.
What you will build
The flow has four parts:
- The app posts a username or email and password to
/api/userplus/login/. - WordPress authenticates the account and returns a signed JWT plus the private user object.
- The app stores the token in Keychain, Android Keystore-backed storage, or an equivalent secure vault.
- Protected calls send
Authorization: Bearer TOKEN.
The package returns the token in the JSON field named token. Some older examples and marketing copy use the generic term “access token”; when parsing User Plus 5.0.4, read token.
1. Prepare WordPress
Install and activate RESTful JSON API, then install JSON API User Plus. In RESTful JSON API → Settings → Controllers, enable the required controllers. Open the User Plus settings page, enter and activate the license key, and confirm that UserPlus is available.
Use the public discovery endpoint as a quick health check:
curl "https://example.com/api/userplus/info/"
Version 5.0.4 reports JWT authentication and the userplus, postsplus, and buddypress controllers. Your production site must use HTTPS. The login controller explicitly enforces HTTPS for authentication.
2. Request a JWT
Send credentials as a POST request. username accepts either the WordPress login name or an email address; you can alternatively send the same email value under email.
curl -X POST "https://example.com/api/userplus/login/" \ --data-urlencode "username=alex@example.com" \ --data-urlencode "password=correct-horse-battery-staple"
A successful response has this shape:
{
"token": "eyJ…",
"token_type": "Bearer",
"expires": 1770000000,
"expires_in": 86400,
"user": {
"id": 42,
"username": "alex",
"email": "alex@example.com",
"display_name": "Alex",
"roles": ["subscriber"],
"avatar": "https://…"
}
}
The default lifetime is one day. The endpoint also accepts an optional seconds parameter, but the server—not the mobile UI—should own your session policy. Avoid turning long-lived bearer tokens into permanent passwords.
Incorrect credentials produce a 401 response with a deliberately generic message. Show a friendly “Email or password is incorrect” message instead of revealing whether an account exists.
3. Store the token safely
Do not place a JWT in plain preferences, a local SQLite row, logs, analytics events, crash reports, or source code. Use:
- iOS: Keychain Services, normally with an accessibility level appropriate to your background-use requirements.
- Android: encrypted storage backed by Android Keystore.
- Flutter: a secure-storage package that delegates to Keychain and Keystore.
Store the token and its expiry. The returned user object can seed the interface, but treat the server as authoritative and refresh profile data after restoring a session.
JWT logout is usually local: delete the stored token and clear private cached data. A JWT already issued by the server remains valid until expiry unless the parent API implements a separate revocation strategy, so shorter lifetimes reduce exposure.
4. Call a protected endpoint
Attach the token as an HTTP header—not a query parameter:
curl "https://example.com/api/userplus/me/" \ -H "Authorization: Bearer eyJ..."
The same header protects profile updates, user-meta writes, password changes, account deletion, PostsPlus writes, and BuddyPress actions.
A small client wrapper keeps this consistent:
async function api(path: string, init: RequestInit = {}) {
const token = await secureStore.get('userPlusToken');
const headers = new Headers(init.headers);
headers.set('Accept', 'application/json');
if (token) headers.set('Authorization', `Bearer ${token}`);
const response = await fetch(`https://example.com/api/${path}/`, {
...init,
headers,
});
const data = await response.json();
if (response.status === 401) {
await secureStore.delete('userPlusToken');
throw new Error('SESSION_EXPIRED');
}
if (!response.ok) throw new Error(data.error ?? 'REQUEST_FAILED');
return data;
}
5. Restore and validate a session
At app launch, read the stored token and call:
curl "https://example.com/api/userplus/validate_token/" \ -H "Authorization: Bearer eyJ..."
A valid token returns valid: true, the user ID, and the private user object. An absent, invalid, or expired token returns valid: false with a message. Route the user to the authenticated area only after validation; on failure, remove the stale token and show the login screen.
This server check matters because decoding a JWT locally only reveals its claims. It does not prove that the signature is valid under the WordPress server’s current secret or that the user still exists.
6. Update the profile
After login, a typical profile screen reads /api/userplus/me/ and posts selected fields to /api/userplus/update_user/. Send only fields the user changed. User Plus supports display name, first name, last name, email, description, and URL.
Custom app preferences belong in user meta:
curl -X POST "https://example.com/api/userplus/set_meta_many/" \ -H "Authorization: Bearer eyJ..." \ -d "custom_fields[theme]=dark" \ -d "custom_fields[notifications]=enabled"
The controller blocks sensitive WordPress keys such as capabilities, user level, session tokens, and application passwords. That is a useful boundary, but your app should still maintain an allowlist of meta keys it knows how to use.
Production checklist
- Enforce HTTPS everywhere and reject clear-text traffic in the app.
- Keep JWTs out of logs, URLs, screenshots, and analytics.
- Store bearer tokens only in platform secure storage.
- Handle offline, timeout, 401, 403, and malformed-response states separately.
- Clear private caches whenever the user logs out or the session expires.
- Use WordPress roles and capabilities for authorization; a valid token does not imply permission to perform every action.
- Rate-limit login attempts at the web server, CDN, WAF, or security-plugin layer.
- Test email login, username login, wrong credentials, expired tokens, deleted users, and clock differences.
The final architecture
User Plus keeps the contract compact: login creates the session, validate_token restores it, and the Bearer header authenticates every protected call. Once this wrapper is in place, registration, profiles, meta, SSO, PostsPlus, and BuddyPress all share the same JWT session—exactly what a native WordPress app needs.







