Chapters
26 Chapters
|
2:18:26
|
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.
Login to bookmark this video
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
09.
Creating Token Resources in the API
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
Subscribe to jump to this part in the video!
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.
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
}
}
11 Comments
Ryan,
First off, thank you for these informative courses. I really enjoy learning about these techniques.
Now, in the part about checking the authentication and enabling the Silex HTTP Authentication (https://knpuniversity.com/s... ff.), when the 'http' argument is enabled, the API does not in fact return a API Problem response. Instead, it just sends an empty HTTP 401 response with content-type text/html. So technically, the feature should not pass in that case.
That can indeed be solved by using '$this->enforceUserSecurity()', however, in that case the 'http' argument on the firewall needs to be disabled (that is not said explicitly in this course).
Now as for my question: is it possible to use the Silex HTTP Authentication feature, and still return an API Problem response when the request is for one of the /api routes?
Actually, disabling the HTTP authentication makes the $this->enforceUserSecurity() call fail. So to keep the HTTP Basic authentication working, it does need to be enabled. However, in that case, not API Problem response is returned as said before.
Further investigation shows that the BasicAuthenticationListener of the Firewall catches the AuthenticationException and sets a response. Therefore, the error handler is not returned.
To solve this, it is necessary to register our own BasicAuthenticationListener for the API:
$this['security.authentication_listener.api.http'] = $app->share(function () use ($app) {
return new ApiBasicAuthenticationListener(
$app['security'],
$app['security.authentication_manager'],
'api',
$app['security.entry_point.api.http'],
$app['logger']
);
});
I just copied the default BasicAuthenticationListener, and removed the try/catch block around the authenticate call in its ::handle() method.
That makes sure that the AuthenticationException is thrown. Now our custom error listener is called. We have one more problem here: the error listener sets the statusCode to 500. Therefore, we have to manually register the ErrorListener, instead of using the Silex $app->error() method. Furthermore, the callback needs to handle the AuthenticationException. And finally, the priority of this event listener needs to be higher.
The full implementation of the new error listener callback is as follows:
private function configureListeners()
{
$app = $this;
$callback = function(GetResponseForExceptionEvent $event, $statusCode) use ($app) {
$exception = $event->getException();
// only act on /api URLs
if (strpos($app['request']->getPathInfo(), '/api') !== 0) {
return;
}
// allow 500 errors in debug to be thrown
if ($app['debug'] && $statusCode == 500) {
return;
}
if ($exception instanceof ApiProblemException) {
$apiProblem = $exception->getApiProblem();
} elseif ($exception instanceof AuthenticationException) {
$apiProblem = new ApiProblem(401, ApiProblem::TYPE_AUTHENTICATION_ERROR);
} else {
$apiProblem = new ApiProblem(
$statusCode
);
/*
* If it's an HttpException message (e.g. for 404, 403),
* we'll say as a rule that the exception message is safe
* for the client. Otherwise, it could be some sensitive
* low-level exception, which should *not* be exposed
*/
if ($exception instanceof HttpException) {
$apiProblem->set('detail', $exception->getMessage());
}
}
$response = $app['api.response_factory']->createResponse($apiProblem);
$event->setResponse($response);
$event->stopPropagation();
};
$this->on(KernelEvents::EXCEPTION, $callback, 100);
}
You might be able to use this if you decide to update this course.
In order to make sure that it is possible: to the extent possible under law,
Jacob Kiers
has waived all copyright and related or neighboring rights to
this post ( https://creativecommons.org... ).
Again, thanks for this great course!
-- Jacob
Hey Jacob!
Haha, wow - nice work. And you're right - the HTTP Basic error is a 401, but not an Api Problem response - I had overlooked that. The response *is* blank, so we could really probably be "ok" with this - the headers and status code tell the whole story.
But, in the interest of consistency, let's make this *also* return an API Problem response. The Symfony security stuff here is complex, but fortunately you don't need to override the BasicAuthenticationListener (but nice digging!). There is something called an "entry point", which is what's called when the system needs to tell the user "hey, you should login!". That's used inside the BasicAuthenticationListener, and we can use that. In fact, we already have an entry point setup for the "api_token" authentication system, and we can re-use it here.
The end result - summarized - looks like this: https://gist.github.com/wea....
Let me know if that works or doesn't make any sense at all :).
Cheers!
Hi Ryan. I tested that, and it works indeed. Furthermore, it seems that Silex overrides the entry point each time a new authentication provider is registered. So moving the "http => true" line above the "api_token => true" line works too. That is a bit hacky, though. Your solution is definitively cleaner.
Yep, you're right about the ordering. Silex is missing a setting where we can set the "entry point" at the firewall level explicitlyThe Symfony framework has this - it's probably missing from Silex just on accident.
Cheers!
I basically copypasted your TokenController and when sending a request to /tokens the addRoutes gets executed (adding some echoes there show in the response). However the newAction function is not called, adding an echo at the beginning (before enforcing security) doesn't show up. The server returns a 401 unauthorized. I am not sure of where is this error thrown. In the ApiTokenListener I can make sure that the basic auth headers are received properly, Authorization equals something of the form "Basic c35CWETCWERC2c52" (so now I understand why the "token ABCD123" ;D). However I am not sure of if this "c35..." has to equal the encoded password in the database (it does not). Also, if Basic auth were incorrect it would fail after enforceUserSecurity and that function never gets executed so the Unauthorized must come from somewhere else. Do you have any idea of what may I be doing wrong or how could I debug here?
I did change the config at the time to register the Security Provider but it works well with all the other Controllers... i have both 'http' and 'api_token' set to true...
Thanks!
Hey Joan!
I hope it's this easy: are you making a request to /api/tokens or just /tokens? If you're requesting to /tokens, that doesn't exist, but it *is* a secured path, so this *could* be the issue :). And since the "api" firewall is only active for URLs under /api, sending your token will not activate the ApiTokenListener.
Let me know!
Thanks for the prompt answer Ryan. As I don't intend to do a web version I haven't used the /api/ distinction. The requests look like this https://github.com/mezod/mu...
Therefore, my Application.php looks like follows:
$this->register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'main' => array(
'pattern' => '^/',
'form' => true,
'users' => $this->share(function () use ($app) {
return $app['repository.user'];
}),
'anonymous' => true,
'logout' => true,
'stateless' => true,
'http' => true,
'api_token' => true,
),
)
));
// require login for application management
$this['security.access_rules'] = array(
// allow anonymous API - if auth is needed, it's handled in the controller
array('^/', 'IS_AUTHENTICATED_ANONYMOUSLY'),
);
I'm not sure if you mean that /tokens is some kind of reserved path by Silex or similar. I have changed that by /apitokens or /api/tokens with no luck (same 401). I am not sure if it is that the route is not matched or if there's something in the config of the security that throws it off. It is a 401 Unauthorized but it is not returning any ApiProblem at all. Must be an exception thrown by Silex. But my access rules are basically accepting everything and anonymously. The ApiTokenListener is being activated, but as the Authorization is of the type "Basic XXXX" the listener just returns because he's got nothing to do there. I'll try to find it out tomorrow, but if you come up with any other idea I'll be glad to try it out!
Hey Joan!
If you send an API token to this endpoint (i.e. authenticate like you do with any other endpoint), does it work? If so, then we know the problem is quite simply that the HTTP Basic authentication isn't working - and that there's nothing special about this URL/controller that is causing issues.
If you're debugging this, I wouldn't look directly at the Authorization header, as I believe there's some encoding that happens, so it might be confusing (http://en.wikipedia.org/wik.... Instead, add some var_dump and die statements to the BasicAuthenticationListener class (deep in the core of Symfony) - it is the equivalent of ApiTokenListener, but for the HTTP Basic authentication. My guess is that the username/password is being sent incorrectly, or isn't matching up with how you're encoding/salting passwords in the db. Also, tail logs/development.log as some of what's going on in the security system *is* logged.
Good luck!
Sorry that I start a new thread, apparently disqus is not loading our previous conversation. I just came to share my stupidity with everyone x)
Ryan, as you suggested, making the request with a token worked. I narrowed it down to the fact that the passwords weren't matching, yesterday I spent the whole day trying to find out how did the encode actually work and also, I discarded the Salt as I wasn't using it, also got lost into the Security Component. But today, after a couple of tests in the same direction I thought, what if I am not saving the password properly? I dumped the encoded password and realized that it had 88 chars. My row in the database was limited to 50. Shame on me.
All of this made me realize that the validation via annotations doesn't work for the user password, because of all of this encoding process. Is there an obvious workaround?
In other words:
if ($errors = $this->validate($programmer)) {
$this->throwApiProblemValidationException($errors);
}
doesn't throw an Exception for a password with 1 character with this Annotations:
/**
* @Assert\Length(
* min = 8,
* max = 26,
* minMessage = "The password must be at least {{ limit }} characters long",
* maxMessage = "The password cannot be longer than {{ limit }} characters long"
* )
*/
public $password;
Hey Joan!
Yes, the Disqus issue is actually my fault - but I noticed it too and will fix it soon! Thanks for re-posting.
To answer your question about validation, what you typically have is 2 properties in this case: plainPassword (which is *not* persisted, but which *is* validated) and password (which is persisted, but not validated, and contains the encoded password). You can see that this solves you problem. It also solves the potential problem of setting a plain password on your property, somehow forgetting to encode it, and ending up with plain passwords in your database.
Cheers!
"Houston: no signs of life"
Start the conversation!