Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Oauth2 Amocrm Laravel Package

amocrm/oauth2-amocrm

AmoCRM OAuth 2.0 provider for PHP and Laravel. Integrate AmoCRM authentication into your app with a simple, standards-compliant league/oauth2-client adapter supporting authorization code flow, token refresh, and easy configuration for CRM integrations.

View on GitHub
Deep Wiki
Context7

Getting Started

This is not a Laravel package. There is no service provider, no config file, no facade, no migrations. It is a single League OAuth2 Client provider class (AmoCRM\OAuth2\Client\Provider\AmoCRMProvider) that talks to amoCRM's /oauth endpoints. Budget ~30 minutes: read the class in vendor/amocrm/oauth2-amocrm/src/, then write your own wiring. It is first-party (published under the amocrm org), which is the main reason to trust it over its 24 stars — but pin the version, because zero dependents means nobody else is catching regressions for you.

Step 1 — install and get credentials

composer require amocrm/oauth2-amocrm

Create an integration in amoCRM (Settings → Integrations → Create integration) and copy the Client ID, Secret key, and set the Redirect URI. The redirect URI must be HTTPS and must match your config character-for-character — amoCRM rejects the authorize request otherwise, with no useful error beyond a page redirect.

Step 2 — config

// config/services.php
'amocrm' => [
    'client_id'     => env('AMOCRM_CLIENT_ID'),
    'client_secret' => env('AMOCRM_CLIENT_SECRET'),
    'redirect'      => env('AMOCRM_REDIRECT_URI'),
],

Step 3 — first use case: connect an account

Put the provider behind a small factory rather than resolving it as a singleton — the baseDomain (subdomain) is per-account and varies at runtime.

// app/Services/AmoCrm/AmoCrmProviderFactory.php
public function make(?string $baseDomain = null): AmoCRMProvider
{
    return new AmoCRMProvider([
        'clientId'     => config('services.amocrm.client_id'),
        'clientSecret' => config('services.amocrm.client_secret'),
        'redirectUri'  => config('services.amocrm.redirect'),
        'baseDomain'   => $baseDomain ?? 'www.amocrm.ru',
    ]);
}
// Controller
public function redirect(AmoCrmProviderFactory $factory)
{
    $provider = $factory->make();
    $url = $provider->getAuthorizationUrl();

    session(['amocrm_state' => $provider->getState()]);

    return redirect()->away($url);
}

public function callback(Request $request, AmoCrmProviderFactory $factory)
{
    abort_unless(
        $request->filled('state') && $request->state === session('amocrm_state'),
        403, 'Invalid OAuth state'
    );
    session()->forget('amocrm_state');

    $token = $factory->make()->getAccessToken('authorization_code', [
        'code' => $request->code,
    ]);

    AmoIntegration::updateOrCreate(
        ['account_id' => $token->getValues()['account_id'] ?? null],
        [
            'access_token'  => $token->getToken(),
            'refresh_token' => $token->getRefreshToken(),
            'expires_at'    => now()->addSeconds($token->getExpires() - 300),
            'base_domain'   => $factory->make()->getBaseDomain(),
        ]
    );

    return redirect()->route('integrations.index');
}

That's the whole happy path: getAuthorizationUrl() → user consents in amoCRM → getAccessToken('authorization_code', ...) → store. Everything past that (refresh, API calls, revocation) is yours to build.

Implementation Patterns

Token storage model. One row per connected account: base_domain, account_id, access_token, refresh_token, expires_at, is_active, last_error_at, last_error. Encrypt the token columns ('access_token' => 'encrypted' cast) — they are bearer credentials for a paying customer's CRM.

Refresh with rotation, single-flight. This is the pattern that matters most. Access tokens live 24 hours; refresh tokens live 3 months and rotate on every use — consuming a refresh token invalidates the previous one. Concurrent refreshes in queue workers will silently break the integration.

public function freshToken(AmoIntegration $integration): string
{
    return Cache::lock("amocrm:refresh:{$integration->id}", 30)->block(10, function () use ($integration) {
        $integration->refresh();

        if ($integration->expires_at->isFuture()) {
            return $integration->access_token;
        }

        $provider = $this->factory->make($integration->base_domain);

        try {
            $token = $provider->getAccessToken('refresh_token', [
                'refresh_token' => $integration->refresh_token,
            ]);
        } catch (IdentityProviderException $e) {
            $integration->update(['is_active' => false, 'last_error_at' => now()]);
            throw $e;
        }

        $integration->update([
            'access_token'  => $token->getToken(),
            'refresh_token' => $token->getRefreshToken(), // MUST persist — old one is dead
            'expires_at'    => now()->addSeconds($token->getExpires() - 300),
            'is_active'     => true,
        ]);

        return $token->getToken();
    });
}

Add a scheduled command (->dailyAt('03:00')) that refreshes any integration expiring within 48 hours, so background jobs never hit a cold expiry. AmoCRM's 3-month refresh window means a silently broken account is only discovered when it's been dead for a quarter — alert on last_error_at.

Calling the API. The provider only does OAuth; it does not wrap the REST API. Use Laravel's HTTP client:

Http::withToken($this->tokenFor($integration))
    ->acceptJson()
    ->baseUrl("https://{$integration->base_domain}")
    ->timeout(10)
    ->retry(2, 200, throw: false)
    ->post('/api/v4/leads/complex', $payload);

Handle 401 by marking the integration inactive and dispatching a re-auth notification — do not retry a 401, and do not auto-refresh inside the retry callback (you'll race yourself).

Multi-tenant. Never bind AmoCRMProvider as a singleton. Resolve per account via the factory. If you need it in a queued job, serialize integration_id (an int) and re-hydrate — never serialize the token object.

Socialite-style driver (optional). If your team already thinks in Socialite, register a custom driver via Socialite::extend('amocrm', ...) returning the provider. It's cosmetic, but it keeps the redirect/callback boilerplate consistent with your other integrations.

Complement, don't duplicate. For real CRM work (entities, custom fields, webhooks, complex lead creation) pair this with amocrm/amocrm-api-library, which ships its own token model and API client. If you do, pick one owner of the refresh cycle — either this provider or the API library's AmoCRMApiClient, never both writing to the same token row.

Gotchas and Tips

  • Http::fake() will not intercept the OAuth calls. The provider uses Guzzle directly. Tests that stub Http::fake() will still hit the network on token exchange. Either inject a Guzzle MockHandler into the provider's getHttpClient()/collaborators, or wrap the provider in your own interface and fake that — the latter is far less brittle.
  • state is generated, not validated. getAuthorizationUrl() sets state and getState() returns it, but League does nothing to verify it on the way back. If you skip the hash_equals check above, you have CSRF on your integration endpoint.
  • baseDomain naming drift. Config keys and method names have shifted across releases (domain in older versions, baseDomain now). Store the value you passed in, not the one you reconstruct from the response, and keep it scheme-less (acme.amocrm.ru) — the provider prefixes https:// when building URLs. Custom-domain accounts (*.kommo.com) work, but your integration settings must whitelist that host.
  • Expiry is optimistic. $token->hasExpired() compares against expires_in from the response; clock skew and network latency mean a token can be dead before it reads as expired. Always refresh with a 5-minute buffer, and treat a 401 as authoritative over your stored expires_at.
  • Identity/account info is thin. The provider's resource-owner support is a couple of lines over an account endpoint. For anything beyond "which account is this", call /api/v4/account yourself; don't expect a rich AmoCRMResourceOwner.
  • Errors are League-shaped. Auth failures throw League\OAuth2\Client\Provider\Exception\IdentityProviderException. amoCRM's actual message (invalid_grant, "The refresh token is invalid") is in $e->getResponseBody() — but that may be a string or array depending on the response, so normalize before logging or you'll get "Array" in your error dashboard.
  • Revocation is silent and final. If a user removes the integration in amoCRM, the refresh token dies immediately. Catch it once, set is_active = false, and stop the schedule for that row — otherwise your queue backs up with identical failures every 24 hours.
  • Extension points, if you need them. Subclass AmoCRMProvider and override getBaseAuthorizationUrl(), getDefaultHeaders(), or createResourceOwner() — e.g. to inject mode=popup into the authorize URL for an in-CRM iframe install flow, which the package doesn't expose as an option.
  • Version pinning matters more than usual. With 24 stars and no dependents, you are the de facto QA. Pin an exact version ("amocrm/oauth2-amocrm": "1.x.y"), check that the release you install matches your league/oauth2-client major, and read the diff before bumping.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core