Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

Securing More Endpoints

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Securing More Endpoints

Hey! We have this great system where users are actually being authenticated! Now we can start checking for security everywhere we need it. In newAction we’re requiring that you are logged in:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function newAction(Request $request)
{
    if (!$this->isUserLoggedIn()) {
        throw new AccessDeniedException();
    }
    // ...
}

Awesome! In showAction and listAction we are going to leave those anonymous. In updateAction, we do need some extra security. It’s more than just being logged in: we need to check to see if our user is actually the owner of that programmer or not. So we just add some if statement logic: if ($programmer->userId != $this->getLoggedInUser()->id, then throw new AccessDeniedException:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function updateAction($nickname, Request $request)
{
    $programmer = $this->getProgrammerRepository()->findOneByNickname($nickname);

    if ($programmer->userId != $this->getLoggedInUser()->id) {
        throw new AccessDeniedException();
    }

    // ...
}

Easy enough!

Centralizing Security Logic Checks

Since we’re also going to use this in deleteAction let’s go into our BaseController and make this a generic function. Open up the BaseController, create a new protected function enforceProgrammerOwnershipSecurity. Let’s copy the logic in there and don’t forget to add your AccessDeniedException use statement:

// src/KnpU/CodeBattle/Controller/BaseController.php
// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...

protected function enforceProgrammerOwnershipSecurity(Programmer $programmer)
{
    if ($this->getLoggedInUser()->id != $programmer->userId) {
        throw new AccessDeniedException();
    }
}

Perfect, so now go back to our ProgrammerController. It’s a lot easier to just reuse this logic. Let’s also use this down in deleteAction:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function updateAction($nickname, Request $request)
{
    $programmer = $this->getProgrammerRepository()->findOneByNickname($nickname);
    // ...

    $this->enforceProgrammerOwnershipSecurity($programmer);

    // ...
}

public function deleteAction($nickname)
{
    $programmer = $this->getProgrammerRepository()->findOneByNickname($nickname);
    // ...

    $this->enforceProgrammerOwnershipSecurity($programmer);

    // ...
}

Now the only other thing that could go wrong, is if the user is not logged in at all and they hit updateAction. Then, we would die inside this function. The problem is that $this->getLoggedInUser would be null and we’ll call the id property on a null object:

// src/KnpU/CodeBattle/Controller/BaseController.php
// ...

protected function enforceProgrammerOwnershipSecurity(Programmer $programmer)
{
    if ($this->getLoggedInUser()->id != $programmer->userId) {
        throw new AccessDeniedException();
    }
}

So before we call this function, we need to make sure the user is at least logged in. If they aren’t, then they are definitely not the owner of this programmer.

So let’s create another function here called enforceUserSecurity. In this case, go back to ProgrammerController and grab the logic right here:

// src/KnpU/CodeBattle/Controller/BaseController.php
// ...

protected function enforceUserSecurity()
{
    if (!$this->isUserLoggedIn()) {
        throw new AccessDeniedException();
    }
}

There we go. And from inside enforceProgrammerOwnershipSecurity we can just make sure that the user is actually logged in:

// src/KnpU/CodeBattle/Controller/BaseController.php
// ...

protected function enforceProgrammerOwnershipSecurity(Programmer $programmer)
{
    $this->enforceUserSecurity();

    if ($this->getLoggedInUser()->id != $programmer->userId) {
        throw new AccessDeniedException();
    }
}

And in ProgrammerController, we can do the same thing and save ourselves a little bit of code:

// src/KnpU/CodeBattle/Controller/Api/ProgrammerController.php
// ...

public function newAction(Request $request)
{
    $this->enforceUserSecurity();
    // ...
}

Between these two new methods, we have a really easy way to go function by function inside of our controller to make sure that we’re enforcing the right type of security.

Because we’re sending our Authorization header in the background of our scenarios, we should be able to run our entire programmer.feature and see it pass:

php vendor/bin/behat features/api/programmer.feature

Perfect! And just like that, we have our entire application locked down.

Leave a comment!

4
Login or Register to join the conversation
Default user avatar
Default user avatar Miroslav Yovchev | posted 5 years ago

Just one small correction - in the video the ownership check in the delete action is done on the wrong place. It should be:

if ($programmer) {
$this->enforceProgrammerOwnershipSecurity($programmer);
$this->delete($programmer);
}
In the way it's done, it would throw runtime error (method argument is not nullable).

Additionally I would suggest adding not found check in delete action (and keep ownership check where it is).
The behat test to be matched:

Scenario: DELETE a missing programmer
When I request "DELETE /api/programmers/DeletedUnitTester"
Then the response status code should be 404

Devil is in the details :) Not sure if it's not again the standards though...

Cheers!

Reply

Yo Miroslav!

Yea, you're 100% right on that security check - it should go *inside* the `if` - exactly like you've shown. Thanks for pointing that out!

About the DELETE for a missing programmer causing a 404, it's subjective. I tend to favor your approach - returning a 404 when the programmer doesn't exist. But others say - if the programmer doesn't exist, then it's kind of "already deleted" - and they return the 204. That's what we did here, mostly to show how you can make those interesting decisions. But as far as I've seen, the standards to force one way or the other.

Cheers back!

Reply
Default user avatar

is there any important reason I am missing for placing "enforceProgrammerOwnershipSecurity()" in the BaseController? In theory it will be only used in ProgrammerController right?

Reply

No, you're thinking correctly - there is nothing right here that makes this necessary. You could think of it as "planning ahead in case", but I usually don't centralize things until I actually need them. In this case, my aim was really to get the enforceUserSecurity method in the BaseController, so enforceProgrammerOwnerSecurity naturally went there too (bit it didn't need to).

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice