Dynamic custom data via OAuth with Laravel Socialite

If you’ve ever done authentication in a Laravel-based project then you probably had to deal with the Socialite extension, which enables OAuth-based authentication using third-party services (like Google or Facebook).

Passing dynamic data to the external OAuth system and getting that data back to your application, using Socialite, is something neither easily done nor well documented and given that I’ve seen a lot of people wasting time on this, I want to try and shed some light on it here.

Just as a brief clarification, the Socialite/OAuth flow implies the following steps (note that I’m using Google here as an example, but it can be any OAuth service provider):

  1. The Sign-in button:
    The user clicks “Sign in with Google” which calls a method in your app’s authentication controller, for example AuthController::signInWithGoogle();
  2. The Jump to the OAuth screen:
    Your app redirects using Socialite::driver('google')->redirect() which redirects the user to Google’s authentication page;
  3. The remote login:
    Google authenticates the user;
  4. The return to you app:
    Then, Google redirects the user back to your application’s callback route, e.g. AuthController::callbackFromGoogle();
  5. The final login:
    Finally your app extracts the user’s data (returned by Google) using Socialite::driver('google')->user() and proceeds to authenticate current session (btw, the user is created if new).

Note: On step 2, when jumping to Google, Socialite appends a special query parameter called redirect_uri, which is your app’s URI where Google will redirect the user at step 4.

The Problem

Overall, this works very well, except when you need to pass some custom data to the destination URI on-the-fly. You might want to do this, for example, when you have multiple points of login, and you need to know (at the destination) where the user came from.

To save you some time, I’ll mention some ideas here which are not possible solutions.

Altering redirect_uri (violates the OAuth rules)

Just to make this clear from the start, the OAuth standard requires the redirect_uri to be registered with the OAuth provider (you probably know this already) so altering this value dinamically will not help, no matter how we do it.

Data added with ->with([...]) is ignored

Socialite does provide a way to pass optional parameters to the link that takes the user to the OAuth page (step 2), but unfortunately these parameters are not forwarded to your app’s destination URI on step 4. In fact these parameters can only be used as special values intended for the OAuth service itself (e.g. Google or Facebook), and not for your application.

Solutions

From what I could gather so far, I know of two working solutions to this problem:

1. Use the Session, Luke

The title pretty much says it all: store any dynamic data you need in the Session object before redirecting to the external OAuth service (step2) and retrieve that data in the callback route that’s called upon the return. This is the preferable solution and here’s how it looks:

public function signInWithGoogle(Request $request)
{
    Session::put('secret', 123);
    return Socialite::driver('google')->redirect();
}

public function callbackFromGoogle(Request $request)
{
    $socialUser = Socialite::driver('google')->user();   
    $secret = Session::pull('secret');

    // The $secret variable now contains 123.
    // ...
}

Caveat

The only case when you might not be able to use this approach is when you deal with a stateless backend such as an API, which implies that there is no session. If that’s the case, then you should do the following…

2. Hijacking Socialite’s state token

It turns out that Socialite also sends a special token as the state URI parameter to Google (step 2), which does get delivered to the destination (step 4). This is a special token which is checked against the current PHP session when extracting the returned user data (step 5) to protect against XSRF attacks.

When redirecting the user to the OAuth page, we can overwrite the state parameter with anything we need:

public function signInWithGoogle(Request $request)
{
    return Socialite::driver('google')->with([
       'state' => "secret=123"
    ])->redirect();
}

At the end of the authentication flow, we can instruct Socialite to ignore the state token check by using the ->stateless() method and then recover the custom data from the state parameter like this:

public function callbackFromGoogle(Request $request)
{
    $socialUser = Socialite::driver('google')->stateless()->user();
    
    // ...

    $state = $request->input('state');
    parse_str($state, $custom_data);

    // The $custom_data variable now contains ['secret' => 123].
    // ...
}

Caveat

Note that, this approach has the obvious drawback of canceling Socialite’s state-checking feature, which can be detrimental to security if your app uses sessions. So only use this one if you’re building a stateless API’s that allows for OAuth logins.

Comment below if I missed something or you know a better way.

Thanks, enjoy!

Don't keep it to yourself!...