Skip to main content

— Technical

How to get customer ID from a bearer token in Magento 2 Web API

15 May 2024 · 5 min read

magento api authentication php

When you’re building a custom Web API endpoint in Magento 2, there are situations where you need to identify the calling customer from the request itself rather than relying on Magento’s ACL to inject the context. This comes up with endpoints that mix authenticated and unauthenticated access, or when you’re implementing middleware-style logic that needs the customer identity before Magento’s normal request flow resolves it.

The wrong approach is to parse the bearer token yourself. The correct approach is to use the interfaces Magento provides for exactly this purpose.

What not to do

Manually base64-decoding a JWT or doing string operations on the Authorization header is fragile, will break across Magento versions, and doesn’t account for token invalidation. I’ve seen this pattern in production more than once and it always causes problems eventually.

The correct implementation

<?php
declare(strict_types=1);

namespace YourVendor\YourModule\Model\Webapi;

use Magento\Framework\Exception\AuthorizationException;
use Magento\Integration\Api\Exception\UserTokenException;
use Magento\Integration\Api\UserTokenReaderInterface;
use Magento\Integration\Api\UserTokenValidatorInterface;
use Magento\Framework\Webapi\Request;

class CustomerIdResolver
{
    public function __construct(
        private readonly Request $request,
        private readonly UserTokenReaderInterface $userTokenReader,
        private readonly UserTokenValidatorInterface $userTokenValidator
    ) {}

    /**
     * Returns customer ID from bearer token, null if not authenticated.
     *
     * @throws AuthorizationException if token is present but structurally invalid
     */
    public function resolve(): ?int
    {
        $authHeader = $this->request->getHeader('Authorization');

        if (!$authHeader) {
            return null;
        }

        $parts = explode(' ', $authHeader);

        if (count($parts) !== 2 || strtolower($parts[0]) !== 'bearer') {
            return null;
        }

        $bearerToken = $parts[1];

        try {
            $token = $this->userTokenReader->read($bearerToken);
        } catch (UserTokenException $e) {
            throw new AuthorizationException(__($e->getMessage()));
        }

        try {
            $this->userTokenValidator->validate($token);
        } catch (AuthorizationException) {
            // Token is expired or revoked — treat as unauthenticated
            return null;
        }

        return (int) $token->getUserContext()->getUserId();
    }
}

What each piece does

UserTokenReaderInterface::read() decodes the token and returns a UserToken object. This is where structural validation happens — if the token is malformed or can’t be decoded, it throws UserTokenException. I re-throw this as AuthorizationException because a malformed token should be treated as an auth failure, not a silent null.

UserTokenValidatorInterface::validate() checks whether the token is still valid — not expired, not revoked. An expired or revoked token is a legitimate state that doesn’t warrant an exception in all contexts, so I catch AuthorizationException here and return null rather than propagating it. Whether you propagate or swallow depends on your endpoint’s contract.

getUserContext()->getUserId() returns the customer ID as a string. Cast to int — it’s always numeric for customer tokens, but the interface returns a string for flexibility across token types.

Distinguishing customer tokens from admin tokens

getUserContext()->getUserType() returns a constant that tells you who the token belongs to:

use Magento\Authorization\Model\UserContextInterface;

$userType = $token->getUserContext()->getUserType();

if ($userType === UserContextInterface::USER_TYPE_CUSTOMER) {
    // Customer token
} elseif ($userType === UserContextInterface::USER_TYPE_ADMIN) {
    // Admin token
}

If your endpoint can be called by both customers and admins and you need to handle them differently, this is the correct way to branch. Don’t assume that a non-null user ID means it’s a customer.

Availability note

UserTokenReaderInterface and UserTokenValidatorInterface were introduced in Magento 2.4.4. If you’re on an older version, you’ll need to use \Magento\Integration\Model\Oauth\TokenFactory directly and do more manual work. For anything still on 2.3.x, the token reading path is substantially different — worth a separate investigation if that’s your environment.

On 2.4.4+ these interfaces are stable and the approach above is the documented path.

Savan Padaliya

Savan Padaliya

Senior Engineering Consultant

← Back to writing