Search code examples
basic-authenticationrakucro

Basic Authentication in perl6 with Cro


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;
            }
        }
    }
}

Solution

  • 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' {
                ...
            }
        }
    }