9

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;
            }
        }
    }
}
jjmerelo
  • 22,578
  • 8
  • 40
  • 86
Martin Barth
  • 796
  • 4
  • 13
  • what I ment is that it seems not to be invoked. There is a `say "authentication called";`in the Middleware. (And it does not work as expected regarding the `401`and `403`) – Martin Barth Oct 30 '18 at 10:45
  • works not on my box: > curl -v -X POST -H "Content-Type: application/json" http://admin:secret@localhost/api -d '{"string": "hi"}' < HTTP/1.1 401 Unauthorized > curl -v -X POST -H "Content-Type: application/json" -H 'Authorization: Basic YWRtaW46c2VjcmV0' http://localhost/api -d '{"string": "hi"}' < HTTP/1.1 401 Unauthorized – Martin Barth Oct 30 '18 at 11:10

2 Answers2

6

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' {
            ...
        }
    }
}
Jonathan Worthington
  • 29,104
  • 2
  • 97
  • 136
3

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!

Martin Barth
  • 796
  • 4
  • 13