Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic implementation of Google OAuth via authorization code #54

Closed

Conversation

DrDelay
Copy link
Collaborator

@DrDelay DrDelay commented Aug 6, 2016

Regarding #39 and #52 , this lets users use do:

$application = new ApplicationKernel('CanBeGoogleEmailDoesntHaveToBe', 'AuthorizationCode', Factory::AUTHENTICATION_TYPE_GOOGLE_OAUTH);

The first parameter could in the future be used to cache the refresh_token. It is not relevant however as long as the authorization code is valid. It can be obtained here: https://accounts.google.com/o/oauth2/auth?client_id=848232511240-73ri3t7plvk96pj4f85uj8otdat2alem.apps.googleusercontent.com&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&scope=openid%20email%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&approval_prompt=force

@DrDelay DrDelay mentioned this pull request Aug 6, 2016
@NicklasWallgren
Copy link
Owner

Great addition, I started refactoring the authentication earlier today.

What do you think of this approach?

/~https://github.com/NicklasWallgren/PokemonGoAPI-PHP/commits/authentication-managers

@Ni42
Copy link
Collaborator

Ni42 commented Aug 6, 2016

I understand this right, it means we can store the auth token ourselves, and "relogin" with them without re-logging in on every page refresh/CLI call?

My use case/wish list for the login process:

  • Login once using credentials
  • Store auth token for these credentials somewhere
  • On another request, try using the app token for logging in, if that doesn't work, fall back to logging in again
  • The login functions need values for timeouts/session duration, and throw specific exceptions so I can catch and deal with them

(Maybe we should move this discussion to an issue XD)

@DrDelay
Copy link
Collaborator Author

DrDelay commented Aug 6, 2016

@Ni42 : I had it implemented like that in my personal project/playground:

interface AuthInterface
{
    /**
     * The auth mechanism the implementation represents.
     *
     * @return string
     */
    public function getAuthType():string;

    /**
     * Get the identifier for the account this tries to login to (most likely a username).
     *
     * @return string
     */
    public function getUniqueIdentifier():string;

    /**
     * Use this AuthInterface to get an access token.
     *
     * @return AccessToken
     *
     * @throws AuthException
     */
    public function invoke(): AccessToken;
}
    /**
     * Perform the login.
     *
     * @return PlayerData
     */
    public function login():PlayerData
    {
        /** @var AuthInterface $auth */
        $auth = null;
        try {
            $auth = $this->container->get(AuthInterface::class);
        } catch (AliasNotFound $e) {
            throw new \BadMethodCallException('You need to set an auth mechanism with setAuth', 0, $e);
        }

        $cacheKey = [$auth->getAuthType(), $auth->getUniqueIdentifier()];
        $item = $this->cache->getItem(static::cacheKey($cacheKey));
        if ($item->isHit()) {
            $this->logger->debug('Login Cache hit');
            $this->accessToken = $item->get();
        } else {
            $this->logger->info('Cache miss -> Doing login');
            $accessToken = $auth->invoke();
            $this->accessToken = $accessToken->getToken();
            $lifetime = $accessToken->getLifetime();
            $item->set($this->accessToken);
            if ($lifetime instanceof \DateTimeInterface) {
                $item->expiresAt($lifetime);
            } else {
                $item->expiresAfter($lifetime);
            }
            $this->cache->save($item);
        }

        (...)

        $this->logger->notice('Using AccessToken '.$this->accessToken);

        $playerData = $this->initialize();

        $this->logger->notice('Login completed. Logged in as '.$playerData->getUsername());

        return $playerData;
    }
class AccessToken
{
    /** @var string */
    protected $token;

    /** @var int */
    protected $lifetime;

    /**
     * AccessToken constructor.
     *
     * @param string                               $token
     * @param int|\DateTimeInterface|\DateInterval $lifetime
     */
    public function __construct(string $token, $lifetime)
    {
        if (!(is_int($lifetime) || $lifetime instanceof \DateTimeInterface || $lifetime instanceof \DateInterval)) {
            throw new \BadMethodCallException('Lifetime must be an integer or instance of DateTimeInterface / DateInterval');
        }
        $this->token = $token;
        $this->lifetime = $lifetime;
    }

