passbolt/passbolt_api · error · InternalErrorException

Failed to obtain SMTP OAuth2 access token.

Error message

Failed to obtain SMTP OAuth2 access token.

What it means

Thrown by SmtpOauthExchangeOnlineService::getAccessToken when the HTTP token request to Microsoft's OAuth2 token endpoint returns a non-OK response. The error_description/error from Microsoft is logged, then a generic InternalErrorException (500) is thrown.

Solutions

  1. Check the passbolt error log for 'SMTP OAuth2 token fetch failed' to read Microsoft's error_description (e.g. AADSTS code).
  2. Verify client_id/client_secret/tenant_id are correct and the client secret has not expired in Azure App registrations > Certificates & secrets.
  3. Ensure the app has the SMTP.Send (or full_access_as_app for Exchange Online) application permission with admin consent granted.
  4. Confirm the server can reach login.microsoftonline.com (outbound HTTPS/firewall/DNS).

Example fix

// before: secret expired, AADSTS7000215
throw new InternalErrorException(__('Failed to obtain SMTP OAuth2 access token.'));

// after: rotate the secret in Azure Portal, update settings, retry
// Azure Portal > App registrations > Certificates & secrets > New client secret
// then re-save smtp settings with the new secret
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: verify credentials can reach Microsoft before configuring
$resp = $http->post("https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token", [
    'grant_type' => 'client_credentials', 'client_id' => $clientId,
    'client_secret' => $secret, 'scope' => 'https://outlook.office365.com/.default',
]);
if (!$resp->isOk()) { /* fix config before wiring into passbolt */ }

Try / catch

try {
    $token = $service->getAccessToken();
} catch (InternalErrorException $e) {
    // read application logs for the AADSTS error_description and fix Azure config
    Log::error('OAuth token exchange failed; check Azure secret/consent.');
}

Prevention

When it happens

Trigger: POST to https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token fails: invalid client credentials (AADSTS7000215), wrong tenant, unauthorized client for SMTP (AADSTS700027 / missing SMTP.Send scope consent), network errors producing non-OK responses, or expired client secret.

Common situations: Expired or rotated Azure client secret; app not granted/consented to SMTP.Send or full_access_as_app permission; tenant_id GUID whose app belongs to another tenant; firewall blocking outbound calls to login.microsoftonline.com.

Related errors


AI-assisted analysis of passbolt/passbolt_api@31c1bbc10f (2026-09-17). Data as JSON: /api/errors/5c89de929b2d7f63. Report an issue: GitHub.

Appendix: source

Thrown at plugins/PassboltCe/SmtpSettings/src/Service/SmtpOauthExchangeOnlineService.php:124

     * @throws \Cake\Http\Exception\InternalErrorException If the token request fails.
     * @see https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow#get-a-token
     */
    public function getAccessToken(): string
    {
        $tokenUrl = str_replace('__TENANT_ID__', $this->tenantId, self::LOGIN_TOKEN_URL);

        $response = $this->httpClient->post($tokenUrl, [
            'grant_type' => 'client_credentials',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'scope' => self::SCOPE,
        ]);

        if (!$response->isOk()) {
            $body = $response->getJson();
            $error = $body['error_description'] ?? $body['error'] ?? 'Unknown error';
            Log::error(sprintf('SMTP OAuth2 token fetch failed: %s', $error));
            throw new InternalErrorException(__('Failed to obtain SMTP OAuth2 access token.'));
        }

        $body = $response->getJson();
        if (empty($body['access_token'])) {
            throw new InternalErrorException(
                __('SMTP OAuth2 token response from Microsoft did not contain an access token.')
            );
        }

        return $body['access_token'];
    }

    /**
     * Get the OAuth2 username (email address of the sending mailbox).
     *
     * @return string
     */
    public function getUsername(): string

View on GitHub (pinned to 31c1bbc10f)