.ai/skills/woocommerce-store-api/authentication.md
The Store API has its own authentication model distinct from the rest of the WP REST API. Read this before adding any new route, especially one that mutates state.
Authentication::check_authentication() is hooked on rest_authentication_errors for every /wc/store/v1/* request. It deliberately returns true to override WP's default cookie-nonce check, so WP's built-in CSRF protection does not apply to Store API routes.
// plugins/woocommerce/src/StoreApi/Authentication.php
public function check_authentication( $result ) {
// Enable Rate Limiting for logged-in users without 'edit posts' capability.
if ( ! current_user_can( 'edit_posts' ) ) {
$result = $this->apply_rate_limiting( $result );
}
return ! empty( $result ) ? $result : true;
}
The class docblock literally says "The Store API does not require authentication" — meaning it doesn't enforce nonces or capabilities globally. Each route is responsible for its own auth model.
AbstractCartRouteState-changing routes get their CSRF protection from AbstractCartRoute::check_nonce(), which:
get_response(), gated by requires_nonce() — which returns true on non-GET requests that don't carry a valid Cart-Token header. Cart-token-bearing requests are authenticated via the token instead and skip the nonce check.Nonce header against the wc_store_api action inside check_nonce().401 woocommerce_rest_missing_nonce or 403 woocommerce_rest_invalid_nonce.Nonce response header on every response (set in add_response_headers()) that the client echoes back on the next state-changing request.Routes that extend AbstractRoute directly do not get this. They will accept any logged-in cookie session without a nonce check, which is a real CSRF surface.
| Use case | Extend | Why |
|---|---|---|
| Read-only routes (catalog, public data) | AbstractRoute | No state to protect from CSRF. |
| Cart-related state mutation | AbstractCartRoute | Existing precedent; nonce + cart-session machinery wired up. |
| Login-required mutation routes (per-user preferences, account-scoped writes) | AbstractCartRoute, or implement equivalent nonce protection on AbstractRoute | Mutation via cookie auth without CSRF protection is unacceptable. |
| Read routes with own auth model (e.g. order ownership) | AbstractRoute + custom permission_callback | The auth check is the protection. |
If you find yourself extending AbstractRoute for a route that POSTs/DELETEs based on cookie auth, stop and reconsider. Either inherit from AbstractCartRoute, or document and implement an equivalent nonce flow.
permission_callback conventions| Use case | Pattern | Reference |
|---|---|---|
| Guest-accessible | '__return_true' | Cart.php, most cart routes |
| Login-required | function () { return is_user_logged_in(); } | Patterns.php |
| Owner-only access | [ $this, 'is_authorized' ] | Order.php |
Don't use bare callable strings like 'is_user_logged_in'. They work but diverge from the codebase convention. Reviewers will look for the closure form. The closure also gives you a place to add capability checks later without changing the callback type.
// ❌ Don't
'permission_callback' => 'is_user_logged_in',
// ✅ Do
'permission_callback' => function () {
return is_user_logged_in();
},
Store API routes accept two auth methods, and they have different testing implications:
WC()->cart (e.g., cart_item_key lookups). Subject to nonce enforcement on cart-route mutations.If a route needs to read WC()->cart->cart_contents, document that Application Password testing won't work for it — clients must use cookie auth.
permission_callback => '__return_true' and extend AbstractRoute. No auth, no nonce — anyone can mutate. Only acceptable for guest carts where the user-id boundary is the cart token.AbstractRoute with login-only auth and no nonce. A logged-in shopper visiting a malicious site can be silently POSTed at via their cookie. Use AbstractCartRoute or implement equivalent nonce checks.permission_callback. Stylistically inconsistent and makes future capability-check additions awkward.Authentication::check_authentication() directly. It's an internal filter, not an API. Use the abstract base classes.For any route requiring auth, add tests covering:
woocommerce_rest_missing_nonce.woocommerce_rest_invalid_nonce.