blob: e77a672293a5d854c80001df3a0c02d7a8d27fa2 [file] [log] [blame]
Akron864c2932018-11-16 17:18:55 +01001package Kalamar::Plugin::Auth;
2use Mojo::Base 'Mojolicious::Plugin';
Akron59992122019-10-29 11:28:45 +01003use File::Basename 'dirname';
4use File::Spec::Functions qw/catdir/;
Akron864c2932018-11-16 17:18:55 +01005use Mojo::ByteStream 'b';
Akron7fb78d62021-06-10 12:51:13 +02006use Mojo::Util 'deprecated';
Akron864c2932018-11-16 17:18:55 +01007
Akroncdfd9d52019-07-23 11:35:00 +02008# This is a plugin to deal with the Kustvakt OAuth server.
9# It establishes both the JWT as well as the OAuth password
10# flow for login.
11# All tokens are stored in the session. Access tokens are short-lived,
12# which limits the effects of misuse.
13# Refresh tokens are bound to client id and client secret,
14# which again limits the effects of misuse.
15
16# TODO:
17# Establish a plugin 'OAuth' that works independent of 'Auth'.
18
Akron864c2932018-11-16 17:18:55 +010019# TODO:
Akronc82b1bc2018-11-18 18:06:14 +010020# CSRF-protect logout!
Akron864c2932018-11-16 17:18:55 +010021
Akroncdfd9d52019-07-23 11:35:00 +020022# TODO:
23# Remove the Bearer prefix from auth.
24
25# In case no expiration time is returned by the server,
26# take this time.
Akron8bbbecf2019-07-01 18:57:30 +020027our $EXPECTED_EXPIRATION_IN = 259200;
28
Akron864c2932018-11-16 17:18:55 +010029# Register the plugin
30sub register {
31 my ($plugin, $app, $param) = @_;
32
Akron864c2932018-11-16 17:18:55 +010033 # Load parameter from config file
34 if (my $config_param = $app->config('Kalamar-Auth')) {
35 $param = { %$param, %$config_param };
36 };
37
Akron864c2932018-11-16 17:18:55 +010038 # Load 'notifications' plugin
39 unless (exists $app->renderer->helpers->{notify}) {
40 $app->plugin(Notifications => {
41 HTML => 1
42 });
43 };
44
Akron33f5c672019-06-24 19:40:47 +020045 # Get the client id and the client_secret as a requirement
46 unless ($param->{client_id} && $param->{client_secret}) {
47 $app->log->error('client_id or client_secret not defined');
48 };
Akron864c2932018-11-16 17:18:55 +010049
Akron59992122019-10-29 11:28:45 +010050 # Load localize
Akron864c2932018-11-16 17:18:55 +010051 $app->plugin('Localize' => {
52 dict => {
Akrone997bb52021-06-11 16:44:06 +020053 de => {
54 abort => 'Abbrechen'
55 },
56 -en => {
57 abort => 'Abort'
58 },
Akron864c2932018-11-16 17:18:55 +010059 Auth => {
60 _ => sub { $_->locale },
61 de => {
62 loginSuccess => 'Anmeldung erfolgreich',
63 loginFail => 'Anmeldung fehlgeschlagen',
64 logoutSuccess => 'Abmeldung erfolgreich',
65 logoutFail => 'Abmeldung fehlgeschlagen',
66 csrfFail => 'Fehlerhafter CSRF Token',
Akron8bbbecf2019-07-01 18:57:30 +020067 openRedirectFail => 'Weiterleitungsfehler',
Akroncdfd9d52019-07-23 11:35:00 +020068 tokenExpired => 'Zugriffstoken abgelaufen',
69 tokenInvalid => 'Zugriffstoken ungültig',
70 refreshFail => 'Fehlerhafter Refresh-Token',
Akron59992122019-10-29 11:28:45 +010071 responseError => 'Unbekannter Autorisierungsfehler',
Akronc1aaf932021-06-09 12:19:15 +020072 revokeFail => 'Der Token kann nicht widerrufen werden',
73 revokeSuccess => 'Der Token wurde erfolgreich widerrufen',
Akron59992122019-10-29 11:28:45 +010074 paramError => 'Einige Eingaben sind fehlerhaft',
75 redirectUri => 'Weiterleitungs-Adresse',
76 homepage => 'Webseite',
77 desc => 'Kurzbeschreibung',
Akronc1aaf932021-06-09 12:19:15 +020078 revoke => 'Widerrufen',
Akron59992122019-10-29 11:28:45 +010079 clientCredentials => 'Client Daten',
80 clientType => 'Art der Client-Applikation',
81 clientName => 'Name der Client-Applikation',
82 clientID => 'ID der Client-Applikation',
83 clientSecret => 'Client-Secret',
84 clientRegister => 'Neue Client-Applikation registrieren',
85 registerSuccess => 'Registrierung erfolgreich',
86 registerFail => 'Registrierung fehlgeschlagen',
87 oauthSettings => 'OAuth',
Akrone997bb52021-06-11 16:44:06 +020088 oauthUnregister => {
89 -long => 'Möchten sie <span class="client-name"><%= $client_name %></span> wirklich löschen?',
90 short => 'Löschen'
91 },
Akron83209f72021-01-29 17:54:15 +010092 loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
Akrone997bb52021-06-11 16:44:06 +020093 oauthIssueToken => {
94 -long => 'Stelle einen neuen Token für <span class="client-name"><%= $client_name %></span> aus',
95 short => 'Neuen Token ausstellen'
96 },
Akron9ffb4a32021-06-08 16:11:21 +020097 accessToken => 'Access Token',
Akrone997bb52021-06-11 16:44:06 +020098 oauthRevokeToken => {
99 -long => 'Widerrufe einen Token für <span class="client-name"><%= $client_name %></span>',
100 short => 'Widerrufe'
101 },
102 createdAt => 'Erstellt am <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
103 expiresIn => 'Läuft in <%= stash("seconds") %> Sekunden ab.'
Akron864c2932018-11-16 17:18:55 +0100104 },
105 -en => {
106 loginSuccess => 'Login successful',
107 loginFail => 'Access denied',
108 logoutSuccess => 'Logout successful',
109 logoutFail => 'Logout failed',
110 csrfFail => 'Bad CSRF token',
Akron8bbbecf2019-07-01 18:57:30 +0200111 openRedirectFail => 'Redirect failure',
Akroncdfd9d52019-07-23 11:35:00 +0200112 tokenExpired => 'Access token expired',
113 tokenInvalid => 'Access token invalid',
114 refreshFail => 'Bad refresh token',
Akron59992122019-10-29 11:28:45 +0100115 responseError => 'Unknown authorization error',
Akronc1aaf932021-06-09 12:19:15 +0200116 revokeFail => 'Token can\'t be revoked',
117 revokeSuccess => 'Token was revoked successfully',
Akron59992122019-10-29 11:28:45 +0100118 paramError => 'Some fields are invalid',
119 redirectUri => 'Redirect URI',
120 homepage => 'Homepage',
121 desc => 'Short description',
Akronc1aaf932021-06-09 12:19:15 +0200122 revoke => 'Revoke',
Akron59992122019-10-29 11:28:45 +0100123 clientCredentials => 'Client Credentials',
124 clientType => 'Type of the client application',
125 clientName => 'Name of the client application',
126 clientID => 'ID of the client application',
127 clientSecret => 'Client secret',
128 clientRegister => 'Register new client application',
129 registerSuccess => 'Registration successful',
130 registerFail => 'Registration denied',
131 oauthSettings => 'OAuth',
Akrone997bb52021-06-11 16:44:06 +0200132 oauthUnregister => {
133 -long => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
134 short => 'Unregister'
135 },
Akron83209f72021-01-29 17:54:15 +0100136 loginHint => 'Maybe you need to log in first?',
Akrone997bb52021-06-11 16:44:06 +0200137 oauthIssueToken => {
138 -long => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
139 short => 'Issue new token'
140 },
Akron9ffb4a32021-06-08 16:11:21 +0200141 accessToken => 'Access Token',
Akrone997bb52021-06-11 16:44:06 +0200142 oauthRevokeToken => {
143 -long => 'Revoke a token for <span class="client-name"><%= $client_name %></span>',
144 short => 'Revoke'
145 },
146 createdAt => 'Created at <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
147 expiresIn => 'Expires in <%= stash("seconds") %> seconds.'
Akron864c2932018-11-16 17:18:55 +0100148 }
149 }
150 }
151 });
152
153
Akrona9c8b0e2018-11-16 20:20:28 +0100154 # Add login frame to sidebar
155 $app->content_block(
156 sidebar => {
157 template => 'partial/auth/login'
158 }
159 );
160
161
Akronc82b1bc2018-11-18 18:06:14 +0100162 # Add logout button to header button list
163 $app->content_block(
164 headerButtonGroup => {
165 template => 'partial/auth/logout'
166 }
167 );
168
Akron27031aa2020-04-28 14:57:10 +0200169
170 # Add hook after search
171 $app->hook(
172 after_search => sub {
173 my $c = shift;
174
175 # User is not logged in
176 if ($c->stash('results')->size == 0 && !$c->auth->token) {
177 $c->content_for(
178 'after_search_results' =>
179 $c->render_to_string(
180 inline => '<p class="hint"><%= loc "Auth_loginHint" %></p>'
181 )
182 );
183 };
184 }
185 );
186
Akron59992122019-10-29 11:28:45 +0100187 # The plugin path
188 my $path = catdir(dirname(__FILE__), 'Auth');
189
190 # Append "templates"
191 push @{$app->renderer->paths}, catdir($path, 'templates');
Akron864c2932018-11-16 17:18:55 +0100192
Akron4796e002019-07-05 10:13:15 +0200193 # Get or set the user token necessary for authorization
Akron864c2932018-11-16 17:18:55 +0100194 $app->helper(
195 'auth.token' => sub {
Akroncdfd9d52019-07-23 11:35:00 +0200196 my ($c, $token, $expires_in) = @_;
Akron864c2932018-11-16 17:18:55 +0100197
Akroncdfd9d52019-07-23 11:35:00 +0200198 if ($token) {
199 # Set auth token
Akron4796e002019-07-05 10:13:15 +0200200 $c->stash(auth => $token);
Akroncdfd9d52019-07-23 11:35:00 +0200201 $c->session(auth => $token);
202 $c->session(auth_exp => time + $expires_in);
203 return 1;
Akron4796e002019-07-05 10:13:15 +0200204 };
205
Akroncdfd9d52019-07-23 11:35:00 +0200206 # Get token from stash
207 $token = $c->stash('auth');
208
209 return $token if $token;
210
211 # Get auth from session
212 $token = $c->session('auth') or return;
213 $c->stash(auth => $token);
214
215 # Return stashed value
216 return $token;
Akron864c2932018-11-16 17:18:55 +0100217 }
218 );
219
220
221 # Log in to the system
222 my $r = $app->routes;
Akron864c2932018-11-16 17:18:55 +0100223
Akron33f5c672019-06-24 19:40:47 +0200224 if ($param->{oauth2}) {
Akron864c2932018-11-16 17:18:55 +0100225
Akron8bbbecf2019-07-01 18:57:30 +0200226 my $client_id = $param->{client_id};
227 my $client_secret = $param->{client_secret};
228
Akroncdfd9d52019-07-23 11:35:00 +0200229
230 # Sets a requested token and returns
231 # an error, if it didn't work
Akron8bbbecf2019-07-01 18:57:30 +0200232 $app->helper(
Akroncdfd9d52019-07-23 11:35:00 +0200233 'auth.set_tokens_p' => sub {
234 my ($c, $json) = @_;
235 my $promise = Mojo::Promise->new;
236
237 # No json object
238 unless ($json) {
239 return $promise->reject({
240 message => 'Response is no valid JSON object (remote)'
241 });
242 };
243
244 # There is an error here
245 # Dealing with errors here
246 if ($json->{error} && ref $json->{error} ne 'ARRAY') {
247 return $promise->reject(
248 {
249 message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
250 }
251 );
252 }
253
254 # There is an array of errors
255 elsif (my $error = $json->{errors} // $json->{error}) {
256 if (ref $error eq 'ARRAY') {
257 my @errors = ();
258 foreach (@{$error}) {
259 if ($_->[1]) {
260 push @errors, { code => $_->[0], message => $_->[1]}
261 }
262 }
263 return $promise->reject(@errors);
264 }
265
266 return $promise->reject({message => $error});
267 };
268
269 # Everything is fine
270 my $access_token = $json->{access_token};
Akronc58bfc42020-10-05 12:09:45 +0200271 my $token_type = $json->{token_type};
Akroncdfd9d52019-07-23 11:35:00 +0200272 my $refresh_token = $json->{refresh_token};
273 my $expires_in = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
274 my $auth = $token_type . ' ' . $access_token;
275 # my $scope = $json->{scope};
276
277 # Set session info
278 $c->session(auth => $auth);
279
280 # Expiration of the token minus tolerance
281 $c->session(auth_exp => time + $expires_in - 60);
282
283 # Set session info for refresh token
284 # This can be stored in the session, as it is useless
285 # unless the client secret is stolen
286 $c->session(auth_r => $refresh_token) if $refresh_token;
287
288 # Set stash info
289 $c->stash(auth => $auth);
290
291 return $promise->resolve;
292 }
293 );
294
295
296 # Refresh tokens and return a promise
297 $app->helper(
298 'auth.refresh_p' => sub {
Akron8bbbecf2019-07-01 18:57:30 +0200299 my $c = shift;
300 my $refresh_token = shift;
301
Akron8bbbecf2019-07-01 18:57:30 +0200302 # Get OAuth access token
Akroncdfd9d52019-07-23 11:35:00 +0200303 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
Akron8bbbecf2019-07-01 18:57:30 +0200304
Akron4cefe1f2019-09-04 10:11:28 +0200305 $c->app->log->debug("Refresh at $r_url");
Akroncdfd9d52019-07-23 11:35:00 +0200306
307 return $c->kalamar_ua->post_p($r_url, {} => form => {
Akron8bbbecf2019-07-01 18:57:30 +0200308 grant_type => 'refresh_token',
309 client_id => $client_id,
310 client_secret => $client_secret,
311 refresh_token => $refresh_token
312 })->then(
313 sub {
Akroncdfd9d52019-07-23 11:35:00 +0200314 my $tx = shift;
315 my $json = $tx->result->json;
316
317 # Response is fine
318 if ($tx->res->is_success) {
319
320 $c->app->log->info("Refresh was successful");
321
322 # Set the tokens and return a promise
323 return $c->auth->set_tokens_p($json);
324 };
325
326 # There is a client error - refresh fails
327 if ($tx->res->is_client_error && $json) {
328
329 $c->stash(auth => undef);
330 $c->stash(auth_exp => undef);
331 delete $c->session->{user};
332 delete $c->session->{auth};
333 delete $c->session->{auth_r};
334 delete $c->session->{auth_exp};
335
336 # Response is 400
337 return Mojo::Promise->reject(
338 $json->{error_description} // $c->loc('Auth_refreshFail')
339 );
340 };
341
342 $c->notify(error => $c->loc('Auth_responseError'));
343 return Mojo::Promise->reject;
344 }
345 )
346 }
347 );
348
Akron0f1b93b2020-03-17 11:37:19 +0100349 # Get a list of registered clients
350 $app->helper(
351 'auth.client_list_p' => sub {
352 my $c = shift;
353
354 # Get list of registered clients
355 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/list');
356
Akron1a9d5be2020-03-19 17:28:33 +0100357 # Get the list of all clients
Akron0f1b93b2020-03-17 11:37:19 +0100358 return $c->korap_request(post => $r_url, {} => form => {
Akron276afc02021-06-14 11:00:21 +0200359 super_client_id => $client_id,
360 super_client_secret => $client_secret,
Akron0f1b93b2020-03-17 11:37:19 +0100361 authorized_only => 'no'
362 })->then(
363 sub {
364 my $tx = shift;
365 my $json = $tx->result->json;
366
367 # Response is fine
368 if ($tx->res->is_success) {
369 return Mojo::Promise->resolve($json);
370 };
371
372 $c->log->error($c->dumper($tx->res->to_string));
373
374 # Failure
375 $c->notify(error => $c->loc('Auth_responseError'));
376 return Mojo::Promise->reject($json // 'No response');
377 }
378 );
379 }
380 );
Akroncdfd9d52019-07-23 11:35:00 +0200381
Akron1a9d5be2020-03-19 17:28:33 +0100382
Akronbc94a9c2021-04-15 00:07:35 +0200383 # Get a list of registered clients
384 $app->helper(
385 'auth.token_list_p' => sub {
386 my $c = shift;
387 my $user_client_id = shift;
388
389 # Revoke the token
390 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token/list');
391
392 my $form = {
393 super_client_id => $client_id,
394 super_client_secret => $client_secret,
395 token_type => 'access_token',
396 };
397
398 if ($user_client_id) {
399 $form->{client_id} = $user_client_id;
400 };
401
402 # Get the list of all clients
403 return $c->korap_request(post => $r_url, {} => form => $form)->then(
404 sub {
405 my $tx = shift;
406 my $json = $tx->result->json;
407
408 # Response is fine
409 if ($tx->res->is_success) {
410 return Mojo::Promise->resolve($json);
411 };
412
413 $c->log->error($c->dumper($tx->res->to_string));
414
415 # Failure
416 $c->notify(error => $c->loc('Auth_responseError'));
417 return Mojo::Promise->reject($json // 'No response');
418 }
419 );
420 }
421 );
422
423
Akroncdfd9d52019-07-23 11:35:00 +0200424 # Issue a korap request with "oauth"orization
425 # This will override the core request helper
426 $app->helper(
427 korap_request => sub {
428 my $c = shift;
429 my $method = shift;
430 my $path = shift;
431 my @param = @_;
432
433 # TODO:
434 # Check if $tx is not leaked!
435
436 # Get plugin user agent
437 my $ua = $c->kalamar_ua;
438
439 my $url = Mojo::URL->new($path);
440 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
441
442 # Set X-Forwarded for
443 $tx->req->headers->header(
444 'X-Forwarded-For' => $c->client_ip
445 );
446
447 # Emit Hook to alter request
448 $c->app->plugins->emit_hook(
449 before_korap_request => ($c, $tx)
450 );
451
452 my $h = $tx->req->headers;
453
454 # If the request already has an Authorization
455 # header, respect it!
456 if ($h->authorization) {
457 return $ua->start_p($tx);
458 };
459
460 # Get auth token
461 if (my $auth_token = $c->auth->token) {
462
463 # The token is already expired!
464 my $exp = $c->session('auth_exp');
465 if (defined $exp && $exp < time) {
466
467 # Remove auth ...
468 $c->stash(auth => undef);
469
470 # And get refresh token from session
471 if (my $refresh_token = $c->session('auth_r')) {
472
473 $c->app->log->debug("Refresh is required");
474
475 # Refresh
476 return $c->auth->refresh_p($refresh_token)->then(
477 sub {
478 $c->app->log->debug("Search with refreshed tokens");
479
480 # Tokens were set - now send the request the first time!
481 $tx->req->headers->authorization($c->stash('auth'));
482 return $ua->start_p($tx);
483 }
484 );
485 }
486
487 # The token is expired and no refresh token is
488 # available - issue an unauthorized request!
489 else {
490 $c->stash(auth => undef);
491 $c->stash(auth_exp => undef);
492 delete $c->session->{user};
493 delete $c->session->{auth};
494 delete $c->session->{auth_r};
495 delete $c->session->{auth_exp};
496
497 # Warn on Error!
498 $c->notify(warn => $c->loc('Auth_tokenExpired'));
499 return $ua->start_p($tx);
500 };
501 }
502
503 # Auth token is fine
504 else {
505
506 # Set auth
507 $h->authorization($auth_token);
508 }
509 }
510
511 # No token set
512 else {
513
514 # Return unauthorized request
515 return $ua->start_p($tx);
516 };
517
518 # Issue an authorized request and automatically
519 # refresh the token on expiration!
520 return $ua->start_p($tx)->then(
521 sub {
522 my $tx = shift;
523
524 # Response is fine
525 if ($tx->res->is_success) {
526 return Mojo::Promise->resolve($tx);
527 }
528
529 # There is a client error - maybe refresh!
530 elsif ($tx->res->is_client_error) {
531
532 # Check the error
533 my $json = $tx->res->json('/errors/0/1');
534 if ($json && ($json =~ /expired|invalid/)) {
535 $c->stash(auth => undef);
536 $c->stash(auth_exp => undef);
537 delete $c->session->{user};
538 delete $c->session->{auth};
539
540 # And get refresh token from session
541 if (my $refresh_token = $c->session('auth_r')) {
542
543 # Refresh
544 return $c->auth->refresh_p($refresh_token)->then(
545 sub {
546 $c->app->log->debug("Search with refreshed tokens");
547
548 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
549
550 # Set X-Forwarded for
551 $tx->req->headers->header(
552 'X-Forwarded-For' => $c->client_ip
553 );
554
555 # Tokens were set - now send the request the first time!
556 $tx->req->headers->authorization($c->stash('auth'));
557 return $ua->start_p($tx);
558 }
559 )
560 };
561
562 # Reject the invalid token
563 $c->notify(error => $c->loc('Auth_tokenInvalid'));
564 return Mojo::Promise->reject;
565 };
566
567 return Mojo::Promise->resolve($tx);
568 };
569
570 $c->notify(error => $c->loc('Auth_responseError'));
571 return Mojo::Promise->reject;
Akron8bbbecf2019-07-01 18:57:30 +0200572 }
573 );
574 }
575 );
576
Akroncdfd9d52019-07-23 11:35:00 +0200577 # Password flow for OAuth
Akron33f5c672019-06-24 19:40:47 +0200578 $r->post('/user/login')->to(
579 cb => sub {
580 my $c = shift;
Akron864c2932018-11-16 17:18:55 +0100581
Akron33f5c672019-06-24 19:40:47 +0200582 # Validate input
583 my $v = $c->validation;
Akrone208d302020-11-28 11:14:50 +0100584 $v->required('handle', 'trim');
Akron33f5c672019-06-24 19:40:47 +0200585 $v->required('pwd', 'trim');
586 $v->csrf_protect;
587 $v->optional('fwd')->closed_redirect;
Akron864c2932018-11-16 17:18:55 +0100588
Akrone208d302020-11-28 11:14:50 +0100589 my $user = $v->param('handle');
Akron33f5c672019-06-24 19:40:47 +0200590 my $fwd = $v->param('fwd');
Akron864c2932018-11-16 17:18:55 +0100591
Akron33f5c672019-06-24 19:40:47 +0200592 # Set flash for redirect
Akrone208d302020-11-28 11:14:50 +0100593 $c->flash(handle => $user);
Akron864c2932018-11-16 17:18:55 +0100594
Akron33f5c672019-06-24 19:40:47 +0200595 if ($v->has_error || index($user, ':') >= 0) {
596 if ($v->has_error('fwd')) {
597 $c->notify(error => $c->loc('Auth_openRedirectFail'));
598 }
599 elsif ($v->has_error('csrf_token')) {
600 $c->notify(error => $c->loc('Auth_csrfFail'));
601 }
602 else {
603 $c->notify(error => $c->loc('Auth_loginFail'));
Akron864c2932018-11-16 17:18:55 +0100604 };
605
Akron864c2932018-11-16 17:18:55 +0100606 return $c->relative_redirect_to($fwd // 'index');
607 }
Akron864c2932018-11-16 17:18:55 +0100608
Akron33f5c672019-06-24 19:40:47 +0200609 my $pwd = $v->param('pwd');
Akron864c2932018-11-16 17:18:55 +0100610
Akroncdfd9d52019-07-23 11:35:00 +0200611 $c->app->log->debug("Login from user $user");
Akron33f5c672019-06-24 19:40:47 +0200612
613 # <specific>
614
615 # Get OAuth access token
616 my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
617
618 # Korap request for login
619 $c->korap_request('post', $url, {}, form => {
620 grant_type => 'password',
621 username => $user,
622 password => $pwd,
Akron8bbbecf2019-07-01 18:57:30 +0200623 client_id => $client_id,
624 client_secret => $client_secret
Akron33f5c672019-06-24 19:40:47 +0200625 })->then(
626 sub {
Akron8bbbecf2019-07-01 18:57:30 +0200627 # Set the tokens and return a promise
Akroncdfd9d52019-07-23 11:35:00 +0200628 return $c->auth->set_tokens_p(shift->result->json)
Akron33f5c672019-06-24 19:40:47 +0200629 }
Akron3b3c7af2020-05-15 16:23:55 +0200630 )->then(
631 sub {
632 # Set user info
633 $c->session(user => $user);
634 $c->stash(user => $user);
635
636 # Notify on success
637 $c->app->log->debug(qq!Login successful: "$user"!);
638 $c->notify(success => $c->loc('Auth_loginSuccess'));
639 }
Akron33f5c672019-06-24 19:40:47 +0200640 )->catch(
641 sub {
Akron33f5c672019-06-24 19:40:47 +0200642
Akron8bbbecf2019-07-01 18:57:30 +0200643 # Notify the user on login failure
644 unless (@_) {
645 $c->notify(error => $c->loc('Auth_loginFail'));
646 }
Akron33f5c672019-06-24 19:40:47 +0200647
Akron8bbbecf2019-07-01 18:57:30 +0200648 # There are known errors
649 foreach (@_) {
650 if (ref $_ eq 'HASH') {
651 my $err = ($_->{code} ? $_->{code} . ': ' : '') .
652 $_->{message};
653 $c->notify(error => $err);
654 # Log failure
655 $c->app->log->debug($err);
656 }
657 else {
658 $c->notify(error => $_);
659 $c->app->log->debug($_);
660 };
661 };
Akron33f5c672019-06-24 19:40:47 +0200662
663 $c->app->log->debug(qq!Login fail: "$user"!);
Akron8bbbecf2019-07-01 18:57:30 +0200664 }
Akron33f5c672019-06-24 19:40:47 +0200665 )->finally(
666 sub {
Akron33f5c672019-06-24 19:40:47 +0200667 # Redirect to slash
668 return $c->relative_redirect_to($fwd // 'index');
669 }
670 )
671
672 # Start IOLoop
673 ->wait;
674
675 return 1;
676 }
677 )->name('login');
Akroncdfd9d52019-07-23 11:35:00 +0200678
679
680 # Log out of the session
Akron4cefe1f2019-09-04 10:11:28 +0200681 $r->get('/user/logout')->to(
682 cb => sub {
683 my $c = shift;
684
685 # TODO: csrf-protection!
686
687 my $refresh_token = $c->session('auth_r');
688
689 # Revoke the token
690 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke');
691
692 $c->kalamar_ua->post_p($url => {} => form => {
693 client_id => $client_id,
694 client_secret => $client_secret,
695 token => $refresh_token,
696 token_type => 'refresh_token'
697 })->then(
698 sub {
699 my $tx = shift;
700 my $json = $tx->result->json;
701
702 my $promise;
703
704 # Response is fine
705 if ($tx->res->is_success) {
706 $c->app->log->info("Revocation was successful");
707 $c->notify(success => $c->loc('Auth_logoutSuccess'));
708
709 $c->stash(auth => undef);
710 $c->stash(auth_exp => undef);
Akrone208d302020-11-28 11:14:50 +0100711 $c->flash(handle => delete $c->session->{user});
Akron4cefe1f2019-09-04 10:11:28 +0200712 delete $c->session->{auth};
713 delete $c->session->{auth_r};
714 delete $c->session->{auth_exp};
715 return Mojo::Promise->resolve;
716 }
717
718 # Token may be invalid
719 $c->notify('error', $c->loc('Auth_logoutFail'));
720
721 # There is a client error - refresh fails
722 if ($tx->res->is_client_error && $json) {
723
724 return Mojo::Promise->reject(
725 $json->{error_description}
726 );
727 };
728
729 # Resource may not be found (404)
730 return Mojo::Promise->reject
731
732 }
733 )->catch(
734 sub {
735 my $err = shift;
736
737 # Server may be irresponsible
738 $c->notify('error', $c->loc('Auth_logoutFail'));
739 return Mojo::Promise->reject($err);
740 }
741 )->finally(
742 sub {
743 return $c->redirect_to('index');
744 }
745 )->wait;
746 }
747 )->name('logout');
Akron59992122019-10-29 11:28:45 +0100748
Akron041ca4d2021-06-10 11:52:51 +0200749
Akron59992122019-10-29 11:28:45 +0100750 # If "experimental_registration" is set, open
751 # OAuth registration dialogues.
752 if ($param->{experimental_client_registration}) {
753
754 # Add settings
755 $app->navi->add(settings => (
756 $app->loc('Auth_oauthSettings'), 'oauth'
757 ));
758
759 # Route to oauth settings
760 $r->get('/settings/oauth')->to(
761 cb => sub {
Akron0f1b93b2020-03-17 11:37:19 +0100762 my $c = shift;
763
Akronad011bb2021-06-10 12:16:36 +0200764 _set_no_cache($c->res->headers);
765
Akron0f1b93b2020-03-17 11:37:19 +0100766 unless ($c->auth->token) {
767 return $c->render(
768 content => 'Unauthorized',
769 status => 401
770 );
771 };
772
773 # Wait for async result
774 $c->render_later;
775
776 $c->auth->client_list_p->then(
777 sub {
778 $c->stash('client_list' => shift);
779 }
780 )->catch(
781 sub {
782 return;
783 }
784 )->finally(
785 sub {
Akron17de86e2020-04-16 16:03:40 +0200786 return $c->render(template => 'auth/clients')
Akron0f1b93b2020-03-17 11:37:19 +0100787 }
788 );
Akron59992122019-10-29 11:28:45 +0100789 }
Akron1a9d5be2020-03-19 17:28:33 +0100790 )->name('oauth-settings');
Akron59992122019-10-29 11:28:45 +0100791
792 # Route to oauth client registration
793 $r->post('/settings/oauth/register')->to(
794 cb => sub {
795 my $c = shift;
Akronad011bb2021-06-10 12:16:36 +0200796
797 _set_no_cache($c->res->headers);
798
Akron59992122019-10-29 11:28:45 +0100799 my $v = $c->validation;
800
801 unless ($c->auth->token) {
Akron0f1b93b2020-03-17 11:37:19 +0100802 return $c->render(
803 content => 'Unauthorized',
804 status => 401
805 );
Akron59992122019-10-29 11:28:45 +0100806 };
807
808 $v->csrf_protect;
809 $v->required('name', 'trim')->size(3, 255);
810 $v->required('type')->in('PUBLIC', 'CONFIDENTIAL');
811 $v->required('desc', 'trim')->size(3, 255);
812 $v->optional('url', 'trim')->like(qr/^(http|$)/i);
Akrondc50c892021-05-05 18:12:02 +0200813 $v->optional('redirect_uri', 'trim')->like(qr/^(http|$)/i);
Akron59992122019-10-29 11:28:45 +0100814
815 # Render with error
816 if ($v->has_error) {
817 if ($v->has_error('csrf_token')) {
818 $c->notify(error => $c->loc('Auth_csrfFail'));
819 }
820 else {
Akronc1aaf932021-06-09 12:19:15 +0200821 $c->notify(error => $c->loc('Auth_paramError'));
Akron59992122019-10-29 11:28:45 +0100822 };
Akron83209f72021-01-29 17:54:15 +0100823 # return $c->redirect_to('oauth-settings');
824 return $c->render(template => 'auth/clients');
Akron59992122019-10-29 11:28:45 +0100825 };
826
827 # Wait for async result
828 $c->render_later;
829
830 # Register on server
831 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/client/register');
832 $c->korap_request('POST', $url => {} => json => {
833 name => $v->param('name'),
834 type => $v->param('type'),
835 description => $v->param('desc'),
836 url => $v->param('url'),
Akrondc50c892021-05-05 18:12:02 +0200837 redirect_uri => $v->param('redirect_uri')
Akron59992122019-10-29 11:28:45 +0100838 })->then(
839 sub {
840 my $tx = shift;
841 my $result = $tx->result;
842
843 if ($result->is_error) {
844 return Mojo::Promise->reject;
845 };
846
847 my $json = $result->json;
848
Akron59992122019-10-29 11:28:45 +0100849 my $client_id = $json->{client_id};
850 my $client_secret = $json->{client_secret};
851
852 $c->stash('client_name' => $v->param('name'));
853 $c->stash('client_desc' => $v->param('desc'));
854 $c->stash('client_type' => $v->param('type'));
855 $c->stash('client_url' => $v->param('url'));
Akrondc50c892021-05-05 18:12:02 +0200856 $c->stash('client_redirect_uri' => $v->param('redirect_uri'));
Akron59992122019-10-29 11:28:45 +0100857 $c->stash('client_id' => $client_id);
858
859 if ($client_secret) {
860 $c->stash('client_secret' => $client_secret);
861 };
862
863 $c->notify(success => $c->loc('Auth_en_registerSuccess'));
864
Akron17de86e2020-04-16 16:03:40 +0200865 return $c->render(template => 'auth/client');
Akron59992122019-10-29 11:28:45 +0100866 }
867 )->catch(
868 sub {
869 # Server may be irresponsible
870 my $err = shift;
871 $c->notify('error' => $c->loc('Auth_en_registerFail'));
872 return Mojo::Promise->reject($err);
873 }
874 )->finally(
875 sub {
876 return $c->redirect_to('settings' => { scope => 'oauth' });
877 }
878 );
879 }
880 )->name('oauth-register');
Akron1a9d5be2020-03-19 17:28:33 +0100881
882
Akron041ca4d2021-06-10 11:52:51 +0200883 # Unregister client page
884 $r->get('/settings/oauth/:client_id/unregister')->to(
Akron1a9d5be2020-03-19 17:28:33 +0100885 cb => sub {
Akronad011bb2021-06-10 12:16:36 +0200886 my $c = shift;
887 _set_no_cache($c->res->headers);
888 $c->render(template => 'auth/unregister');
Akron1a9d5be2020-03-19 17:28:33 +0100889 }
890 )->name('oauth-unregister');
891
Akron17de86e2020-04-16 16:03:40 +0200892
Akron1a9d5be2020-03-19 17:28:33 +0100893 # Unregister client
Akron041ca4d2021-06-10 11:52:51 +0200894 $r->post('/settings/oauth/:client_id/unregister')->to(
Akron1a9d5be2020-03-19 17:28:33 +0100895 cb => sub {
896 my $c = shift;
Akronad011bb2021-06-10 12:16:36 +0200897 _set_no_cache($c->res->headers);
Akron1a9d5be2020-03-19 17:28:33 +0100898
899 my $v = $c->validation;
900
901 unless ($c->auth->token) {
902 return $c->render(
903 content => 'Unauthorized',
904 status => 401
905 );
906 };
907
908 $v->csrf_protect;
909 $v->required('client-name', 'trim')->size(3, 255);
Akron1a9d5be2020-03-19 17:28:33 +0100910
911 # Render with error
912 if ($v->has_error) {
913 if ($v->has_error('csrf_token')) {
914 $c->notify(error => $c->loc('Auth_csrfFail'));
915 }
916 else {
Akronc1aaf932021-06-09 12:19:15 +0200917 $c->notify(error => $c->loc('Auth_paramError'));
Akron1a9d5be2020-03-19 17:28:33 +0100918 };
Akron83209f72021-01-29 17:54:15 +0100919 return $c->redirect_to('oauth-settings');
Akron1a9d5be2020-03-19 17:28:33 +0100920 };
921
Akron041ca4d2021-06-10 11:52:51 +0200922 my $client_id = $c->stash('client_id');
Akron1a9d5be2020-03-19 17:28:33 +0100923 my $client_name = $v->param('client-name');
924 my $client_secret = $v->param('client-secret');
925
926 # Get list of registered clients
927 my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/deregister/')->path(
928 $client_id
929 );
930
931 my $send = {};
932
933 if ($client_secret) {
934 $send->{client_secret} = $client_secret;
935 };
936
937 # Get the list of all clients
938 return $c->korap_request(delete => $r_url, {} => form => $send)->then(
939 sub {
940 my $tx = shift;
941
942 # Response is fine
943 if ($tx->res->is_success) {
944 # Okay
945 $c->notify(success => 'Successfully deleted ' . $client_name);
946 }
947 else {
948
949 # Failure
950 my $json = $tx->result->json;
951 if ($json && $json->{error_description}) {
952 $c->notify(error => $json->{error_description});
953 } else {
954 $c->notify(error => $c->loc('Auth_responseError'));
955 };
956 };
957
958 return $c->redirect_to('oauth-settings');
959 }
960 );
961 }
962 )->name('oauth-unregister-post');
Akron17de86e2020-04-16 16:03:40 +0200963
964
965 # Show information of a client
Akron041ca4d2021-06-10 11:52:51 +0200966 $r->get('/settings/oauth/:client_id')->to(
Akron17de86e2020-04-16 16:03:40 +0200967 cb => sub {
968 my $c = shift;
969
Akronad011bb2021-06-10 12:16:36 +0200970 _set_no_cache($c->res->headers);
971
Akron17de86e2020-04-16 16:03:40 +0200972 $c->render_later;
973
974 $c->auth->client_list_p->then(
975 sub {
976 my $json = shift;
977
978 my ($item) = grep {
Akrondc50c892021-05-05 18:12:02 +0200979 $c->stash('client_id') eq $_->{client_id}
Akron17de86e2020-04-16 16:03:40 +0200980 } @$json;
981
982 unless ($item) {
983 return Mojo::Promise->reject;
984 };
985
Akrondc50c892021-05-05 18:12:02 +0200986 $c->stash(client_name => $item->{client_name});
Akronbc94a9c2021-04-15 00:07:35 +0200987 $c->stash(client_desc => $item->{client_description});
988 $c->stash(client_url => $item->{client_url});
Akron17de86e2020-04-16 16:03:40 +0200989 $c->stash(client_type => 'PUBLIC');
990
Akronbc94a9c2021-04-15 00:07:35 +0200991 $c->auth->token_list_p($c->stash('client_id'));
992 }
993 )->then(
994 sub {
995 my $json = shift;
996
997 $c->stash(tokens => $json);
998
Akron17de86e2020-04-16 16:03:40 +0200999 return Mojo::Promise->resolve;
1000 }
1001 )->catch(
1002 sub {
1003 return $c->reply->not_found;
1004 }
1005 )->finally(
1006 sub {
Akron17de86e2020-04-16 16:03:40 +02001007 return $c->render(template => 'auth/client')
1008 }
1009 );
1010
1011 return;
1012 }
1013 )->name('oauth-tokens');
Akron59992122019-10-29 11:28:45 +01001014 };
Akron83209f72021-01-29 17:54:15 +01001015
1016
Akronc1aaf932021-06-09 12:19:15 +02001017 # Ask if new token should be issued
Akron041ca4d2021-06-10 11:52:51 +02001018 $r->get('/settings/oauth/:client_id/token')->to(
Akron83209f72021-01-29 17:54:15 +01001019 cb => sub {
Akronad011bb2021-06-10 12:16:36 +02001020 my $c = shift;
1021 _set_no_cache($c->res->headers);
1022 $c->render(template => 'auth/issue-token');
Akron83209f72021-01-29 17:54:15 +01001023 }
1024 )->name('oauth-issue-token');
1025
1026
Akronc1aaf932021-06-09 12:19:15 +02001027 # Ask if a token should be revoked
Akron041ca4d2021-06-10 11:52:51 +02001028 $r->post('/settings/oauth/:client_id/token/revoke')->to(
Akronc1aaf932021-06-09 12:19:15 +02001029 cb => sub {
1030 shift->render(template => 'auth/revoke-token');
1031 }
1032 )->name('oauth-revoke-token');
1033
1034
1035 # Issue new token
Akron041ca4d2021-06-10 11:52:51 +02001036 $r->post('/settings/oauth/:client_id/token')->to(
Akron83209f72021-01-29 17:54:15 +01001037 cb => sub {
1038 my $c = shift;
Akronad011bb2021-06-10 12:16:36 +02001039 _set_no_cache($c->res->headers);
Akron83209f72021-01-29 17:54:15 +01001040
1041 my $v = $c->validation;
1042
1043 unless ($c->auth->token) {
1044 return $c->render(
1045 content => 'Unauthorized',
1046 status => 401
1047 );
1048 };
1049
1050 $v->csrf_protect;
Akron83209f72021-01-29 17:54:15 +01001051 $v->optional('client-secret');
1052 $v->required('name', 'trim');
1053
1054 # Render with error
1055 if ($v->has_error) {
1056 if ($v->has_error('csrf_token')) {
1057 $c->notify(error => $c->loc('Auth_csrfFail'));
1058 }
1059 else {
Akronc1aaf932021-06-09 12:19:15 +02001060 $c->notify(error => $c->loc('Auth_paramError'));
Akron83209f72021-01-29 17:54:15 +01001061 };
1062 return $c->redirect_to('oauth-settings')
1063 };
1064
1065 # Get authorization token
1066 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
1067 my $client_id = $c->stash('client_id');
1068 my $name = $v->param('name');
1069 my $redirect_url = $c->url_for->query({name => $name});
1070
1071 return $c->korap_request(post => $r_url, {} => form => {
1072 response_type => 'code',
1073 client_id => $client_id,
1074 redirect_uri => $redirect_url,
1075 # TODO: State
1076 })->then(
1077 sub {
1078 my $tx = shift;
1079
1080 # Strip the token from the location header of the fake redirect
1081 # TODO: Alternatively redirect!
1082 my ($code, $scope, $loc, $name);
1083 foreach (@{$tx->redirects}) {
1084 $loc = $_->res->headers->header('Location');
1085 if (index($loc, 'code') > 0) {
1086 my $q = Mojo::URL->new($loc)->query;
1087 $code = $q->param('code');
1088 $scope = $q->param('scope');
1089 $name = $q->param('name');
1090 last;
1091 };
1092 };
1093
1094 # Fine!
1095 if ($code) {
1096 return Mojo::Promise->resolve(
1097 $client_id,
1098 $redirect_url,
1099 $code,
1100 $scope,
1101 $name
1102 );
1103 };
1104 return Mojo::Promise->reject;
1105 }
1106 )->then(
1107 sub {
1108 my ($client_id, $redirect_url, $code, $scope, $name) = @_;
1109
1110 # Get OAuth access token
1111 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
1112 return $c->kalamar_ua->post_p($r_url, {} => form => {
1113 client_id => $client_id,
1114 # NO CLIENT_SECRET YET SUPPORTED
1115 grant_type => 'authorization_code',
1116 code => $code,
1117 redirect_uri => $redirect_url
1118 })->then(
1119 sub {
1120 my $tx = shift;
1121 my $json = $tx->res->json;
1122
1123 if ($tx->res->is_error) {
1124 $c->notify(error => 'Unable to fetch new token');
1125 return Mojo::Promise->reject;
1126 };
1127
1128 $c->notify(success => 'New access token created');
1129
Akronbc94a9c2021-04-15 00:07:35 +02001130 $c->redirect_to('oauth-tokens' => { client_id => $client_id })
Akron83209f72021-01-29 17:54:15 +01001131 }
1132 )->catch(
1133 sub {
1134 my $err_msg = shift;
1135
1136 # Only raised in case of connection errors
1137 if ($err_msg) {
1138 $c->notify(error => { src => 'Backend' } => $err_msg)
1139 };
1140
1141 $c->render(
1142 status => 400,
1143 template => 'failure'
1144 );
1145 }
1146 )
1147
1148 # Start IOLoop
1149 ->wait;
1150
1151 }
1152 )->catch(
1153 sub {
1154 my $err_msg = shift;
1155
1156 # Only raised in case of connection errors
1157 if ($err_msg) {
1158 $c->notify(error => { src => 'Backend' } => $err_msg)
1159 };
1160
1161 return $c->render(
1162 status => 400,
1163 template => 'failure'
1164 );
1165 }
1166 )
1167
1168 # Start IOLoop
1169 ->wait;
1170
1171 return 1;
1172 }
1173 )->name('oauth-issue-token-post');
Akronc1aaf932021-06-09 12:19:15 +02001174
1175
1176 # Revoke token
Akron041ca4d2021-06-10 11:52:51 +02001177 $r->delete('/settings/oauth/:client_id/token')->to(
Akronc1aaf932021-06-09 12:19:15 +02001178 cb => sub {
1179 my $c = shift;
1180
1181 my $v = $c->validation;
1182
1183 unless ($c->auth->token) {
1184 return $c->render(
1185 content => 'Unauthorized',
1186 status => 401
1187 );
1188 };
1189
1190 $v->csrf_protect;
1191 $v->required('token', 'trim');
1192 $v->optional('name', 'trim');
1193 my $private_client_id = $c->stash('client_id');
1194
1195 # Render with error
1196 if ($v->has_error) {
1197 if ($v->has_error('csrf_token')) {
1198 $c->notify(error => $c->loc('Auth_csrfFail'));
1199 }
1200 else {
1201 $c->notify(error => $c->loc('Auth_paramError'));
1202 };
1203 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1204 };
1205
1206 # Revoke token using super client privileges
1207 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke/super');
1208
1209 my $token = $v->param('token');
1210
1211 return $c->korap_request(post => $r_url, {} => form => {
1212 super_client_id => $client_id,
1213 super_client_secret => $client_secret,
1214 token => $token
1215 })->then(
1216 sub {
1217 my $tx = shift;
1218
1219 # Response is fine
1220 if ($tx->res->is_success) {
1221 $c->notify(success => $c->loc('Auth_revokeSuccess'));
1222 return Mojo::Promise->resolve;
1223 };
1224
1225 return Mojo::Promise->reject;
1226 }
1227 )->catch(
1228 sub {
1229 my $err_msg = shift;
1230 if ($err_msg) {
1231 $c->notify(error => { src => 'Backend' } => $err_msg );
1232 }
1233 else {
1234 $c->notify(error => $c->loc('Auth_revokeFail'));
1235 };
1236 }
1237 )->finally(
1238 sub {
1239 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1240 }
1241 )
1242
1243 # Start IOLoop
1244 ->wait;
1245 }
1246 )->name('oauth-revoke-token-delete');
Akron33f5c672019-06-24 19:40:47 +02001247 }
Akroncdfd9d52019-07-23 11:35:00 +02001248
Akron33f5c672019-06-24 19:40:47 +02001249 # Use JWT login
1250 else {
1251
Akron7fb78d62021-06-10 12:51:13 +02001252 deprecated 'JWT flow is deprecated in favor of OAuth2 flow';
1253
Akroncdfd9d52019-07-23 11:35:00 +02001254 # Inject authorization to all korap requests
1255 $app->hook(
1256 before_korap_request => sub {
1257 my ($c, $tx) = @_;
1258 my $h = $tx->req->headers;
1259
1260 # If the request already has an Authorization
1261 # header, respect it
1262 unless ($h->authorization) {
1263
1264 # Get valid auth token and set as header
1265 if (my $auth_token = $c->auth->token) {
1266 $h->authorization($auth_token);
1267 };
1268 };
1269 }
1270 );
1271
1272 # Password flow with JWT
Akron33f5c672019-06-24 19:40:47 +02001273 $r->post('/user/login')->to(
1274 cb => sub {
1275 my $c = shift;
1276
1277 # Validate input
1278 my $v = $c->validation;
Akrone208d302020-11-28 11:14:50 +01001279 $v->required('handle', 'trim');
Akron33f5c672019-06-24 19:40:47 +02001280 $v->required('pwd', 'trim');
1281 $v->csrf_protect;
1282 $v->optional('fwd')->closed_redirect;
1283
Akrone208d302020-11-28 11:14:50 +01001284 my $user = $v->param('handle');
Akron33f5c672019-06-24 19:40:47 +02001285 my $fwd = $v->param('fwd');
1286
1287 # Set flash for redirect
Akrone208d302020-11-28 11:14:50 +01001288 $c->flash(handle => $user);
Akron33f5c672019-06-24 19:40:47 +02001289
1290 if ($v->has_error || index($user, ':') >= 0) {
1291 if ($v->has_error('fwd')) {
1292 $c->notify(error => $c->loc('Auth_openRedirectFail'));
1293 }
1294 elsif ($v->has_error('csrf_token')) {
1295 $c->notify(error => $c->loc('Auth_csrfFail'));
1296 }
1297 else {
1298 $c->notify(error => $c->loc('Auth_loginFail'));
1299 };
1300
1301 return $c->relative_redirect_to($fwd // 'index');
1302 }
1303
1304 my $pwd = $v->param('pwd');
1305
Akroncdfd9d52019-07-23 11:35:00 +02001306 $c->app->log->debug("Login from user $user");
Akron33f5c672019-06-24 19:40:47 +02001307
1308 my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
1309
1310 # Korap request for login
1311 $c->korap_request('get', $url, {
1312
1313 # Set authorization header
1314 Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
1315
1316 })->then(
1317 sub {
1318 my $tx = shift;
1319
1320 # Get the java token
1321 my $jwt = $tx->result->json;
1322
1323 # No java web token
1324 unless ($jwt) {
1325 $c->notify(error => 'Response is no valid JWT (remote)');
1326 return;
1327 };
1328
1329 # There is an error here
1330 # Dealing with errors here
1331 if (my $error = $jwt->{error} // $jwt->{errors}) {
1332 if (ref $error eq 'ARRAY') {
1333 foreach (@$error) {
1334 unless ($_->[1]) {
1335 $c->notify(error => $c->loc('Auth_loginFail'));
1336 }
1337 else {
1338 $c->notify(error => $_->[0] . ($_->[1] ? ': ' . $_->[1] : ''));
1339 };
1340 };
1341 }
1342 else {
1343 $c->notify(error => 'There is an unknown JWT error');
1344 };
1345 return;
1346 };
1347
1348 # TODO: Deal with user return values.
1349 my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
1350
Akroncdfd9d52019-07-23 11:35:00 +02001351 $c->app->log->debug(qq!Login successful: "$user"!);
Akron33f5c672019-06-24 19:40:47 +02001352
1353 $user = $jwt->{username} ? $jwt->{username} : $user;
1354
1355 # Set session info
1356 $c->session(user => $user);
1357 $c->session(auth => $auth);
1358
1359 # Set stash info
1360 $c->stash(user => $user);
1361 $c->stash(auth => $auth);
Akron33f5c672019-06-24 19:40:47 +02001362 $c->notify(success => $c->loc('Auth_loginSuccess'));
1363 }
1364 )->catch(
1365 sub {
1366 my $e = shift;
1367
1368 # Notify the user
1369 $c->notify(
1370 error =>
1371 ($e->{code} ? $e->{code} . ': ' : '') .
1372 $e->{message} . ' for Login (remote)'
1373 );
1374
1375 # Log failure
1376 $c->app->log->debug(
1377 ($e->{code} ? $e->{code} . ' - ' : '') .
1378 $e->{message}
1379 );
1380
1381 $c->app->log->debug(qq!Login fail: "$user"!);
1382 $c->notify(error => $c->loc('Auth_loginFail'));
1383 }
1384 )->finally(
1385 sub {
1386
1387 # Redirect to slash
1388 return $c->relative_redirect_to($fwd // 'index');
1389 }
1390 )
1391
1392 # Start IOLoop
1393 ->wait;
1394
1395 return 1;
1396 }
1397 )->name('login');
Akron4cefe1f2019-09-04 10:11:28 +02001398
1399
1400 # Log out of the session
1401 $r->get('/user/logout')->to(
1402 cb => sub {
1403 my $c = shift;
1404
1405 # TODO: csrf-protection!
1406
1407 # Log out of the system
1408 my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
1409
1410 $c->korap_request(
1411 'get', $url
1412 )->then(
1413 # Logged out
1414 sub {
1415 my $tx = shift;
1416 # Clear cache
1417 # ?? Necesseary
1418 # $c->chi('user')->remove($c->auth->token);
1419
1420 # TODO:
1421 # Revoke refresh token!
1422 # based on auth token!
1423 # my $refresh_token = $c->chi('user')->get('refr_' . $c->auth->token);
1424 # $c->auth->revoke_token($refresh_token)
1425
1426 # Expire session
1427 $c->session(user => undef);
1428 $c->session(auth => undef);
1429 $c->notify(success => $c->loc('Auth_logoutSuccess'));
1430 }
1431
1432 )->catch(
1433 # Something went wrong
1434 sub {
1435 # my $err_msg = shift;
1436 $c->notify('error', $c->loc('Auth_logoutFail'));
1437 }
1438
1439 )->finally(
1440 # Redirect
1441 sub {
1442 return $c->redirect_to('index');
1443 }
1444 )
1445
1446 # Start IOLoop
1447 ->wait;
1448
1449 return 1;
1450 }
1451 )->name('logout');
Akron33f5c672019-06-24 19:40:47 +02001452 };
Akron59992122019-10-29 11:28:45 +01001453
1454 $app->log->info('Successfully registered Auth plugin');
Akron864c2932018-11-16 17:18:55 +01001455};
1456
Akronb3f33592020-03-16 15:14:44 +01001457
Akronad011bb2021-06-10 12:16:36 +02001458# Set 'no caching' headers
1459sub _set_no_cache {
1460 my $h = shift;
1461 $h->cache_control('max-age=0, no-cache, no-store, must-revalidate');
1462 $h->expires('Thu, 01 Jan 1970 00:00:00 GMT');
1463 $h->header('Pragma','no-cache');
1464};
1465
1466
Akron864c2932018-11-16 17:18:55 +010014671;
Akrona9c8b0e2018-11-16 20:20:28 +01001468
Akronc82b1bc2018-11-18 18:06:14 +01001469
Akrona9c8b0e2018-11-16 20:20:28 +01001470__END__
Akron59992122019-10-29 11:28:45 +01001471
1472=pod
1473
1474=encoding utf8
1475
1476=head1 NAME
1477
1478Kalamar::Plugin::Auth - OAuth-2.0-based authorization plugin
1479
1480=head1 DESCRIPTION
1481
1482L<Kalamar::Plugin::Auth> is an OAuth-2.0-based authorization
1483plugin for L<Kalamar>. It requires a C<Kustvakt> full server
1484with OAuth 2.0 capabilities.
1485It is activated by loading C<Auth> as a plugin in the C<Kalamar.plugins>
1486parameter in the Kalamar configuration.
1487
1488=head1 CONFIGURATION
1489
1490L<Kalamar::Plugin::Auth> supports the following parameter for the
1491C<Kalamar-Auth> configuration section in the Kalamar configuration:
1492
1493=over 2
1494
1495=item B<client_id>
1496
1497The client identifier of Kalamar to be send with every OAuth 2.0
1498management request.
1499
1500=item B<client_secret>
1501
1502The client secret of Kalamar to be send with every OAuth 2.0
1503management request.
1504
1505=item B<oauth2>
1506
1507Initially L<Kalamar-Plugin-Auth> was based on JWT. This parameter
1508is historically used to switch between oauth2 and jwt. It is expected
1509to be deprecated in the future, but for the moment it is required
1510to be set to a true value.
1511
1512=item B<experimental_client_registration>
1513
1514Activates the oauth client registration flow.
1515
1516=back
1517
1518=head2 COPYRIGHT AND LICENSE
1519
1520Copyright (C) 2015-2020, L<IDS Mannheim|http://www.ids-mannheim.de/>
1521Author: L<Nils Diewald|http://nils-diewald.de/>
1522
1523Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
1524Corpus Analysis Platform at the
1525L<Leibniz Institute for the German Language (IDS)|http://ids-mannheim.de/>,
1526member of the
1527L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de>
1528and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
1529funded by the
1530L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
1531
1532Kalamar is free software published under the
1533L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
1534
1535=cut