Basic Authentication in perl6 with Cro - basic-authentication

I am looking for a simple solution to protect my routes with the Basic Authentication mechanism with Cro. In my example I'd like to see a 401 Unauthorized if you don't provide any credentials at all. If you provide wrong credentials I like to see a 403 Forbidden
In my code example I never saw the MyBasicAuth middleware being called:
class MyUser does Cro::HTTP::Auth {
has $.username;
}
subset LoggedInUser of MyUser where { .username.defined }
class MyBasicAuth does Cro::HTTP::Auth::Basic[MyUser, "username"] {
method authenticate(Str $user, Str $pass --> Bool) {
# No, don't actually do this!
say "authentication called";
my $success = $user eq 'admin' && $pass eq 'secret';
forbidden without $success;
return $success
}
}
sub routes() is export {
my %storage;
route {
before MyBasicAuth.new;
post -> LoggedInUser $user, 'api' {
request-body -> %json-object {
my $uuid = UUID.new(:version(4));
%storage{$uuid} = %json-object;
created "api/$uuid", 'application/json', %json-object;
}
}
}
}

This structure:
route {
before MyBasicAuth.new;
post -> LoggedInUser $user, 'api' {
...
}
}
Depends on the new before/after semantics in the upcoming Cro 0.8.0. In the current Cro release at the time of asking/writing - and those prior to it - before in a route block would apply only to routes that had already been matched. However, this was too late for middleware that was meant to impact what would match. The way to do this prior to Cro 0.8.0 is to either mount the middleware at server level, or to do something like this:
route {
before MyBasicAuth.new;
delegate <*> => route {
post -> LoggedInUser $user, 'api' {
...
}
}
}
Which ensures that the middleware is applied before any route matching is considered. This isn't so pretty, thus the changes in the upcoming 0.8.0 (which also will introduce a before-matched that has the original before semantics).
Finally, forbidden without $success; is not going to work here. The forbidden sub is part of Cro::HTTP::Router and for use in route handlers, whereas middleware is not tied to the router (so you could decide to route requests in a different way, for example, without losing the ability to use all of the middleware). The contract of the authenticate method is that it returns a truthy value determining what should happen; it's not an appropriate place to try and force a different response code.
A failure to match an auth constraint like LoggedInUser will produce a 401. To rewrite that, add an after in the outermost route block to map it:
route {
before MyBasicAuth.new;
after { forbidden if response.status == 401; }
delegate <*> => route {
post -> LoggedInUser $user, 'api' {
...
}
}
}

It seems that there is currently a bug in the cro release version which is already fixed in upstream on github. With the help of sena_kun from the #perl6 IRC channel we did use the current version of cro-http:
$ git clone https://github.com/croservices/cro-http.git
$ perl6 -Icro-http/lib example.p6
And then with curl i finally saw the "authentication called".
We've discovered another two little bugs:
The first one: When I invoked curl -v -d '{"string":"hi"}' http://admin:secret#localhost:10000/api I forgot to add -H 'Content-Type: application/json'
authentication called An operation first awaited: in sub request-body at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Router.pm6 (Cro::HTTP::Router) line 716 in block at restapp/example.pl line 24 in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Router.pm6 (Cro::HTTP::Router) line 130
Died with the exception:
Cannot unbox a type object (Nil) to int.
in sub decode-payload-to-pairs at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/BodyParsers.pm6 (Cro::HTTP::BodyParsers) line 62
in method parse at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/BodyParsers.pm6 (Cro::HTTP::BodyParsers) line 50
in method body at /home/martin/.rakudobrew/moar-2018.08/install/share/perl6/site/sources/557D6C932894CB1ADE0F83C0596851F9212C2A67 (Cro::MessageWithBody) line 77
in sub request-body at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Router.pm6 (Cro::HTTP::Router) line 716
in block at restapp/example.pl line 24
in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Router.pm6 (Cro::HTTP::Router) line 130
The second one was because of calling forbidden in this part:
class MyBasicAuth does Cro::HTTP::Auth::Basic[MyUser, "username"] {
method authenticate(Str $user, Str $pass --> Bool) {
say "authentication called";
my $success = $user eq 'admin' && $pass eq 'secret';
forbidden unless $success;
return $success;
} }
Which leads to this stacktrace:
authentication called
Can only use 'content' inside of a request handler
in sub set-status at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Router.pm6 (Cro::HTTP::Router) line 846
in sub forbidden at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Router.pm6 (Cro::HTTP::Router) line 823
in method authenticate at restapp/example.pl line 15
in method process-auth at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Auth/Basic.pm6 (Cro::HTTP::Auth::Basic) line 26
in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Auth/Basic.pm6 (Cro::HTTP::Auth::Basic) line 11
in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Auth/Basic.pm6 (Cro::HTTP::Auth::Basic) line 8
in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/Internal.pm6 (Cro::HTTP::Internal) line 22
in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/RequestParser.pm6 (Cro::HTTP::RequestParser) line 109
in block at /home/martin/.workspace/voteimproved/cro-http/lib/Cro/HTTP/RequestParser.pm6 (Cro::HTTP::RequestParser) line 90
in block at /home/martin/.rakudobrew/moar-2018.08/install/share/perl6/site/sources/F048BB66854D2463798A39CC2B01D4CC1532F957 (Cro::TCP) line 53
I think this bugs will be fixed soon!

Related

Symfony redirect to external URL

How can I redirect to an external URL within a symfony action?
I tried this options :
1- return $this->redirect("www.example.com");
Error : No route found for "GET /www.example.com"
2- $this->redirect("www.example.com");
Error : The controller must return a response (null given).
3- $response = new Response();
$response->headers->set("Location","www.example.com");
return $response
No Error but blank page !
Answer to your question is in official Symfony book.
http://symfony.com/doc/current/book/controller.html#redirecting
public function indexAction()
{
return $this->redirect('http://stackoverflow.com');
// return $this->redirect('http://stackoverflow.com', 301); - for changing HTTP status code from 302 Found to 301 Moved Permanently
}
What is the "URL"? Do you have really defined route for this pattern? If not, then not found error is absolutelly correct. If you want to redirect to external site, always use absolute URL format.
You have to use RedirectResponse instead of Response
use Symfony\Component\HttpFoundation\RedirectResponse;
And then:
return new RedirectResponse('http://your.location.com');

Matching a URL to a route in Symfony

We have files behind authentication, and I want to do different things for post-authentication redirect if the user entered the application using a URL of a file versus a URL of an HTML resource.
I have a URL: https://subdomain.domain.com/resource/45/identifiers/567/here/11abdf51e3d7-some%20file%20name.png/download. I want to get the route name for this URL.
app/console router:debug outputs this: _route_name GET ANY subdomain.domain.{tld} /resource/{id2}/identifiers/{id2}/here/{id3}/download.
Symfony has a Routing component (http://symfony.com/doc/current/book/routing.html), and I'm trying to call match() on an instance of Symfony\Bundle\FrameworkBundle\Routing\Router as provided by Symfony IOC. I have tried with with the domain and without the domain, but they both create a MethodNotAllowed exception because the route cannot be found. How can I match this URL to a route?
Maybe a bit late but as I was facing the same problem, what I come to is something like
$request = Request::create($targetPath, Request::METHOD_GET, [], [], [], $_SERVER);
try {
$matches = $router->matchRequest($request);
} catch (\Exception $e) {
// throw a 400
}
The key part is to use $_SERVER superglobal array in order to have all things setted straight away.
According to this, Symfony uses current request's HTTP method while matching. I guess your controller serves POST request, while your download links are GET.
The route name is available in the _route_name attribute of the Request object: $request->attributes->get('_route_name').
You can do something like this ton get the route name:
public/protected/private function getRefererRoute(Request $request = null)
{
if ($request == null)
$request = $this->getRequest();
//look for the referer route
$referer = $request->headers->get('referer');
$path = substr($referer, strpos($referer, $request->getBaseUrl()));
$path = str_replace($request->getBaseUrl(), '', $lastPath);
$matcher = $this->get('router')->getMatcher();
$parameters = $matcher->match($path);
$route = $parameters['_route'];
return $route;
}
EDIT:
I forgot to explain what I was doing. So basicly you are getting the page url ($referer) then taking out your website's base url with str_replace and then trying to match the remaining part of the path with a know route pattern using route matcher.
EDIT2:
Obviously you need to have this inside you controller if you want to be able to use $this->get(...)

Supress Error messages in Symfony/Twig?

How can I suppress error messages in a symfony controller / swift message, e.g. if the email of the user doesn't exist, something is wrong with the smpt server or the email address simply doesn't comply with RFC 2822.
For example, we got following CRITICAL error...
request.CRITICAL: Swift_RfcComplianceException: Address in mailbox given [ xxx#xxx.com ] does not comply with RFC 2822, 3.6.2. (uncaught exception) at $
... the user then get's a symfony error page "An Error occured" which I need to suppress in any case.
A simple #$this->get('mailer')->send($message); doesn't work here unfortunately ...
protected function generateEmail($name)
{
$user = $this->getDoctrine()
->getRepository('XXX')
->findOneBy(array('name' => $name));
if (!$user) {
exit();
}
else {
$message = \Swift_Message::newInstance()
->setSubject('xxx')
->setFrom(array('xxx#xxx.com' => 'xxx'))
->setTo($user->getEmail())
->setContentType('text/html')
->setBody(
$this->renderView(
'AcmeBundle:Template:mail/confirmed.html.twig'
)
)
;
$this->get('mailer')->send($message);
// a simple #$this->get('mailer')->send($message); doesn't work here
}
return true;
}
To simply suppress the error, wrap the send method in a try-catch block. Choose the exception type wisely. The following example just catches Swift_RfcComplicanceExceptions.
try {
$this->get('mailer')->send($message);
} catch (\Swift_RfcComplianceException $exception) {
// do something like proper error handling or log this error somewhere
}
I'd consider it advisably to apply some validation beforehand, so you can display a nice error to your user.
I'm not sure that a try catch block will work, because mails may be sent later in the request process : http://symfony.com/fr/doc/current/components/http_kernel/introduction.html#l-evenement-kernel-terminate
If you use the framework Full Stack edition, this is the default behavior.
I've also found some refs here : Unable to send e-mail from within custom Symfony2 command but can from elsewhere in app
You can change the Swiftmailer spool strategy in your config to send direct mails...

Meteor.http.call gives not allowed by Access-Control-Allow-Origin

When I try to call an external server for JSON queries in Meteor with the Meteor.http.call("GET") method I get the error message "not allowed by Access-Control-Allow-Origin".
How do I allow my meteor app to make HTTP calls to other servers?
Right now I run it on localhost.
The code I run is this:
Meteor.http.call("GET",
"http://api.vasttrafik.se/bin/rest.exe/v1/location.name?authKey=XXXX&format=json&jsonpCallback=processJSON&input=kungsportsplatsen",
function(error, result) {
console.log("test");
}
);
There are other questions similar to this on StackOverflow.
You're restricted by the server you're trying to connect to when you do this from the client side (AJAX).
One way to solve it is if you have access to the external server, you can modify the header file to allow some, or all origins by:
Access-Control-Allow-Origin: *
However, if you place the call on the server side and not provide a callback function, the call will be made synchronously, thus not with AJAX, and it should succeed.
Here's
Meteor.methods({checkTwitter: function (userId) {
this.unblock();
var result = Meteor.http.call("GET", "http://api.twitter.com/xyz", {params: {user: userId}});
if (result.statusCode === 200) return true
return false;
}});

Symfony2 Templating without request

I'm trying to send an email from a ContainerAwareCommand in Symfony2. But I get this exception when the email template is render by:
$body = $this->templating->render($template, $data);
Exception:
("You cannot create a service ("templating.helper.assets") of an inactive scope ("request").")
I found in github that this helper need the request object. Anybody knows how can I to instance the Request object?
You need to set the container into the right scope and give it a (fake) request. In most cases this will be enough:
//before you render template add bellow code
$this->getContainer()->enterScope('request');
$this->getContainer()->set('request', new Request(), 'request');
The full story is here. If you want to know the details read this issue on github.
The problem arises because you use asset() function in your template.
By default, asset() relies on Request service to generate urls to your assets (it needs to know what is the base path to you web site or what is the domain name if you use absolute asset urls, for example).
But when you run your application from command line there is no Request.
One way to fix this it to explicitely define base urls to your assets in config.yml like this:
framework:
templating:
assets_base_urls: { http: ["http://yoursite.com"], ssl: ["http://yoursite.com"] }
It is important to define both http and ssl, because if you omit one of them asset() will still depend on Request service.
The (possible) downside is that all urls to assets will now be absolute.
Since you don't have a request, you need to call the templating service directly like this:
$this->container->get('templating')->render($template, $data);
Following BetaRide's answer put me on the right track but that wasn't sufficient. Then it was complaining: "Unable to generate a URL for the named route "" as such route does not exist."
To create a valid request I've modified it to request the root of the project like so:
$request = new Request();
$request->create('/');
$this->container->enterScope('request');
$this->container->set('request', $request, 'request');
You might need to call a different route (secured root?), root worked for me just fine.
Symfony2 Docs
Bonus addition:
I had to do so much templating/routing in cli through Symfony2 commands that I've updated the initializeContainer() method in AppKernel. It creates a route to the root of the site, sets the router context and fakes a user login:
protected function initializeContainer()
{
parent::initializeContainer();
if (PHP_SAPI == 'cli') {
$container = $this->getContainer();
/**
* Fake request to home page for cli router.
* Need to set router base url to request uri because when request object
* is created it perceives the "/portal" part as path info only, not base
* url and thus router will not include it in the generated url's.
*/
$request = Request::create($container->getParameter('domain'));
$container->enterScope('request');
$container->set('request', $request, 'request');
$context = new RequestContext();
$context->fromRequest($request);
$container->get('router')->setContext($context);
$container->get('router')->getContext()->setBaseUrl($request->getRequestUri());
/**
* Fake admin user login for cli. Try database read,
* gracefully print error message if failed and continue.
* Continue mainly for doctrine:fixture:load when db still empty.
*/
try {
$user = $container->get('fos_user.user_manager')->findUserByUsername('admin');
if ($user !== null) {
$token = $token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->getContainer()->get('security.token_storage')->setToken($token);
}
} catch (\Exception $e) {
echo "Fake Admin user login failed.\n";
}
}
}
You might not need the last $container->get('router')->getContext()->setBaseUrl($request->getRequestUri()); part, but I had to do it because my site root was at domain.com/siteroot/ and the router was stripping /siteroot/ away for url generation.

Resources