league/oauth2-instagram
Instagram OAuth 2.0 provider for the League OAuth2 Client. Adds Instagram authorization and token handling to PHP applications so you can authenticate users and access Instagram APIs with minimal setup.
league/oauth2-instagram 3.0.0 is a breaking release: it drops the Legacy API (api.instagram.com/v1/...) and swaps the resource-owner call to the Instagram Basic Display endpoint (https://graph.instagram.com/me). If you have 2.x in a composer.lock, audit before bumping — three named getters were removed (see Gotchas).
composer require league/oauth2-instagram:^3.0
Same provider API as every other league/oauth2-client provider — instantiate from config, never inline:
use League\OAuth2\Client\Provider\Instagram;
$provider = new Instagram([
'clientId' => config('services.instagram.client_id'),
'clientSecret' => config('services.instagram.client_secret'),
'redirectUri' => route('instagram.callback'),
// 3.0.0: Graph host is configurable — useful when you must point at
// graph.facebook.com instead of graph.instagram.com. Check the
// constructor in the tagged source for the exact option key.
// 'graphHost' => 'https://graph.facebook.com/v21.0',
]);
The canonical three-legged flow is unchanged, but note the default scope is now ['user_profile'], so a bare getAuthorizationUrl() only buys you a profile:
// 1. Redirect out
$url = $provider->getAuthorizationUrl([
'scope' => ['user_profile'], // add 'user_media' only if you really need media
]);
session(['ig_state' => $provider->getState()]);
return redirect($url);
// 2. Handle callback
$token = $provider->getAccessToken('authorization_code', [
'code' => $request->query('code'),
]);
// 3. Fetch the user
$owner = $provider->getResourceOwner($token);
$owner->getId(); // Instagram-scoped user id — still present
$owner->toArray(); // ['id', 'username', 'account_type', 'media_count', 'profile_picture_url']
Read InstagramResourceOwner first. After 3.0.0 it no longer carries legacy-API fields, so anything you learned from a 2.x debug dump (name, description, website, followers_count) is gone.
Register the provider as a singleton so credentials and the Graph host live in one place — the host option is the whole point of 3.0.0's new feature, and you do not want it duplicated across call sites:
$this->app->singleton(Instagram::class, fn () => new Instagram([
'clientId' => config('services.instagram.client_id'),
'clientSecret' => config('services.instagram.client_secret'),
'redirectUri' => route('instagram.callback'),
// 'graphHost' => config('services.instagram.graph_host'),
]));
Persist the token, not the provider. Store $token->getToken(), $token->getExpires(), and $token->getResourceOwnerId() alongside the $owner->getId() value on a social_accounts row.
Map the resource owner once, into your own DTO. Since the getters are thinner now, centralise the mapping instead of sprinkling toArray() keys through controllers:
$data = $owner->toArray();
InstagramProfile::from([
'id' => $data['id'],
'username' => $data['username'] ?? null,
'avatar' => $data['profile_picture_url'] ?? null,
'account' => $data['account_type'] ?? null,
]);
Call Graph yourself — this package only does OAuth. With 3.0.0 the resource-owner URL moved to the Graph host, but nothing else is wrapped:
Http::withToken($token->getToken())
->get('https://graph.instagram.com/me', [
'fields' => 'id,username,account_type,media_count,profile_picture_url',
]);
Keep getAuthorizationUrl() and getAccessToken() as two separate service methods so each is independently unit-testable and the state round-trip is explicit.
Testing. Inject a mock HTTP client via the httpClient provider option and assert on the outgoing request — the endpoint change in 3.0.0 makes this worth doing, because a test that asserts on graph.instagram.com/me will catch the next host/format move:
$provider = new Instagram($config, ['httpClient' => new \Http\Mock\Client()]);
Integration tip: if you're on Laravel and only need login + a profile, a Socialite driver may still be the better fit — it handles state/session plumbing that this package deliberately leaves to you.
getName(), getImageUrl(), and getDescription() are removed in 3.0.0. Any 2.x code calling them fatals immediately. Grep before upgrading: grep -rn "getImageUrl\|->getName()\|getDescription()" app/. Replace with toArray()['username'], ['profile_picture_url'], and (for description) nothing — the field has no Basic Display equivalent.getBaseAuthorizationUrl, getBaseAccessTokenUrl, getResourceOwnerDetailsUrl) against current docs and override them — that remains the intended extension point. The host option typically does not cover the authorize/token hosts.['user_profile']. Upgrading from 2.x can therefore break media-posting/media-reading flows with a 400/permission error that looks unrelated. Pass ['user_profile', 'user_media'] explicitly if you need media — and only if those scopes are granted in the Meta dashboard.getAccessToken('refresh_token', ...) will not work. Long-lived tokens come from a non-standard grant_type=ig_exchange_token exchange and are refreshed via ig_refresh_token; perform that yourself and store the result. AccessToken::getRefreshToken() will be null.IdentityProviderException, not at the redirect, and the message rarely names the missing scope.redirectUri must match byte-for-byte (scheme, host, trailing slash, query). Still the #1 cause of invalid redirect_uri.League\OAuth2\Client\Provider\Exception\IdentityProviderException from getAccessToken/getResourceOwner. Inspect $e->getResponseBody() — Graph returns {"error": {"message": ..., "type": ...}}, which the default checkResponse may not surface cleanly.getAuthorizationUrl() generates state and getState() returns it, but nothing validates the callback. Compare against the session value and reject mismatches.createResourceOwner() with your own InstagramResourceOwner subclass that re-adds the keys you rely on — cleaner than normalising in ten controllers.httpClient. OAuth failures are almost always legible in the raw response body.getPkceMethod().league/oauth2-client deliberately. This provider tracks v2.x APIs; v3 changes getResourceOwner internals, and 3.0.0 of this package won't have accounted for it.How can I help you explore Laravel packages today?