    /**
     * Get the access token.
     *
     * @return string
     */
    public function getToken():string
    {
        return $this->token;
    }

    /**
     * Get the lifetime.
     *
     * @return int|\DateTimeInterface|\DateInterval
     */
    public function getLifetime()
    {
        return $this->lifetime;
    }
}

By forcing AuthInterface->invoke() to return an AccessToken containing a lifetime I can easily save it in a user-defined PSR-6-cache in the login-method.
Also, my AuthInterface doesn't force exactly username and password, but rather leaves this up to the implementation. That's what I like about the AuthenticationManager, it actually is pretty similar.

@Ni42
Copy link
Collaborator

Ni42 commented Aug 6, 2016

Yep, looked through your things a little while ago, I liked that part:+1:
Now I think something like this would also be needed for the configuration settings in general, but I think Nicklas has that planned anyways ^^

@DrDelay
Copy link
Collaborator Author

DrDelay commented Aug 6, 2016

@NicklasWallgren : Your new concept of the Authentication looks good to me.
Combining Google credentials and auth-code like this is interesting, but as both return a refresh token it makes sense, you can share the loginByRefreshToken-method.
I was using https://www.googleapis.com/oauth2/v4/token for both, exchangeAuthCode and getOauthToken($refreshToken), but if yours works that doesn't matter.

@NicklasWallgren
Copy link
Owner

NicklasWallgren commented Aug 7, 2016

Here is my latest addition.

// Initialize oauth token authentication manager
$manager = new AuthenticationOauthTokenManager();

// Add manager listener
$manager->addListener(function ($event, $data) {
    // Check whether we retrieved a access token
    if ($event === Manager::EVENT_ACCESS_TOKEN) {
        // Persist the access token into cache, session or whatever
        $accessToken = $data;
    }
});

// Initialize the pokemon go application
$application = new ApplicationKernel($manager);

@DrDelay
Copy link
Collaborator Author

DrDelay commented Aug 7, 2016

Very clean solution in my opinion, I think better then passing a cache into the application 👍 .

How would one implement something like on pokeadvisor ❓ : User provides authorization code -> application exchanges it for access and refresh token. Wouldn't I need 3 managers but can only pass one?

Edit: Because I want to save the exchanged tokens in something (session) and use them again, even though the end-user only provides auth codes.

@NicklasWallgren
Copy link
Owner

NicklasWallgren commented Aug 7, 2016

I guess this would work, or am I missing something?

        ###
        ### Initial request, the user provides authentication code, or by callback from google.
        ###

        $manager = new AuthenticationCodeManager('INSERT_AUTH_CODE');

        // Add a manager listener
        $manager->addListener(function ($event, $data) {
            // Check whether we retrieved a access token
            if ($event === Manager::EVENT_ACCESS_TOKEN) {

                // Store the AccessToken (including oauth token, refresh token, ttl) in session
                Session::set('access-token', $data); 

                // Direct to the homepage, or whatever
                Redirect::home();
            }
        });

        ###
        ### Homepage request (redirect)
        ###
        $accessToken = Session::get('access-token');

        // Validate the oauth token, check ttl
        if (!$accessToken->isValid()) {
            // Get new oauth token via AuthenticationRefreshTokenManager
            // Update session
        }

        $manager = new AuthenticationOauthTokenManager($accessToken->getToken());

        // Initialize the pokemon go application
        $application = new ApplicationKernel($manager);




@DrDelay
Copy link
Collaborator Author

DrDelay commented Aug 7, 2016

No, this is good, thank you for clearing it up. I was thinking in a more CLI'ish context where I only have on entry point, what doesn't have to be like this of course. Now that I see your example it makes perfect sense.

Closing this since it wouldn't be compatible with how you are refactoring the auth-system. and also implemented it yourself.

@DrDelay DrDelay closed this Aug 7, 2016
@NicklasWallgren
Copy link
Owner

NicklasWallgren commented Aug 7, 2016

Thanks for the input / contribution @DrDelay. Much appreciated :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants