Merge "Multipage Tour (with session-cookie to next page)"
diff --git a/Changes b/Changes
index 7948840..6efd47c 100755
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.34 2019-06-24
+0.34 2019-06-26
- Introduced guided tour (hebasta, #19).
- Updated dependency on M::P::Notifications to
be compatible with recent versions of Mojolicious.
@@ -6,6 +6,7 @@
- Improve QueryCreator to single-quote-escape special
characters in orth-line and include more symbols.
- Remove deprecated auth_support support.
+ - Add OAuth2 password grand flow.
0.33 2019-03-28
- Fix problem with serialization and deserialization
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 1add8f5..8f770a7 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -5,6 +5,8 @@
# TODO:
# CSRF-protect logout!
+our $EXPECTED_EXPIRATION_IN = 259200;
+
# Register the plugin
sub register {
my ($plugin, $app, $param) = @_;
@@ -24,11 +26,10 @@
});
};
-
- # unless ($param->{client_id} && $param->{client_secret}) {
- # $mojo->log->error('client_id or client_secret not defined');
- # return;
- # };
+ # Get the client id and the client_secret as a requirement
+ unless ($param->{client_id} && $param->{client_secret}) {
+ $app->log->error('client_id or client_secret not defined');
+ };
# TODO:
# Define user CHI cache
@@ -43,7 +44,8 @@
logoutSuccess => 'Abmeldung erfolgreich',
logoutFail => 'Abmeldung fehlgeschlagen',
csrfFail => 'Fehlerhafter CSRF Token',
- openRedirectFail => 'Weiterleitungsfehler'
+ openRedirectFail => 'Weiterleitungsfehler',
+ refreshFail => 'Fehlerhafter Refresh-Token'
},
-en => {
loginSuccess => 'Login successful',
@@ -51,7 +53,8 @@
logoutSuccess => 'Logout successful',
logoutFail => 'Logout failed',
csrfFail => 'Bad CSRF token',
- openRedirectFail => 'Redirect failure'
+ openRedirectFail => 'Redirect failure',
+ refreshFail => 'Bad refresh token'
}
}
}
@@ -80,6 +83,10 @@
my $auth_token = $c->auth->token or return;
my $h = $tx->req->headers;
$h->header('Authorization' => $auth_token);
+
+ # TODO:
+ # When a request fails because the access token timed out,
+ # rerequest with the refresh token.
}
);
@@ -107,134 +114,280 @@
# Log in to the system
my $r = $app->routes;
- $r->post('/user/login')->to(
- cb => sub {
- my $c = shift;
- # Validate input
- my $v = $c->validation;
- $v->required('handle_or_email', 'trim');
- $v->required('pwd', 'trim');
- $v->csrf_protect;
- $v->optional('fwd')->closed_redirect;
+ if ($param->{oauth2}) {
- my $user = $v->param('handle_or_email');
- my $fwd = $v->param('fwd');
+ my $client_id = $param->{client_id};
+ my $client_secret = $param->{client_secret};
- # Set flash for redirect
- $c->flash(handle_or_email => $user);
+ # This refreshes an oauth2 token and
+ # returns a promise
+ $app->helper(
+ 'auth.refresh_token' => sub {
+ my $c = shift;
+ my $refresh_token = shift;
- if ($v->has_error || index($user, ':') >= 0) {
- if ($v->has_error('fwd')) {
- $c->notify(error => $c->loc('Auth_openRedirectFail'));
- }
- elsif ($v->has_error('csrf_token')) {
- $c->notify(error => $c->loc('Auth_csrfFail'));
- }
- else {
- $c->notify(error => $c->loc('Auth_loginFail'));
+ unless ($refresh_token) {
+ return Mojo::Promise->reject({message => 'Missing refresh token'})
};
- return $c->relative_redirect_to($fwd // 'index');
+ # Get OAuth access token
+ my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+
+ return $c->korap_request('POST', $url, {} => form => {
+ grant_type => 'refresh_token',
+ client_id => $client_id,
+ client_secret => $client_secret,
+ refresh_token => $refresh_token
+ })->then(
+ sub {
+ # Set the tokens and return a promise
+ return $plugin->set_tokens(
+ $c,
+ shift->result->json
+ )
+ }
+ );
}
+ );
- my $pwd = $v->param('pwd');
+ # Password flow
+ $r->post('/user/login')->to(
+ cb => sub {
+ my $c = shift;
- $c->app->log->debug("Login from user $user:$pwd");
+ # Validate input
+ my $v = $c->validation;
+ $v->required('handle_or_email', 'trim');
+ $v->required('pwd', 'trim');
+ $v->csrf_protect;
+ $v->optional('fwd')->closed_redirect;
- my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
+ my $user = $v->param('handle_or_email');
+ my $fwd = $v->param('fwd');
- # Korap request for login
- $c->korap_request('get', $url, {
+ # Set flash for redirect
+ $c->flash(handle_or_email => $user);
- # Set authorization header
- Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
-
- })->then(
- sub {
- my $tx = shift;
-
- # Get the java token
- my $jwt = $tx->result->json;
-
- # No java web token
- unless ($jwt) {
- $c->notify(error => 'Response is no valid JWT (remote)');
- return;
+ if ($v->has_error || index($user, ':') >= 0) {
+ if ($v->has_error('fwd')) {
+ $c->notify(error => $c->loc('Auth_openRedirectFail'));
+ }
+ elsif ($v->has_error('csrf_token')) {
+ $c->notify(error => $c->loc('Auth_csrfFail'));
+ }
+ else {
+ $c->notify(error => $c->loc('Auth_loginFail'));
};
- # There is an error here
- # Dealing with errors here
- if (my $error = $jwt->{error} // $jwt->{errors}) {
- if (ref $error eq 'ARRAY') {
- foreach (@$error) {
- unless ($_->[1]) {
- $c->notify(error => $c->loc('Auth_loginFail'));
- }
- else {
- $c->notify(error => $_->[0] . ($_->[1] ? ': ' . $_->[1] : ''));
- };
- };
- }
- else {
- $c->notify(error => 'There is an unknown JWT error');
- };
- return;
- };
-
- # TODO: Deal with user return values.
- my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
-
- $c->app->log->debug(qq!Login successful: "$user" with "$auth"!);
-
- $user = $jwt->{username} ? $jwt->{username} : $user;
-
- # Set session info
- $c->session(user => $user);
- $c->session(auth => $auth);
-
- # Set stash info
- $c->stash(user => $user);
- $c->stash(auth => $auth);
-
- # Set cache
- $c->chi('user')->set($auth => $user);
- $c->notify(success => $c->loc('Auth_loginSuccess'));
- }
- )->catch(
- sub {
- my $e = shift;
-
- # Notify the user
- $c->notify(
- error =>
- ($e->{code} ? $e->{code} . ': ' : '') .
- $e->{message} . ' for Login (remote)'
- );
-
- # Log failure
- $c->app->log->debug(
- ($e->{code} ? $e->{code} . ' - ' : '') .
- $e->{message}
- );
-
- $c->app->log->debug(qq!Login fail: "$user"!);
- $c->notify(error => $c->loc('Auth_loginFail'));
- }
- )->finally(
- sub {
-
- # Redirect to slash
return $c->relative_redirect_to($fwd // 'index');
}
- )
- # Start IOLoop
- ->wait;
+ my $pwd = $v->param('pwd');
- return 1;
- }
- )->name('login');
+ $c->app->log->debug("Login from user $user:XXXX");
+
+ # <specific>
+
+ # Get OAuth access token
+ my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+
+ # Korap request for login
+ $c->korap_request('post', $url, {}, form => {
+ grant_type => 'password',
+ username => $user,
+ password => $pwd,
+ client_id => $client_id,
+ client_secret => $client_secret
+ })->then(
+ sub {
+ # Set the tokens and return a promise
+ return $plugin->set_tokens(
+ $c,
+ shift->result->json
+ )
+ }
+ )->catch(
+ sub {
+
+ # Notify the user on login failure
+ unless (@_) {
+ $c->notify(error => $c->loc('Auth_loginFail'));
+ }
+
+ # There are known errors
+ foreach (@_) {
+ if (ref $_ eq 'HASH') {
+ my $err = ($_->{code} ? $_->{code} . ': ' : '') .
+ $_->{message};
+ $c->notify(error => $err);
+ # Log failure
+ $c->app->log->debug($err);
+ }
+ else {
+ $c->notify(error => $_);
+ $c->app->log->debug($_);
+ };
+ };
+
+ $c->app->log->debug(qq!Login fail: "$user"!);
+ }
+ )->then(
+ sub {
+ # Set user info
+ $c->session(user => $user);
+ $c->stash(user => $user);
+
+ # Notify on success
+ $c->app->log->debug(qq!Login successful: "$user"!);
+ $c->notify(success => $c->loc('Auth_loginSuccess'));
+ }
+ )->finally(
+ sub {
+ # Redirect to slash
+ return $c->relative_redirect_to($fwd // 'index');
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+
+ return 1;
+ }
+ )->name('login');
+ }
+ # Use JWT login
+ else {
+
+ $r->post('/user/login')->to(
+ cb => sub {
+ my $c = shift;
+
+ # Validate input
+ my $v = $c->validation;
+ $v->required('handle_or_email', 'trim');
+ $v->required('pwd', 'trim');
+ $v->csrf_protect;
+ $v->optional('fwd')->closed_redirect;
+
+ my $user = $v->param('handle_or_email');
+ my $fwd = $v->param('fwd');
+
+ # Set flash for redirect
+ $c->flash(handle_or_email => $user);
+
+ if ($v->has_error || index($user, ':') >= 0) {
+ if ($v->has_error('fwd')) {
+ $c->notify(error => $c->loc('Auth_openRedirectFail'));
+ }
+ elsif ($v->has_error('csrf_token')) {
+ $c->notify(error => $c->loc('Auth_csrfFail'));
+ }
+ else {
+ $c->notify(error => $c->loc('Auth_loginFail'));
+ };
+
+ return $c->relative_redirect_to($fwd // 'index');
+ }
+
+ my $pwd = $v->param('pwd');
+
+ $c->app->log->debug("Login from user $user:XXXX");
+
+ my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
+
+ # Korap request for login
+ $c->korap_request('get', $url, {
+
+ # Set authorization header
+ Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
+
+ })->then(
+ sub {
+ my $tx = shift;
+
+ # Get the java token
+ my $jwt = $tx->result->json;
+
+ # No java web token
+ unless ($jwt) {
+ $c->notify(error => 'Response is no valid JWT (remote)');
+ return;
+ };
+
+ # There is an error here
+ # Dealing with errors here
+ if (my $error = $jwt->{error} // $jwt->{errors}) {
+ if (ref $error eq 'ARRAY') {
+ foreach (@$error) {
+ unless ($_->[1]) {
+ $c->notify(error => $c->loc('Auth_loginFail'));
+ }
+ else {
+ $c->notify(error => $_->[0] . ($_->[1] ? ': ' . $_->[1] : ''));
+ };
+ };
+ }
+ else {
+ $c->notify(error => 'There is an unknown JWT error');
+ };
+ return;
+ };
+
+ # TODO: Deal with user return values.
+ my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
+
+ $c->app->log->debug(qq!Login successful: "$user" with "$auth"!);
+
+ $user = $jwt->{username} ? $jwt->{username} : $user;
+
+ # Set session info
+ $c->session(user => $user);
+ $c->session(auth => $auth);
+
+ # Set stash info
+ $c->stash(user => $user);
+ $c->stash(auth => $auth);
+
+ # Set cache
+ $c->chi('user')->set($auth => $user);
+ $c->notify(success => $c->loc('Auth_loginSuccess'));
+ }
+ )->catch(
+ sub {
+ my $e = shift;
+
+ # Notify the user
+ $c->notify(
+ error =>
+ ($e->{code} ? $e->{code} . ': ' : '') .
+ $e->{message} . ' for Login (remote)'
+ );
+
+ # Log failure
+ $c->app->log->debug(
+ ($e->{code} ? $e->{code} . ' - ' : '') .
+ $e->{message}
+ );
+
+ $c->app->log->debug(qq!Login fail: "$user"!);
+ $c->notify(error => $c->loc('Auth_loginFail'));
+ }
+ )->finally(
+ sub {
+
+ # Redirect to slash
+ return $c->relative_redirect_to($fwd // 'index');
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+
+ return 1;
+ }
+ )->name('login');
+ };
# Log out of the session
@@ -244,6 +397,9 @@
# TODO: csrf-protection!
+ # TODO:
+ # Revoke refresh token!
+
# Log out of the system
my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
@@ -254,7 +410,14 @@
sub {
my $tx = shift;
# Clear cache
- $c->chi('user')->remove($c->auth->token);
+ # ?? Necesseary
+ # $c->chi('user')->remove($c->auth->token);
+
+ # TODO:
+ # Revoke refresh token!
+ # based on auth token!
+ # my $refresh_token = $c->chi('user')->get('refr_' . $c->auth->token);
+ # $c->auth->revoke_token($refresh_token)
# Expire session
$c->session(user => undef);
@@ -284,6 +447,62 @@
)->name('logout');
};
+# Sets a requested token and returns
+# an error, if it didn't work
+sub set_tokens {
+ my ($plugin, $c, $json) = @_;
+
+ my $promise = Mojo::Promise->new;
+
+ # No json object
+ unless ($json) {
+ return $promise->reject({message => 'Response is no valid Json object (remote)'});
+ };
+
+ # There is an error here
+ # Dealing with errors here
+ if ($json->{error} && ref $json->{error} ne 'ARRAY') {
+ return $promise->reject(
+ {
+ message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
+ }
+ );
+ } elsif (my $error = $json->{errors} // $json->{error}) {
+ if (ref $error eq 'ARRAY') {
+ my @errors = ();
+ foreach (@{$error}) {
+ if ($_->[1]) {
+ push @errors, { code => $_->[0], message => $_->[1]}
+ }
+ }
+ return $promise->reject(@errors);
+ }
+
+ return $promise->reject({message => $error});
+ };
+
+ my $access_token = $json->{access_token};
+ my $token_type = $json->{token_type};
+ my $refresh_token = $json->{refresh_token};
+ # my $scope = $json->{scope};
+ my $expires_in = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
+ my $auth = $token_type . ' ' . $access_token;
+
+ # Set session info
+ $c->session(auth => $auth);
+
+ # Set stash info
+ $c->stash(auth => $auth);
+
+ # Remember refresh token in cache
+ $c->chi('user')->set(
+ "refr_" . $auth => $refresh_token,
+ $expires_in
+ );
+
+ return $promise->resolve;
+}
+
1;
__DATA__
diff --git a/lib/Kalamar/Plugin/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index 83c5fe2..60fadfe 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -37,6 +37,13 @@
# Set app to server
$plugin->ua->server->app($mojo);
+ # Get a user agent object for Kalamar
+ $mojo->helper(
+ 'kalamar_ua' => sub {
+ return $plugin->ua;
+ }
+ );
+
# Get user handle
$mojo->helper(
'user_handle' => sub {
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
new file mode 100644
index 0000000..d67837f
--- /dev/null
+++ b/t/plugin/auth-oauth.t
@@ -0,0 +1,268 @@
+use Mojo::Base -strict;
+use Test::More;
+use Test::Mojo;
+use Mojo::File qw/path/;
+use Data::Dumper;
+
+
+#####################
+# Start Fake server #
+#####################
+my $mount_point = '/api/';
+$ENV{KALAMAR_API} = $mount_point;
+
+my $t = Test::Mojo->new('Kalamar' => {
+ Kalamar => {
+ plugins => ['Auth']
+ },
+ 'Kalamar-Auth' => {
+ client_id => 2,
+ client_secret => 'k414m4r-s3cr3t',
+ oauth2 => 1
+ }
+});
+
+# Mount fake backend
+# Get the fixture path
+my $fixtures_path = path(Mojo::File->new(__FILE__)->dirname, '..', 'server');
+my $fake_backend = $t->app->plugin(
+ Mount => {
+ $mount_point =>
+ $fixtures_path->child('mock.pl')
+ }
+);
+# Configure fake backend
+$fake_backend->pattern->defaults->{app}->log($t->app->log);
+
+$t->get_ok('/api')
+ ->status_is(200)
+ ->content_is('Fake server available');
+
+$t->get_ok('/?q=Baum')
+ ->status_is(200)
+ ->text_like('h1 span', qr/KorAP: Find .Baum./i)
+ ->text_like('#total-results', qr/\d+$/)
+ ->content_like(qr/\"authorized\"\:null/)
+ ->element_exists_not('div.button.top a')
+ ->element_exists_not('aside.active')
+ ->element_exists_not('aside.off')
+ ;
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->element_exists('form[action=/user/login] input[name=handle_or_email]')
+ ->element_exists('aside.active')
+ ->element_exists_not('aside.off')
+ ;
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'fail' })
+ ->status_is(302)
+ ->header_is('Location' => '/');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->element_exists('div.notify-error')
+ ->text_is('div.notify-error', 'Bad CSRF token')
+ ->element_exists('input[name=handle_or_email][value=test]')
+ ->element_exists_not('div.button.top a')
+ ;
+
+$t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'pass' })
+ ->status_is(302)
+ ->header_is('Location' => '/');
+
+my $csrf = $t->get_ok('/')
+ ->status_is(200)
+ ->element_exists('div.notify-error')
+ ->text_is('div.notify-error', 'Bad CSRF token')
+ ->element_exists_not('div.button.top a')
+ ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
+ ;
+
+$t->post_ok('/user/login' => form => {
+ handle_or_email => 'test',
+ pwd => 'ldaperr',
+ csrf_token => $csrf
+})
+ ->status_is(302)
+ ->content_is('')
+ ->header_is('Location' => '/');
+
+$csrf = $t->get_ok('/')
+ ->status_is(200)
+ ->element_exists('div.notify-error')
+ ->text_is('div.notify-error', '2022: LDAP Authentication failed due to unknown user or password!')
+ ->element_exists('input[name=handle_or_email][value=test]')
+ ->element_exists_not('div.button.top a')
+ ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
+ ;
+
+$t->post_ok('/user/login' => form => {
+ handle_or_email => 'test',
+ pwd => 'unknown',
+ csrf_token => $csrf
+})
+ ->status_is(302)
+ ->content_is('')
+ ->header_is('Location' => '/');
+
+$csrf = $t->get_ok('/')
+ ->status_is(200)
+ ->element_exists('div.notify-error')
+ ->text_is('div.notify-error', '2022: LDAP Authentication failed due to unknown user or password!')
+ ->element_exists('input[name=handle_or_email][value=test]')
+ ->element_exists_not('div.button.top a')
+ ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
+ ;
+
+$t->post_ok('/user/login' => form => {
+ handle_or_email => 'test',
+ pwd => 'pass',
+ csrf_token => $csrf
+})
+ ->status_is(302)
+ ->content_is('')
+ ->header_is('Location' => '/');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->element_exists_not('div.notify-error')
+ ->element_exists('div.notify-success')
+ ->text_is('div.notify-success', 'Login successful')
+ ->element_exists('aside.off')
+ ->element_exists_not('aside.active')
+ ;
+
+# Now the user is logged in and should be able to
+# search with authorization
+$t->get_ok('/?q=Baum')
+ ->status_is(200)
+ ->text_like('h1 span', qr/KorAP: Find .Baum./i)
+ ->text_like('#total-results', qr/\d+$/)
+ ->element_exists_not('div.notify-error')
+ ->content_like(qr/\"authorized\"\:\"yes\"/)
+ ->element_exists('div.button.top a')
+ ->element_exists('div.button.top a.logout[title~="test"]')
+ ;
+
+# Logout
+$t->get_ok('/user/logout')
+ ->status_is(302)
+ ->header_is('Location' => '/');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->element_exists_not('div.notify-error')
+ ->element_exists('div.notify-success')
+ ->text_is('div.notify-success', 'Logout successful')
+ ;
+
+$t->get_ok('/?q=Baum')
+ ->status_is(200)
+ ->text_like('h1 span', qr/KorAP: Find .Baum./i)
+ ->text_like('#total-results', qr/\d+$/)
+ ->content_like(qr/\"authorized\"\:null/)
+ ;
+
+# Get redirect
+my $fwd = $t->get_ok('/?q=Baum&ql=poliqarp')
+ ->status_is(200)
+ ->element_exists_not('div.notify-error')
+ ->tx->res->dom->at('input[name=fwd]')->attr('value')
+ ;
+
+is($fwd, '/?q=Baum&ql=poliqarp', 'Redirect is valid');
+
+$t->post_ok('/user/login' => form => {
+ handle_or_email => 'test',
+ pwd => 'pass',
+ csrf_token => $csrf,
+ fwd => 'http://bad.example.com/test'
+})
+ ->status_is(302)
+ ->header_is('Location' => '/');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->element_exists('div.notify-error')
+ ->element_exists_not('div.notify-success')
+ ->text_is('div.notify-error', 'Redirect failure')
+ ;
+
+$t->post_ok('/user/login' => form => {
+ handle_or_email => 'test',
+ pwd => 'pass',
+ csrf_token => $csrf,
+ fwd => $fwd
+})
+ ->status_is(302)
+ ->header_is('Location' => '/?q=Baum&ql=poliqarp');
+
+$t->get_ok('/?q=Baum&ql=poliqarp')
+ ->status_is(200)
+ ->element_exists_not('div.notify-error')
+ ->element_exists('div.notify-success')
+ ->text_is('div.notify-success', 'Login successful')
+ ;
+
+$t->app->routes->get(
+ '/user/refresh' => sub {
+ my $c = shift;
+
+ my $old_auth = $c->auth->token;
+ my $refresh = $c->chi('user')->get("refr_$old_auth");
+
+ $c->auth->refresh_token($refresh)->then(
+ sub {
+ my $new_auth = $c->auth->token;
+ $c->notify(success => $new_auth . ' vs. ' . $old_auth);
+ }
+ )->catch(
+ sub {
+
+ # Notify the user on login failure
+ unless (@_) {
+ $c->notify(error => $c->loc('Auth_refreshFail'));
+ }
+
+ # There are known errors
+ foreach (@_) {
+ if (ref $_ eq 'HASH') {
+ my $err = ($_->{code} ? $_->{code} . ': ' : '') .
+ $_->{message};
+ $c->notify(error => $err);
+ }
+ else {
+ $c->notify(error => $_);
+ }
+ };
+ }
+ )->finally(
+ sub {
+ return $c->redirect_to('index');
+ }
+ )->wait;
+ }
+);
+
+$t->get_ok('/user/refresh')
+ ->status_is(302)
+ ->header_is('Location' => '/');
+
+$t->get_ok('/')
+ ->status_is(200)
+ ->element_exists_not('div.notify-error')
+ ->element_exists('div.notify-success')
+ ->text_like('div.notify-success', qr!Bearer abcde vs\. Bearer .{6,}!)
+ ;
+
+
+done_testing;
+__END__
+
+
+
+# Login mit falschem Nutzernamen:
+# 400 und:
+{"errors":[[2022,"LDAP Authentication failed due to unknown user or password!"]]}
+
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 31cfaca..359f78c 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -14,6 +14,7 @@
my $secret = 's3cr3t';
my $fixture_path = path(Mojo::File->new(__FILE__)->dirname)->child('..', 'fixtures');
+# Legacy:
helper jwt_encode => sub {
shift;
return Mojo::JWT->new(
@@ -24,6 +25,7 @@
);
};
+# Legacy;
helper jwt_decode => sub {
my ($c, $auth) = @_;
$auth =~ s/\s*api_token\s+//;
@@ -106,7 +108,13 @@
# Check authentification
if (my $auth = $c->req->headers->header('Authorization')) {
- if (my $jwt = $c->jwt_decode($auth)) {
+
+ my $jwt;
+ if ($auth =~ /^Bearer/) {
+ # Username unknown in OAuth2
+ $response->{json}->{meta}->{authorized} = 'yes';
+ }
+ elsif ($jwt = $c->jwt_decode($auth)) {
$response->{json}->{meta}->{authorized} = $jwt->{username} if $jwt->{username};
};
};
@@ -265,6 +273,107 @@
);
};
+
+# Request API token
+post '/oauth2/token' => sub {
+ my $c = shift;
+
+ if ($c->param('grant_type') eq 'password') {
+
+ # Check for wrong client id
+ if ($c->param('client_id') ne '2') {
+ return $c->render(
+ json => {
+ "error_description" => "Unknown client with " . $_->{client_id},
+ "error" => "invalid_client"
+ },
+ status => 401
+ );
+ }
+
+ # Check for wrong client secret
+ elsif ($c->param('client_secret') ne 'k414m4r-s3cr3t') {
+ return $c->render(
+ json => {
+ "error_description" => "Invalid client credentials",
+ "error" => "invalid_client"
+ },
+ status => 401
+ );
+ }
+
+ # Check for wrong user name
+ elsif ($c->param('username') ne 'test') {
+ return $c->render(json => {
+ error => [[2004, undef]]
+ });
+ }
+
+ # Check for ldap error
+ elsif ($c->param('password') eq 'ldaperr') {
+ return $c->render(
+ format => 'html',
+ status => 401,
+ json => {
+ "errors" => [
+ [
+ 2022,
+ "LDAP Authentication failed due to unknown user or password!"
+ ]
+ ]
+ }
+ );
+ }
+
+ # Check for wrong password
+ elsif ($c->param('password') ne 'pass') {
+ return $c->render(json => {
+ format => 'html',
+ status => 401,
+ "errors" => [[2022,"LDAP Authentication failed due to unknown user or password!"]]
+ });
+ }
+
+ # Return fine access
+ return $c->render(
+ json => {
+ "access_token" => "4dcf8784ccfd26fac9bdb82778fe60e2",
+ "refresh_token" => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
+ "scope" => "all",
+ "token_type" => "Bearer",
+ "expires_in" => 86400
+ });
+ }
+
+ # Refresh token
+ elsif ($c->param('grant_type') eq 'refresh_token') {
+ return $c->render(
+ status => 200,
+ json => {
+ "access_token" => "abcde",
+ "refresh_token" => "fghijk",
+ "token_type" => "Bearer",
+ "expires_in" => 86400
+ }
+ );
+ }
+
+ # Unknown token grant
+ else {
+ return $c->render(
+ json => {
+ "errors" => [
+ [
+ 0, "Grant Type unknown", $c->param("grant_type")
+ ]
+ ]
+ }
+ )
+ }
+};
+
+
+
app->start;