While installing this app is trivial, the setup requires configuration OAuth keys. Since Google requires a publicly visible URL for the redirect, we'll use ngrok, but any tunnel should work (pinggy.io or localtunnel, for example).
git clone survos-sites/oauth-demo && cd oauth-demo
composer install
Setup your database and set DATABASE_URL in .env.local, if you use postgres you can migrate, otherwise run doctrine:schema:update --force --complete
symfony server:start -d
ngrok http http://localhost:8000
There's a quirk with make:user if the database is initially sqlite and then is switched to postgres.
So this demo uses postgres, you can install it with docker easily. Whatever postgres you use, make sure to add the appropriate DATABASE_URL to .env.local
sudo docker run --rm --name pg-docker -e POSTGRES_PASSWORD=docker -d -p 5434:5432 -v $HOME/docker/volumes/postgres16:/var/lib/postgresql/data postgres:16
Install the app with a traditional email-based registration/login.
symfony new --webapp oauth-demo && cd oauth-demo
# change
echo "DATABASE_URL=postgresql://postgres:docker@127.0.0.1:5434/auth-demo?serverVersion=16&charset=utf8" > .env.local
bin/console doctrine:database:create
composer config extra.symfony.allow-contrib true
composer config repositories.tac_oauth '{"type": "vcs", "url": "git@github.com:tacman/oauth2-client-bundle.git"}'
composer require knpuniversity/oauth2-client-bundle:dev-tac league/oauth2-github
bin/console importmap:require bootstrap
echo "import 'bootstrap/dist/css/bootstrap.min.css'" >> assets/app.js
symfony console make:controller AppController
sed -i "s|/app|/|" src/Controller/AppController.php
bin/console make:user --is-entity --identity-property-name=email --with-password User -n
echo "github_id,string,80,yes," | sed "s/,/\n/g" | bin/console make:entity User
echo "yes,no,yes,14" | sed "s/,/\n/g" | bin/console make:registration
bin/console make:migration
bin/console doctrine:migration:migrate -n
echo "1,AppAuthenticator,,," | sed "s/,/\n/g" | bin/console make:auth
sed -i "s|some_route|app_app|" src/Security/AppAuthenticator.php
sed -i "s|// return new|return new|" src/Security/AppAuthenticator.php
sed -i "s|throw new|//throw new|" src/Security/AppAuthenticator.php
echo '' > templates/social_media_login.html.twig
cat <<'EOF' > templates/app/index.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div>
<a href="{{ path('app_app') }}">Home</a>
</div>
{% if is_granted('IS_AUTHENTICATED_FULLY') %}
<a class="btn btn-primary" href="{{ path('app_logout') }}">Logout {{ app.user.email }} </a>
{% else %}
<a class="btn btn-primary" href="{{ path('app_register') }}">Register</a>
<a class="btn btn-secondary" href="{{ path('app_login') }}">Login</a>
{% endif %}
{{ include('social_media_login.html.twig') }}
{% endblock %}
EOF
symfony proxy:domain:attach oauth-demo
symfony server:start -d
symfony open:local --path=/register
Now that the traditional login/registration forms work, let's add the github config and env vars.
cat <<'EOF' > config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
clients:
# an instance of: KnpU\OAuth2ClientBundle\Client\Provider\GithubClient
# composer require league/oauth2-github
github:
# must be "github" - it activates that type!
type: github
# add and configure client_id and client_secret in parameters.yml
client_id: '%env(OAUTH_GITHUB_CLIENT_ID)%'
client_secret: '%env(OAUTH_GITHUB_CLIENT_SECRET)%'
# a route name you'll create
redirect_route: connect_github_check
redirect_params: { clientKey: github } # MUST match the client key above
# whether to check OAuth2 "state": defaults to true
# use_state: true
EOF
cat << 'EOF' >> .env
OAUTH_GITHUB_CLIENT_ID=
OAUTH_GITHUB_CLIENT_SECRET=
EOF
cat << 'EOF' >> .env.local
OAUTH_GITHUB_CLIENT_ID=
OAUTH_GITHUB_CLIENT_SECRET=
EOF
Now configure the app on github. This step now requires 2FA, and is the slowest part of this tutorial! Once you're in, go to https://github.com/settings/developers and click on New OAuth App
Fill out the form, paying careful attention to the redirect url https://oauth-demo.wip/connect/github/check, which we'll define in the controller
Generate the key:
Open .env.local and set the key and client id
Now create the controller that handles the workflow. In this case, we're going to create a user automatically and set the password to the github username. A real application would handle this better.
cat << 'EOF' > src/Controller/GithubController.php
<?php
namespace App\Controller;
use App\Entity\User;
use App\Security\AppAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
class GithubController extends AbstractController
{
public function __construct(
private EntityManagerInterface $entityManager,
private UserAuthenticatorInterface $userAuthenticator,
private AppAuthenticator $authenticator,
private UserPasswordHasherInterface $userPasswordHasher
)
{
}
/**
* Link to this controller to start the "connect" process
*/
#[Route('/connect/github', name: 'connect_github_start')]
public function connect(ClientRegistry $clientRegistry): Response
{
// will redirect to Github!
$redirect = $clientRegistry
->getClient('github') // key used in config/packages/knpu_oauth2_client.yaml
->redirect([
'email' // the scopes you want to access
]);
# hack for not returning https
$redirect->setTargetUrl(str_replace('http%3A', 'https%3A', $redirect->getTargetUrl()));
return $redirect;
}
/**
* After going to Github, you're redirected back here
* because this is the "redirect_route" you configured
* in config/packages/knpu_oauth2_client.yaml and in the Github App page
*/
#[Route('/connect/github/check', name: 'connect_github_check')]
public function connectCheckAction(Request $request, ClientRegistry $clientRegistry): Response
{
/** @var \KnpU\OAuth2ClientBundle\Client\Provider\GithubClient $client */
$client = $clientRegistry->getClient('github');
try {
// the exact class depends on which provider you're using
/** @var \League\OAuth2\Client\Provider\GithubResourceOwner $user */
$accessToken = $client->getAccessToken();
$oAuthUser = $client->fetchUserFromToken($accessToken);
$email = $oAuthUser->getEmail();
if (!$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $email])) {
$user = (new User())
->setEmail($email);
$this->entityManager->persist($user);
}
// better is to redirect to a page requiring the user to set/change their password, or allow null passwords.
$plaintextPassword = $oAuthUser->getId();
$hashedPassword = $this->userPasswordHasher->hashPassword(
$user,
$plaintextPassword
);
$user
->setPassword($hashedPassword)
->setGithubId($accessToken);
$this->entityManager->flush();
$this->userAuthenticator->authenticateUser($user, $this->authenticator, $request);
} catch (IdentityProviderException $e) {
// something went wrong!
// probably you should return the reason to the user
dd($e->getMessage());
}
return $this->redirectToRoute('app_app');
}
}
EOF
cat << 'EOF' > templates/social_media_login.html.twig
<a class="btn btn-danger" href="{{ path('connect_github_start') }}">Login with Github</a>
EOF
symfony open:local --path=/connect/github
Often for deployment, we need a real database, since sqlite is on a disk that might not be writable.