简体   繁体   中英

Laravel Oauth2 controller using League OAuth2 client

I'm trying to use the League OAuth2 Client to allow users to authenticate my Laravel web app to set appointments on their calendar. NOTE: I'm not trying to let users login to my site or authenticate into my site using OAuth. I just want to be able to let users add appointments to their own calendars.

I'm basically following the flow outlined here: https://github.com/thephpleague/oauth2-google and have created a single controller (called OauthController with a single method, redirectGoogle . My redirect route (which is registered with Google) is https://example.com/oauth2/google . When I hit this endpoint in my Laravel app, I get redirected to Google to approve my app to access my account data as expected, and then redirected back to the controller endpoint.

However it fails every time at the exit('Invalid state'); line.

Here's the controller method code:

public function redirectGoogle(Request $request)
{
    $provider = new Google([
        'clientId'     => config('oauth.google_oauth_id'),
        'clientSecret' => config('oauth.google_oauth_secret'),
        'redirectUri'  => 'https://example.com/oauth2/google',
    ]);

    if (!empty($request->input('error'))) {
        // Got an error, probably user denied access
        dd($request->input('error'));
    } elseif (empty($request->input('code'))) {
        // If we don't have an authorization code then get one
        $authUrl = $provider->getAuthorizationUrl();
        session(['oauth2state', $provider->getState()]);
        Log::info('Storing provider state ' . session('oauth2state')); <-- Log entry exists so we know session value was written
        header('Location: ' . $authUrl);
        exit;
    } elseif (empty($request->input('state')) || ($request->input('state') !== session('oauth2state', false))) {
        Log::error($request->input('state') . ' did not equal stored value ' . session('oauth2state', false)); <-- Log entry exists
        // State is invalid, possible CSRF attack in progress
        exit('Invalid state'); <-- Breaks here
    } else {
        // Try to get an access token (using the authorization code grant)
        $token = $provider->getAccessToken('authorization_code', [
            'code' => $request->input('code')
        ]);

        // Optional: Now you have a token you can look up a users profile data
        try {
            // We got an access token, let's now get the owner details
            $ownerDetails = $provider->getResourceOwner($token);

            // Use these details to create a new profile
            dd('Hello %s!', $ownerDetails->getFirstName());

        } catch (Exception $e) {
            // Failed to get user details
            dd('Something went wrong: ' . $e->getMessage());
        }

        // Use this to interact with an API on the users behalf
        echo $token->getToken() . PHP_EOL;

        // Use this to get a new access token if the old one expires
        echo $token->getRefreshToken() . PHP_EOL;

        // Unix timestamp at which the access token expires
        echo $token->getExpires() . PHP_EOL;
        dd();
    }
}

The strange thing is that the log messages noted in the code above both exist, and the values match (at least, it is attempting to write the first session variable with a value that would match the second log file's value):

[2020-05-04 21:02:48] local.INFO: Storing provider state 4963a33bbd5bcf52d3e21c787f24bd7b  
[2020-05-04 21:02:51] local.ERROR: 4963a33bbd5bcf52d3e21c787f24bd7b did not equal stored value <null>

Why is it that the second time through the code the oauth2state session value is null, when it was successfully written on the first loop?

NOTE: the problem appears to be that the sessions are different, which makes sense, but how can this session stay consistent, or otherwise keep the data straight?

[2020-05-05 15:25:06] local.INFO: Session id: bV7F5mNM69rJAVJNWK9ZD0rcoN284FxXvjNAmUiw  
[2020-05-05 15:25:06] local.INFO: Storing provider state 7351b313b741df41a6be9a049f71db6b  
[2020-05-05 15:25:10] local.INFO: Session id: VNiBxr1gYYIA9Nr11x9c4JJArHOiKQScEGh2jkuc  
[2020-05-05 15:25:10] local.ERROR: 7351b313b741df41a6be9a049f71db6b did not equal stored value <null>  

EDIT2: I've tried the tutorial here which uses a slightly different approach using Laravel and the League Oauth library-- it has the exact same problem, the session ID is different between the two requests, meaning there's no way you'll ever get a match between the state keys.

I believe the problem lies with how you redirect to google.

Problem :

Laravel needs to run trough the whole request in order to persist values into the session.

By using exit; you are interrupting the request and therefore Laravel will not get the chance to persist the values into the session.

Solution :

By using the redirect() helper as suggested in the docs , Laravel will be able to complete the request.

elseif(empty($request->input('code'))) {
    // If we don't have an authorization code then get one
    $authUrl = $provider->getAuthorizationUrl();
    session(['oauth2state', $provider->getState()]);
    Log::info('Storing provider state ' . session('oauth2state'));
    return redirect($authUrl);
}

Explanation :

In Laravel you can decide when a middleware is run, from the docs :

Before & After Middleware

Whether a middleware runs before or after a request depends on the middleware itself. For example, the following middleware would perform some task before the request is handled by the application:

public function handle($request, Closure $next)
{
    // Perform action

    return $next($request);
}

However, this middleware would perform its task after the request is handled by the application:

public function handle($request, Closure $next)
{
    $response = $next($request);

    // Perform action

    return $response;
}

Now if we take a look at how Laravel persists the session data in the StartSession middleware , you can see here that Laravel tries to persist the data into the session after the request has been handled by the application, so by using exit; , die(); or dd(); your are stopping the script and Laravel never gets the opportunity to persist the values in the session.

protected function handleStatefulRequest(Request $request, $session, Closure $next)
{
    // Before middleware
    $request->setLaravelSession(
        $this->startSession($request, $session)
    );

    $this->collectGarbage($session);

    $response = $next($request);

    // After middleware
    $this->storeCurrentUrl($request, $session);

    $this->addCookieToResponse($response, $session);

    $this->saveSession($request);

    return $response;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM