If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
Now that it's possible for users to authorize TopCluck to count their COOP eggs, Brent's on his way to showing farmer Scott just whose eggs rule the roost.
Feeling fancy, he wants to make life even easier by letting users skip registration and just login via COOP. Afterall, every farmer who uses the site will already have a COOP account.
Since we've done all the authorization code work already, adding "Login with COOP" or "Login with Facebook" buttons is really easy.
Start back in CoopOAuthController.php
, where we handled the exchange of the
authorization code for the access token. Right now, this assumes that
the user is already logged in and updates their account with the COOP details:
// src/OAuth2Demo/Client/Controllers/CoopOAuthController.php
// ...
public function receiveAuthorizationCode(Application $app, Request $request)
{
// ...
$meData = json_decode($response->getBody(), true);
$user = $this->getLoggedInUser();
$user->coopAccessToken = $accessToken;
$user->coopUserId = $meData['id'];
$this->saveUser($user);
// ...
}
But instead, let's actively allow anonymous users to go through the authorization process. And when they do, let's create a new user in our database:
public function receiveAuthorizationCode(Application $app, Request $request)
{
// ...
$meData = json_decode($response->getBody(), true);
if ($this->isUserLoggedIn()) {
$user = $this->getLoggedInUser();
} else {
$user = $this->createUser(
$meData['email'],
// a blank password - this user hasn't created a password yet!
'',
$meData['firstName'],
$meData['lastName']
);
}
$user->coopAccessToken = $accessToken;
$user->coopUserId = $meData['id'];
$user->coopAccessExpiresAt = $expiresAt;
$this->saveUser($user);
// ...
}
Some of these functions are specific to my app, but it's simple: if the user
isn't logged in, create and insert a new user record using the data from
the /api/me
endpoint.
Notice I'm giving the new user a blank password. Does that mean someone could login as the user by entering a blank password? That would be a huge security hole!
The problem is that the user isn't choosing a password. In fact, they're
opt'ing to not have one and to use their COOP account instead. So one way
or another, it should not be possible to login to this account using any
password. Normally, my passwords are encoded before being saved, like all
passwords should be. You can't see it here, but when the password is set
to a blank string, I'm skipping the encoding process and actually setting
the password
in the database to be blank. If someone does try to login
using a blank password, it'll be encoded first and won't match what's in the database.
As long as you find some way to prevent anyone from logging in as the user via a password, you're in good shape! You could also have the user choose a password right now or have an area to do that in their profile. I'll mention the first approach in a second.
Finally, let's log the user into this new account:
public function receiveAuthorizationCode(Application $app, Request $request)
{
// ...
if ($this->isUserLoggedIn()) {
$user = $this->getLoggedInUser();
} else {
$user = $this->createUser(
$meData['email'],
// a blank password - this user hasn't created a password yet!
'',
$meData['firstName'],
$meData['lastName']
);
$this->loginUser($user);
}
// ...
}
We still need to handle a few edge-cases, but this creates the user, logs them in, and then still updates them with the COOP details.
Let's try it out! Log out and then head over to the login page. Here, we'll
add a "Login with COOP" link. The template that renders this page is at views/user/login.twig
:
{# views/user/login.twig #}
<div class="form-group">
<div class="col-lg-10 col-lg-offset-2">
<button type="submit" class="btn btn-primary">Login!</button>
OR
<a href="{{ path('coop_authorize_start') }}"
class="btn btn-default">Login with COOP</a>
</div>
</div>
The URL for the link is the same as the "Authorize" button on the homepage. If you're already logged in, we'll just update your account. But if you're not, we'll create a new account and log you in. It's that simple!
Let's also completely reset the database, which you can do just by deleting
the data/topcluck.sqlite
file inside the client/
directory:
$ rm data/topcluck.sqlite
When we try it out, we're redirected to COOP, sent back to TopCluck, and are suddenly logged in. If we look at our user details, we can see we're logged in as Brent, with COOP User ID 2.
There's one big hole in our logic. If I logout and go through the process
again, it blows up! This time, it tries to create a second new user for
Brent instead of using the one from before. Let's fix that. For organization,
I'm going to create a new private function called findOrCreateUser()
in
this same class. If we can find a user with this COOP User ID, then we can
just log the user into that account. If not, we'll keep creating a new one:
public function receiveAuthorizationCode(Application $app, Request $request)
{
// ...
if ($this->isUserLoggedIn()) {
$user = $this->getLoggedInUser();
} else {
$user = $this->findOrCreateUser($meData);
$this->loginUser($user);
}
// ...
}
private function findOrCreateUser(array $meData)
{
if ($user = $this->findUserByCOOPId($meData['id'])) {
// this is an existing user. Yay!
return $user;
}
$user = $this->createUser(
$meData['email'],
// a blank password - this user hasn't created a password yet!
'',
$meData['firstName'],
$meData['lastName']
);
return $user;
}
Try the process again. No error this time - we find the existing user and use it instead of creating a new one.
There is one other edge-case. What if we don't find any users with this COOP user id, but there is already a user with this email? This might be because the user registered on TopCluck, but hasn't gone through the COOP authorization process.
Pretty easily, we can do another lookup by email:
private function findOrCreateUser(array $meData)
{
if ($user = $this->findUserByCOOPId($meData['id'])) {
// this is an existing user. Yay!
return $user;
}
if ($user = $this->findUserByEmail($meData['email'])) {
// we match by email
// we have to think if we should trust this. Is it possible to
// register at COOP with someone else's email?
return $user;
}
$user = $this->createUser(
$meData['email'],
// a blank password - this user hasn't created a password yet!
'',
$meData['firstName'],
$meData['lastName']
);
return $user;
}
Cool. But be careful. Is it easy to fake someone else's email address on COOP? If so, I could register with someone else's email there and then use this to login to that user's TopCluck account. With something other than COOP's own user id, you need to think about whether or not it's possible that you're getting falsified information. If you're not sure, it might be safe to break the process here and force the user to type in their TopCluck password for this account before linking them. That's a bit more work, but we do it here on KnpUniversity.com.
When you do have a new user, instead of just creating the account, you may want to show them a finish registration form. This would let them choose a password and fill out any other fields you want.
We've got more OAuth-focused things that we need to get to, so we'll leave
this to you. But the key is simple: store at least the coopAccessToken
,
coopUserId
and token expiration in the session and redirect to a registration
form with fields like email, password and anything else you need. You could
also store the email in the session and use it to prepopulate the form, or
even make another API request to /api/me
to get it. When they finally
submit a valid form, just create your user then. It's really just like any
registration form, except that you'll also save the COOP access token, user
id, and expiration when you create your user.
Hey @Akshit!
Ah, yes, this IS a big jump - you're right. And this is kind of a specialized topic - you either want to learn specifically about OAuth, or you don't.
After the basic PHP track, I would recommend jumping into our object-oriented track :) https://symfonycasts.com/tr...
Also, how did you go from the basic PHP track to this tutorial? I wouldn't recommend this jump - and I want to make sure that we're not accidentally linking from the basic PHP track to this tutorial.
Thanks!
Hi!
I never use Oauth to login, and am very confused with the UX logic here.
Is it really the regular way to do : when the user doesn't exist, we just create an account without telling him?
Or is it just an example and in real life login and registration are separate?
More generally, the "connect" feature seems to me very different than the "API access on behalfs" feature. It's actually so much mixed and the same thing?
Thanks !!!
Hey Francois,
Well, not exactly. When a new user (that wasn't registered on your website) is trying to login via OAuth - usually you may want to open kind of registration page, but prefill it with the data you get from the OAuth provider, e.g. email, name, etc. (usually we do not show password field for them as it's redundant, they already chose OAuth way that replaces password) so that users may edit this information if they needed and then complete registration. Only then you will create a new account for the user and link it to their OAuth provider.
But when the same user returns and trying to login again - this time you will already have their OAuth id in the DB, and you just log them in.
The workflow should be something like this.
About the connect feature - yes, it's a bit different. To connect OAuth provider, the user should be already logged in, and when they approve to connect their OAuth account - you just recored the OAuth ID on the current user, and so, when this user will login later via OAuth - you system will already has their OAuth ID in the DB and just log the related account in.
I hope this helps!
Cheers!
Hi Victor!
Thanks for these explanations!
And then, about what you wrote : "But when the same user returns and trying to login again - this time you will already have their OAuth id in the DB, and you just log them in."
Here, how do you check that it's really him who try to login? I'm missing a
piece I think
Thanks
Hey Francois,
Good question! Well, every time a user click on OAuth social button on your website - you're sending an API request to the OAuth provider, right? And so, the provider is sending back all the info about the users, like his OAuth ID, if the user approved the oauth authorization. That's how you know which user is trying to login via OAuth, you have his ID, and so you can search by the OAuth id in your DB and find the proper user that should be authenticated on your website.
I'd recommend you to look at https://github.com/knpunive... if you're interested in implementing OAuth in a Symfony application :)
Cheers!
Thanks!
Understood :)
Small feedback : it would be very good on the main presentation page of each courses to have the date of publication. There's no real way to know it otherwise, and so you don't really know what you sign for.
And 2nd feedback: It would be cool that you mention this oauth2-client-bundle in the course. Maybe it could be added in the main textual presentation (if you don't want to edit video)? I guess a lot of student who take the course today miss it
Hey Francois,
Great! :)
> Small feedback : it would be very good on the main presentation page of each courses to have the date of publication. There's no real way to know it otherwise, and so you don't really know what you sign for.
Thank you for leaving this little feedback! Yeah, I see your point, though we did it on purpose. This way we want to say that publication date isn't that much important and this content is still relevant. The more important to know which version are used in the course, and that's why we implemented a nice feature where you can click on main dependency name, e.g. on "Symfony 5.0" button on this course: https://symfonycasts.com/sc... - we will show the composer.json content with the exact versions installed. Unfortunately, this OAuth tutorial contains an old file structure, and this does not support this feature, but almost all our other tutorials support it. I hope this is helpful for you.
> And 2nd feedback: It would be cool that you mention this oauth2-client-bundle in the course. Maybe it could be added in the main textual presentation (if you don't want to edit video)? I guess a lot of student who take the course today miss it
Agree, I just double-checked and there's no links to that bundle on this tutorial, we only mention it in comments. So, we will think about a good place where to put a note linking to that bundle, thanks! :)
Cheers!
// src/OAuth2Demo/Client/Controllers/CoopOAuthController.php
// ...
public function receiveAuthorizationCode(Application $app, Request $request)
{
// ...
$meData = json_decode($response->getBody(), true);
$user = $this->getLoggedInUser();
$user->coopAccessToken = $accessToken;
$user->coopUserId = $meData['id'];
$this->saveUser($user);
// ...
Coming from the basic PHP track , this feels like a big jump. There should be an intermediate course in PHP.