oetiker / mojolicious-plugin-spnego

Provide SPNEGO NTLM authentication to Mojolicious applications
Other
2 stars 4 forks source link

Unable to re-use auth code in full Mojo App #7

Closed hussam-qasem closed 1 year ago

hussam-qasem commented 1 year ago

In a full Mojo App, the example code works well if copied it inside each controller. However, I can't figure out how to call it during application startup. For example, in my startup:

my $r = $self->routes;
$r->get   ('/')->to('NP#index');

my $auth = $r->under('/' => sub ($c) {
        # Authenticated
        return 1 if $self->session('user');
        # NTLM Auth
        $c->ntlm_auth({
            auth_success_cb => sub {
            my $c = shift;
            my $user = shift;
            my $ldap = shift; # bound Net::LDAP::SPNEGO connection
            $c->session('user',$user->{samaccountname});
            $c->session('name',$user->{displayname});
            my $groups = $ldap->get_ad_groups($user->{samaccountname});
            $c->session('groups',[ sort keys %$groups]);
            return 1;
               }
                  });
        # Not authenticated
        $c->render(text => "You're not Authenticated", status => 401);
        return undef;
        });

$auth->post('/')->to('NP#search');

When I perform POST, no authentication occurs and I get the following errors:

Mojo::Reactor::Poll: I/O watcher failed: A response has already been rendered at /Users/hq/perl5/lib/perl5/Mojolicious/Controller.pm line 154.
[2023-04-12 13:22:21.71712] [84671] [trace] Inactivity timeout

If I comment-out the $c->render line, authentication occurs, but I get:

 Nothing has been rendered, expecting delayed response

and no results are rendered.

Would you kindly point me to where I've gone wrong?

Thank you!

oetiker commented 1 year ago

I think if you drop the "You're not Authenticated" line, things might work better for you

hussam-qasem commented 1 year ago

Thank you @oetiker for the prompt response. True, when I drop the render line, I do get authenticated; however, it goes no where afterwards. In the logs, I get:

 Nothing has been rendered, expecting delayed response
hussam-qasem commented 1 year ago

Here's a full example demonstrating the problem - just replace ad_server in startup:

1. script/my_app

#!/usr/bin/env perl

use strict;
use warnings;

use Mojo::File qw(curfile);
use lib curfile->dirname->sibling('lib')->to_string;
use Mojolicious::Commands;

Mojolicious::Commands->start_app('MyApp');

2. lib/MyApp.pm

package MyApp;
use Mojo::Base 'Mojolicious', -signatures;

# Run once at server start
sub startup ($self) {
    $self->secrets('s3cret');
    $self->plugin('SPNEGO',ad_server => 'my_ad_server.example.com');
    my $r = $self->routes;
    $r->get('/')->to('Example#index');
    my $auth = $r->under('/' => sub ($c) {
    # Authenticated
    return 1 if $c->session('user');
    # NTLM Auth
    $c->ntlm_auth({
        auth_success_cb => sub {
        my $c = shift;
        my $user = shift;
        my $ldap = shift; # bound Net::LDAP::SPNEGO connection
        $c->session('user',$user->{samaccountname});
        $c->session('name',$user->{displayname});
        my $groups = $ldap->get_ad_groups($user->{samaccountname});
        $c->session('groups',[ sort keys %$groups]);
        return 1;
           }
              });
    # Not authenticated
    #$c->render(text => "You're not Authenticated", status => 401);
    return undef;
             });
    $auth->post('/')->to('Example#search');
}

1;

3. lib/MyApp/Controller/Example.pm

package MyApp::Controller::Example;
use Mojo::Base 'Mojolicious::Controller', -signatures;

sub index ($self)
{
    return $self->render(inline => '<html><body><form method="post"><textarea name="numbers" maxlength="11">123</textarea><button type="submit">Go</button></form></body></html>');
}

sub search ($self)
{
    my $v = $self->validation;
    $v->required("numbers");
    return $self->render(text=>"Validation Error") if $v->has_error;
    my $numbers = $v->param("numbers");
    my @numbers = split(/\r?\n/, $numbers);

    Mojo::Promise
    ->map(
    {concurrency => 2},
    sub {
        $self->ua->get_p("https://httpbin.org/delay/1?q=$_" => {'api-key'=>'shhh'});
    }, @numbers)
    ->then(
    sub{
        my @results = @_;
        my @json = map { $_->[0]->res->json } @results;
        return $self->render(json => \@json);
    })
    ->catch(
    sub {
        my $err = shift;
        return $self->render(text => $err);
    })
    ->wait;
    #return $self->render(text => "This shall result is 'Unhandled rejected promise: A response has already been rendered'");
}

1;
oetiker commented 1 year ago

The problem with your code is that you are not looking at the result of the ntlm_auth code … since you are running this inside under I guess the following should do:

       my $ret = $c->ntlm_auth(
            auth_success_cb => sub {
                [...]
                return 1;
            }
        );
        if ($ret) {
           # if the call returns 1 the authentication has been successful, so 'under' must return nothing.
           return;
        } else {
           # if authentication is in progress, $ret is empty, so `under` must return 1 to stopp futher processing.
           return 1;
        }
hussam-qasem commented 1 year ago

Thank you @oetiker for your time and response.

What you suggested works! Now I need to fix my Mojolicious code and figure out how to render correctly from a promise.

Unhandled rejected promise: A response has already been rendered