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

Fitbit Laravel Package

socialiteproviders/fitbit

Fitbit OAuth provider for Laravel Socialite. Lets your app authenticate users with their Fitbit accounts through Socialite's familiar driver interface. Install via Composer, configure your Fitbit client ID and secret, and add Fitbit login to your Laravel application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up in under ten minutes.

composer require socialiteproviders/fitbit

The subtree package pulls in socialiteproviders/manager, which is what registers the driver with Socialite. Then:

  1. Register the Fitbit app at https://dev.fitbit.com/apps. Note the OAuth 2.0 Application Type you pick (Personal vs Server) — it determines which scopes and endpoints you're allowed to call later. Set the callback URL to an exact, byte-for-byte match of your route (HTTPS required except for localhost).

  2. Add credentials to config/services.php:

'fitbit' => [
    'client_id'     => env('FITBIT_CLIENT_ID'),
    'client_secret' => env('FITBIT_CLIENT_SECRET'),
    'redirect'      => env('FITBIT_REDIRECT_URI'), // https://app.test/auth/fitbit/callback
],
  1. Hook the Socialite event. On Laravel ≤ 10, in app/Providers/EventServiceProvider.php:
protected $listen = [
    \SocialiteProviders\Manager\SocialiteWasCalled::class => [
        \SocialiteProviders\Fitbit\FitbitExtendSocialite::class.'@handle',
    ],
];

On Laravel 11+ (no EventServiceProvider by default), in AppServiceProvider::boot():

Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
    $event->extendSocialite('fitbit', \SocialiteProviders\Fitbit\Provider::class);
});
  1. First use case — sign in and capture tokens:
Route::get('/auth/fitbit', fn () => Socialite::driver('fitbit')
    ->scopes(['profile', 'activity', 'heartrate', 'sleep'])
    ->redirect());

Route::get('/auth/fitbit/callback', function (Request $request) {
    $fitbitUser = Socialite::driver('fitbit')->user();

    // Socialite\Two\User exposes the token endpoint payload directly:
    $tokens = $fitbitUser->accessTokenResponseBody; // access_token, refresh_token, expires_in, scope, user_id

    $account = FitbitAccount::updateOrCreate(
        ['fitbit_user_id' => $fitbitUser->getRaw()['user']['encodedId']],
        [
            'access_token'  => $tokens['access_token'],
            'refresh_token' => $tokens['refresh_token'],
            'expires_at'    => now()->addSeconds($tokens['expires_in']), // 8 hours
        ]
    );

    Auth::login($account->user);
    return redirect('/dashboard');
});

Where to look first: the provider source (vendor/socialiteproviders/fitbit/Provider.php) is ~80 lines. Read it before you build anything — it tells you exactly which fields it maps, which endpoint it calls (/1/user/-/profile.json), and whether token refresh is implemented.

Implementation Patterns

Wrap the driver behind your own service. The package is a thin translator between Socialite and Fitbit's OAuth endpoints; your application should not call Socialite::driver('fitbit') from more than one place. Put the redirect, callback, token storage, and refresh behind a FitbitConnector class. That makes the eventual replacement (vendoring the provider, or writing 60 lines yourself) a contained change instead of a codebase-wide grep.

Store the refresh token, and refresh proactively. Fitbit access tokens expire in 8 hours — far shorter than a typical session. Don't wait for a 401:

public function freshToken(FitbitAccount $account): string
{
    if ($account->expires_at->isFuture()) {
        return $account->access_token;
    }

    return DB::transaction(function () use ($account) {
        $account = FitbitAccount::lockForUpdate()->find($account->id); // see gotcha on rotation
        if ($account->expires_at->isFuture()) {
            return $account->access_token; // another worker refreshed it
        }

        $response = Http::asForm()
            ->withBasicAuth(config('services.fitbit.client_id'), config('services.fitbit.client_secret'))
            ->post('https://api.fitbit.com/oauth2/token', [
                'grant_type'    => 'refresh_token',
                'refresh_token' => $account->refresh_token,
            ])->throw()->json();

        $account->update([
            'access_token'  => $response['access_token'],
            'refresh_token' => $response['refresh_token'], // rotates — must persist
            'expires_at'    => now()->addSeconds($response['expires_in']),
        ]);

        return $response['access_token'];
    });
}

Call the API with the stored token, not the Socialite user. Socialite gives you the profile once; everything else is plain HTTP:

Http::withToken($this->freshToken($account))
    ->get('https://api.fitbit.com/1/user/-/activities/date/today.json')
    ->throw()
    ->json();

- is Fitbit's alias for "the authenticated user"; use the encodedId when an admin or webhook context needs to act on someone else.

Queue the sync, cache the read. Rate limits are per-user and hourly, so put activity/sleep pulls on a job, keyed by user, and let the dashboard read from your own table. This is also the pattern that makes the Fitbit dependency removable later — your domain model never depends on Fitbit's JSON shape.

Extend the provider rather than fork the package. If you need a different map (custom fields on the User object) or a different profile endpoint, subclass it and register your class in the event listener:

class AppFitbitProvider extends \SocialiteProviders\Fitbit\Provider
{
    protected function mapUserToObject(array $user)
    {
        return (new User)->setRaw($user)->map([
            'id'       => $user['user']['encodedId'],
            'nickname' => $user['user']['displayName'],
            'name'     => $user['user']['fullName'] ?? $user['user']['displayName'],
            'avatar'   => $user['user']['avatar150'],
            'email'    => null,
        ]);
    }
}

Testing. Socialite bypasses Http::fake() on older versions because it constructs its own Guzzle client, so fake at the Socialite boundary instead:

Socialite::shouldReceive('driver->user')->andReturn(
    (new \Laravel\Socialite\Two\User)->map(['id' => 'ABC123', 'nickname' => 'jane'])
);

For the refresh/API layer, test your own Http::fake()-able service — another reason to keep the connector separate.

Multi-surface usage. The same connector serves web login, a mobile handoff (issue a short-lived signed URL after social login), and background sync jobs. Keep scopes in config per surface: profile for sign-in only, activity,sleep,heartrate for the data-consent screen — ask for the minimum at login and upgrade scopes later via a second authorization pass.

Gotchas and Tips

The 2020 release predates Fitbit's PKCE requirement. Fitbit now requires PKCE (code_challenge/code_verifier) for new app registrations. This provider's getAuthUrl()/getTokenFields() almost certainly don't send it, so a freshly created Fitbit app may fail at the token exchange with invalid_grant. Before you build on it, confirm against your own app; if it fails, the fix is a ~30-line subclass overriding getAuthUrl() and getTokenFields(), storing the verifier in the session. Treat this as the single biggest adoption risk.

Refresh tokens rotate and the old one dies immediately. Two concurrent requests refreshing the same account will leave one with an invalidated token and the user logged out. Always refresh under a row lock (lockForUpdate) or a cache lock keyed by account id.

Socialite never refreshes for you. Socialite::driver('fitbit')->user() is a one-shot exchange. There's no built-in "get a valid token later" — that's your code (see above).

getEmail() is effectively null. Fitbit's profile endpoint doesn't return an email address. Don't key accounts on email or attempt to link a Fitbit login to an existing email account without an explicit in-app confirmation step — otherwise you have an account-takeover vector. Key on encodedId and require a signed-in user to link.

State mismatches. InvalidStateException on the callback usually means the session was lost (cross-domain redirect, cookie SameSite, or a load balancer without sticky sessions). For API/mobile flows use ->stateless() and carry your own state parameter.

Exact-match redirect URI. A trailing slash, http vs https, or 127.0.0.1 vs localhost all produce Fitbit's opaque invalid_request. Also confirm the URL is registered on the same Fitbit app whose client id you're using — a common staging/production mix-up.

Scope string is space-delimited, and invalid scopes fail the whole request. Fitbit returns a redirect with error=invalid_scope rather than a helpful list. Requesting intraday or detailed heart-rate data on a Server-type app will fail — those need Personal type and, in some cases, an approval request. Check per-endpoint restrictions before committing to a feature; several endpoints were tightened after the Google acquisition.

Rate limits are 150 requests/hour/user, with a 429 carrying Fitbit-Rate-Limit-Reset. Handle it in your sync job with exponential backoff, and never loop a user's entire history in one go.

Subtree split means issues and fixes live elsewhere. PRs and bug reports belong on SocialiteProviders/Providers (monorepo), and the subtree tag here is from 2020. Before assuming the code is current, diff vendor/socialiteproviders/fitbit/Provider.php against src/Fitbit/ on the monorepo's master — there may be unshipped fixes. If so, vendor the class into app/Socialite/ and register that instead of relying on Composer.

Pin the version and add an upgrade line to your runbook. socialiteproviders/manager's supported Laravel range is the real compatibility constraint here, not this package. composer require socialiteproviders/fitbit:^4.1 plus a quarterly composer outdated check is the minimum ownership cost.

Debugging the handshake. When the callback errors, log the raw query string and $user->accessTokenResponseBody in non-production only — Fitbit's error bodies (invalid_client, invalid_grant, redirect_uri_mismatch) are more informative than Socialite's exceptions, which tend to collapse into InvalidStateException or a generic ClientException. Never log tokens in production; make sure your exception handler scrubs access_token/refresh_token from request payloads.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk