blob: 66e4d2f3c7d9b93a7f024261bee325525fdbc6d4 [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';
6
Akroncdfd9d52019-07-23 11:35:00 +02007# This is a plugin to deal with the Kustvakt OAuth server.
8# It establishes both the JWT as well as the OAuth password
9# flow for login.
10# All tokens are stored in the session. Access tokens are short-lived,
11# which limits the effects of misuse.
12# Refresh tokens are bound to client id and client secret,
13# which again limits the effects of misuse.
14
15# TODO:
16# Establish a plugin 'OAuth' that works independent of 'Auth'.
17
Akron864c2932018-11-16 17:18:55 +010018# TODO:
Akronc82b1bc2018-11-18 18:06:14 +010019# CSRF-protect logout!
Akron864c2932018-11-16 17:18:55 +010020
Akroncdfd9d52019-07-23 11:35:00 +020021# TODO:
22# Remove the Bearer prefix from auth.
23
24# In case no expiration time is returned by the server,
25# take this time.
Akron8bbbecf2019-07-01 18:57:30 +020026our $EXPECTED_EXPIRATION_IN = 259200;
27
Akron864c2932018-11-16 17:18:55 +010028# Register the plugin
29sub register {
30 my ($plugin, $app, $param) = @_;
31
Akron864c2932018-11-16 17:18:55 +010032 # Load parameter from config file
33 if (my $config_param = $app->config('Kalamar-Auth')) {
34 $param = { %$param, %$config_param };
35 };
36
Akron864c2932018-11-16 17:18:55 +010037 # Load 'notifications' plugin
38 unless (exists $app->renderer->helpers->{notify}) {
39 $app->plugin(Notifications => {
40 HTML => 1
41 });
42 };
43
Akron33f5c672019-06-24 19:40:47 +020044 # Get the client id and the client_secret as a requirement
45 unless ($param->{client_id} && $param->{client_secret}) {
46 $app->log->error('client_id or client_secret not defined');
47 };
Akron864c2932018-11-16 17:18:55 +010048
Akron59992122019-10-29 11:28:45 +010049 # Load localize
Akron864c2932018-11-16 17:18:55 +010050 $app->plugin('Localize' => {
51 dict => {
52 Auth => {
53 _ => sub { $_->locale },
54 de => {
55 loginSuccess => 'Anmeldung erfolgreich',
56 loginFail => 'Anmeldung fehlgeschlagen',
57 logoutSuccess => 'Abmeldung erfolgreich',
58 logoutFail => 'Abmeldung fehlgeschlagen',
59 csrfFail => 'Fehlerhafter CSRF Token',
Akron8bbbecf2019-07-01 18:57:30 +020060 openRedirectFail => 'Weiterleitungsfehler',
Akroncdfd9d52019-07-23 11:35:00 +020061 tokenExpired => 'Zugriffstoken abgelaufen',
62 tokenInvalid => 'Zugriffstoken ungültig',
63 refreshFail => 'Fehlerhafter Refresh-Token',
Akron59992122019-10-29 11:28:45 +010064 responseError => 'Unbekannter Autorisierungsfehler',
Akronc1aaf932021-06-09 12:19:15 +020065 revokeFail => 'Der Token kann nicht widerrufen werden',
66 revokeSuccess => 'Der Token wurde erfolgreich widerrufen',
Akron59992122019-10-29 11:28:45 +010067 paramError => 'Einige Eingaben sind fehlerhaft',
68 redirectUri => 'Weiterleitungs-Adresse',
69 homepage => 'Webseite',
70 desc => 'Kurzbeschreibung',
Akronc1aaf932021-06-09 12:19:15 +020071 revoke => 'Widerrufen',
Akron59992122019-10-29 11:28:45 +010072 clientCredentials => 'Client Daten',
73 clientType => 'Art der Client-Applikation',
74 clientName => 'Name der Client-Applikation',
75 clientID => 'ID der Client-Applikation',
76 clientSecret => 'Client-Secret',
77 clientRegister => 'Neue Client-Applikation registrieren',
78 registerSuccess => 'Registrierung erfolgreich',
79 registerFail => 'Registrierung fehlgeschlagen',
80 oauthSettings => 'OAuth',
Akrondc50c892021-05-05 18:12:02 +020081 oauthUnregister => 'Möchten sie <span class="client-name"><%= $client_name %></span> wirklich löschen?',
Akron83209f72021-01-29 17:54:15 +010082 loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
Akrondc50c892021-05-05 18:12:02 +020083 oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $client_name %></span>',
Akron9ffb4a32021-06-08 16:11:21 +020084 accessToken => 'Access Token',
Akronc1aaf932021-06-09 12:19:15 +020085 oauthRevokeToken => 'Widerrufe einen Token für <span class="client-name"><%= $client_name %></span>',
Akron864c2932018-11-16 17:18:55 +010086 },
87 -en => {
88 loginSuccess => 'Login successful',
89 loginFail => 'Access denied',
90 logoutSuccess => 'Logout successful',
91 logoutFail => 'Logout failed',
92 csrfFail => 'Bad CSRF token',
Akron8bbbecf2019-07-01 18:57:30 +020093 openRedirectFail => 'Redirect failure',
Akroncdfd9d52019-07-23 11:35:00 +020094 tokenExpired => 'Access token expired',
95 tokenInvalid => 'Access token invalid',
96 refreshFail => 'Bad refresh token',
Akron59992122019-10-29 11:28:45 +010097 responseError => 'Unknown authorization error',
Akronc1aaf932021-06-09 12:19:15 +020098 revokeFail => 'Token can\'t be revoked',
99 revokeSuccess => 'Token was revoked successfully',
Akron59992122019-10-29 11:28:45 +0100100 paramError => 'Some fields are invalid',
101 redirectUri => 'Redirect URI',
102 homepage => 'Homepage',
103 desc => 'Short description',
Akronc1aaf932021-06-09 12:19:15 +0200104 revoke => 'Revoke',
Akron59992122019-10-29 11:28:45 +0100105 clientCredentials => 'Client Credentials',
106 clientType => 'Type of the client application',
107 clientName => 'Name of the client application',
108 clientID => 'ID of the client application',
109 clientSecret => 'Client secret',
110 clientRegister => 'Register new client application',
111 registerSuccess => 'Registration successful',
112 registerFail => 'Registration denied',
113 oauthSettings => 'OAuth',
Akrondc50c892021-05-05 18:12:02 +0200114 oauthUnregister => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
Akron83209f72021-01-29 17:54:15 +0100115 loginHint => 'Maybe you need to log in first?',
Akronc1aaf932021-06-09 12:19:15 +0200116 oauthIssueToken => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
Akron9ffb4a32021-06-08 16:11:21 +0200117 accessToken => 'Access Token',
Akronc1aaf932021-06-09 12:19:15 +0200118 oauthRevokeToken => 'Revoka a token for <span class="client-name"><%= $client_name %></span>',
Akron864c2932018-11-16 17:18:55 +0100119 }
120 }
121 }
122 });
123
124
Akrona9c8b0e2018-11-16 20:20:28 +0100125 # Add login frame to sidebar
126 $app->content_block(
127 sidebar => {
128 template => 'partial/auth/login'
129 }
130 );
131
132
Akronc82b1bc2018-11-18 18:06:14 +0100133 # Add logout button to header button list
134 $app->content_block(
135 headerButtonGroup => {
136 template => 'partial/auth/logout'
137 }
138 );
139
Akron27031aa2020-04-28 14:57:10 +0200140
141 # Add hook after search
142 $app->hook(
143 after_search => sub {
144 my $c = shift;
145
146 # User is not logged in
147 if ($c->stash('results')->size == 0 && !$c->auth->token) {
148 $c->content_for(
149 'after_search_results' =>
150 $c->render_to_string(
151 inline => '<p class="hint"><%= loc "Auth_loginHint" %></p>'
152 )
153 );
154 };
155 }
156 );
157
Akron59992122019-10-29 11:28:45 +0100158 # The plugin path
159 my $path = catdir(dirname(__FILE__), 'Auth');
160
161 # Append "templates"
162 push @{$app->renderer->paths}, catdir($path, 'templates');
Akron864c2932018-11-16 17:18:55 +0100163
Akron4796e002019-07-05 10:13:15 +0200164 # Get or set the user token necessary for authorization
Akron864c2932018-11-16 17:18:55 +0100165 $app->helper(
166 'auth.token' => sub {
Akroncdfd9d52019-07-23 11:35:00 +0200167 my ($c, $token, $expires_in) = @_;
Akron864c2932018-11-16 17:18:55 +0100168
Akroncdfd9d52019-07-23 11:35:00 +0200169 if ($token) {
170 # Set auth token
Akron4796e002019-07-05 10:13:15 +0200171 $c->stash(auth => $token);
Akroncdfd9d52019-07-23 11:35:00 +0200172 $c->session(auth => $token);
173 $c->session(auth_exp => time + $expires_in);
174 return 1;
Akron4796e002019-07-05 10:13:15 +0200175 };
176
Akroncdfd9d52019-07-23 11:35:00 +0200177 # Get token from stash
178 $token = $c->stash('auth');
179
180 return $token if $token;
181
182 # Get auth from session
183 $token = $c->session('auth') or return;
184 $c->stash(auth => $token);
185
186 # Return stashed value
187 return $token;
Akron864c2932018-11-16 17:18:55 +0100188 }
189 );
190
191
192 # Log in to the system
193 my $r = $app->routes;
Akron864c2932018-11-16 17:18:55 +0100194
Akron33f5c672019-06-24 19:40:47 +0200195 if ($param->{oauth2}) {
Akron864c2932018-11-16 17:18:55 +0100196
Akron8bbbecf2019-07-01 18:57:30 +0200197 my $client_id = $param->{client_id};
198 my $client_secret = $param->{client_secret};
199
Akroncdfd9d52019-07-23 11:35:00 +0200200
201 # Sets a requested token and returns
202 # an error, if it didn't work
Akron8bbbecf2019-07-01 18:57:30 +0200203 $app->helper(
Akroncdfd9d52019-07-23 11:35:00 +0200204 'auth.set_tokens_p' => sub {
205 my ($c, $json) = @_;
206 my $promise = Mojo::Promise->new;
207
208 # No json object
209 unless ($json) {
210 return $promise->reject({
211 message => 'Response is no valid JSON object (remote)'
212 });
213 };
214
215 # There is an error here
216 # Dealing with errors here
217 if ($json->{error} && ref $json->{error} ne 'ARRAY') {
218 return $promise->reject(
219 {
220 message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
221 }
222 );
223 }
224
225 # There is an array of errors
226 elsif (my $error = $json->{errors} // $json->{error}) {
227 if (ref $error eq 'ARRAY') {
228 my @errors = ();
229 foreach (@{$error}) {
230 if ($_->[1]) {
231 push @errors, { code => $_->[0], message => $_->[1]}
232 }
233 }
234 return $promise->reject(@errors);
235 }
236
237 return $promise->reject({message => $error});
238 };
239
240 # Everything is fine
241 my $access_token = $json->{access_token};
Akronc58bfc42020-10-05 12:09:45 +0200242 my $token_type = $json->{token_type};
Akroncdfd9d52019-07-23 11:35:00 +0200243 my $refresh_token = $json->{refresh_token};
244 my $expires_in = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
245 my $auth = $token_type . ' ' . $access_token;
246 # my $scope = $json->{scope};
247
248 # Set session info
249 $c->session(auth => $auth);
250
251 # Expiration of the token minus tolerance
252 $c->session(auth_exp => time + $expires_in - 60);
253
254 # Set session info for refresh token
255 # This can be stored in the session, as it is useless
256 # unless the client secret is stolen
257 $c->session(auth_r => $refresh_token) if $refresh_token;
258
259 # Set stash info
260 $c->stash(auth => $auth);
261
262 return $promise->resolve;
263 }
264 );
265
266
267 # Refresh tokens and return a promise
268 $app->helper(
269 'auth.refresh_p' => sub {
Akron8bbbecf2019-07-01 18:57:30 +0200270 my $c = shift;
271 my $refresh_token = shift;
272
Akron8bbbecf2019-07-01 18:57:30 +0200273 # Get OAuth access token
Akroncdfd9d52019-07-23 11:35:00 +0200274 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
Akron8bbbecf2019-07-01 18:57:30 +0200275
Akron4cefe1f2019-09-04 10:11:28 +0200276 $c->app->log->debug("Refresh at $r_url");
Akroncdfd9d52019-07-23 11:35:00 +0200277
278 return $c->kalamar_ua->post_p($r_url, {} => form => {
Akron8bbbecf2019-07-01 18:57:30 +0200279 grant_type => 'refresh_token',
280 client_id => $client_id,
281 client_secret => $client_secret,
282 refresh_token => $refresh_token
283 })->then(
284 sub {
Akroncdfd9d52019-07-23 11:35:00 +0200285 my $tx = shift;
286 my $json = $tx->result->json;
287
288 # Response is fine
289 if ($tx->res->is_success) {
290
291 $c->app->log->info("Refresh was successful");
292
293 # Set the tokens and return a promise
294 return $c->auth->set_tokens_p($json);
295 };
296
297 # There is a client error - refresh fails
298 if ($tx->res->is_client_error && $json) {
299
300 $c->stash(auth => undef);
301 $c->stash(auth_exp => undef);
302 delete $c->session->{user};
303 delete $c->session->{auth};
304 delete $c->session->{auth_r};
305 delete $c->session->{auth_exp};
306
307 # Response is 400
308 return Mojo::Promise->reject(
309 $json->{error_description} // $c->loc('Auth_refreshFail')
310 );
311 };
312
313 $c->notify(error => $c->loc('Auth_responseError'));
314 return Mojo::Promise->reject;
315 }
316 )
317 }
318 );
319
Akron0f1b93b2020-03-17 11:37:19 +0100320 # Get a list of registered clients
321 $app->helper(
322 'auth.client_list_p' => sub {
323 my $c = shift;
324
325 # Get list of registered clients
326 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/list');
327
Akron1a9d5be2020-03-19 17:28:33 +0100328 # Get the list of all clients
Akron0f1b93b2020-03-17 11:37:19 +0100329 return $c->korap_request(post => $r_url, {} => form => {
330 client_id => $client_id,
331 client_secret => $client_secret,
332 authorized_only => 'no'
333 })->then(
334 sub {
335 my $tx = shift;
336 my $json = $tx->result->json;
337
338 # Response is fine
339 if ($tx->res->is_success) {
340 return Mojo::Promise->resolve($json);
341 };
342
343 $c->log->error($c->dumper($tx->res->to_string));
344
345 # Failure
346 $c->notify(error => $c->loc('Auth_responseError'));
347 return Mojo::Promise->reject($json // 'No response');
348 }
349 );
350 }
351 );
Akroncdfd9d52019-07-23 11:35:00 +0200352
Akron1a9d5be2020-03-19 17:28:33 +0100353
Akronbc94a9c2021-04-15 00:07:35 +0200354 # Get a list of registered clients
355 $app->helper(
356 'auth.token_list_p' => sub {
357 my $c = shift;
358 my $user_client_id = shift;
359
360 # Revoke the token
361 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token/list');
362
363 my $form = {
364 super_client_id => $client_id,
365 super_client_secret => $client_secret,
366 token_type => 'access_token',
367 };
368
369 if ($user_client_id) {
370 $form->{client_id} = $user_client_id;
371 };
372
373 # Get the list of all clients
374 return $c->korap_request(post => $r_url, {} => form => $form)->then(
375 sub {
376 my $tx = shift;
377 my $json = $tx->result->json;
378
379 # Response is fine
380 if ($tx->res->is_success) {
381 return Mojo::Promise->resolve($json);
382 };
383
384 $c->log->error($c->dumper($tx->res->to_string));
385
386 # Failure
387 $c->notify(error => $c->loc('Auth_responseError'));
388 return Mojo::Promise->reject($json // 'No response');
389 }
390 );
391 }
392 );
393
394
Akroncdfd9d52019-07-23 11:35:00 +0200395 # Issue a korap request with "oauth"orization
396 # This will override the core request helper
397 $app->helper(
398 korap_request => sub {
399 my $c = shift;
400 my $method = shift;
401 my $path = shift;
402 my @param = @_;
403
404 # TODO:
405 # Check if $tx is not leaked!
406
407 # Get plugin user agent
408 my $ua = $c->kalamar_ua;
409
410 my $url = Mojo::URL->new($path);
411 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
412
413 # Set X-Forwarded for
414 $tx->req->headers->header(
415 'X-Forwarded-For' => $c->client_ip
416 );
417
418 # Emit Hook to alter request
419 $c->app->plugins->emit_hook(
420 before_korap_request => ($c, $tx)
421 );
422
423 my $h = $tx->req->headers;
424
425 # If the request already has an Authorization
426 # header, respect it!
427 if ($h->authorization) {
428 return $ua->start_p($tx);
429 };
430
431 # Get auth token
432 if (my $auth_token = $c->auth->token) {
433
434 # The token is already expired!
435 my $exp = $c->session('auth_exp');
436 if (defined $exp && $exp < time) {
437
438 # Remove auth ...
439 $c->stash(auth => undef);
440
441 # And get refresh token from session
442 if (my $refresh_token = $c->session('auth_r')) {
443
444 $c->app->log->debug("Refresh is required");
445
446 # Refresh
447 return $c->auth->refresh_p($refresh_token)->then(
448 sub {
449 $c->app->log->debug("Search with refreshed tokens");
450
451 # Tokens were set - now send the request the first time!
452 $tx->req->headers->authorization($c->stash('auth'));
453 return $ua->start_p($tx);
454 }
455 );
456 }
457
458 # The token is expired and no refresh token is
459 # available - issue an unauthorized request!
460 else {
461 $c->stash(auth => undef);
462 $c->stash(auth_exp => undef);
463 delete $c->session->{user};
464 delete $c->session->{auth};
465 delete $c->session->{auth_r};
466 delete $c->session->{auth_exp};
467
468 # Warn on Error!
469 $c->notify(warn => $c->loc('Auth_tokenExpired'));
470 return $ua->start_p($tx);
471 };
472 }
473
474 # Auth token is fine
475 else {
476
477 # Set auth
478 $h->authorization($auth_token);
479 }
480 }
481
482 # No token set
483 else {
484
485 # Return unauthorized request
486 return $ua->start_p($tx);
487 };
488
489 # Issue an authorized request and automatically
490 # refresh the token on expiration!
491 return $ua->start_p($tx)->then(
492 sub {
493 my $tx = shift;
494
495 # Response is fine
496 if ($tx->res->is_success) {
497 return Mojo::Promise->resolve($tx);
498 }
499
500 # There is a client error - maybe refresh!
501 elsif ($tx->res->is_client_error) {
502
503 # Check the error
504 my $json = $tx->res->json('/errors/0/1');
505 if ($json && ($json =~ /expired|invalid/)) {
506 $c->stash(auth => undef);
507 $c->stash(auth_exp => undef);
508 delete $c->session->{user};
509 delete $c->session->{auth};
510
511 # And get refresh token from session
512 if (my $refresh_token = $c->session('auth_r')) {
513
514 # Refresh
515 return $c->auth->refresh_p($refresh_token)->then(
516 sub {
517 $c->app->log->debug("Search with refreshed tokens");
518
519 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
520
521 # Set X-Forwarded for
522 $tx->req->headers->header(
523 'X-Forwarded-For' => $c->client_ip
524 );
525
526 # Tokens were set - now send the request the first time!
527 $tx->req->headers->authorization($c->stash('auth'));
528 return $ua->start_p($tx);
529 }
530 )
531 };
532
533 # Reject the invalid token
534 $c->notify(error => $c->loc('Auth_tokenInvalid'));
535 return Mojo::Promise->reject;
536 };
537
538 return Mojo::Promise->resolve($tx);
539 };
540
541 $c->notify(error => $c->loc('Auth_responseError'));
542 return Mojo::Promise->reject;
Akron8bbbecf2019-07-01 18:57:30 +0200543 }
544 );
545 }
546 );
547
Akroncdfd9d52019-07-23 11:35:00 +0200548 # Password flow for OAuth
Akron33f5c672019-06-24 19:40:47 +0200549 $r->post('/user/login')->to(
550 cb => sub {
551 my $c = shift;
Akron864c2932018-11-16 17:18:55 +0100552
Akron33f5c672019-06-24 19:40:47 +0200553 # Validate input
554 my $v = $c->validation;
Akrone208d302020-11-28 11:14:50 +0100555 $v->required('handle', 'trim');
Akron33f5c672019-06-24 19:40:47 +0200556 $v->required('pwd', 'trim');
557 $v->csrf_protect;
558 $v->optional('fwd')->closed_redirect;
Akron864c2932018-11-16 17:18:55 +0100559
Akrone208d302020-11-28 11:14:50 +0100560 my $user = $v->param('handle');
Akron33f5c672019-06-24 19:40:47 +0200561 my $fwd = $v->param('fwd');
Akron864c2932018-11-16 17:18:55 +0100562
Akron33f5c672019-06-24 19:40:47 +0200563 # Set flash for redirect
Akrone208d302020-11-28 11:14:50 +0100564 $c->flash(handle => $user);
Akron864c2932018-11-16 17:18:55 +0100565
Akron33f5c672019-06-24 19:40:47 +0200566 if ($v->has_error || index($user, ':') >= 0) {
567 if ($v->has_error('fwd')) {
568 $c->notify(error => $c->loc('Auth_openRedirectFail'));
569 }
570 elsif ($v->has_error('csrf_token')) {
571 $c->notify(error => $c->loc('Auth_csrfFail'));
572 }
573 else {
574 $c->notify(error => $c->loc('Auth_loginFail'));
Akron864c2932018-11-16 17:18:55 +0100575 };
576
Akron864c2932018-11-16 17:18:55 +0100577 return $c->relative_redirect_to($fwd // 'index');
578 }
Akron864c2932018-11-16 17:18:55 +0100579
Akron33f5c672019-06-24 19:40:47 +0200580 my $pwd = $v->param('pwd');
Akron864c2932018-11-16 17:18:55 +0100581
Akroncdfd9d52019-07-23 11:35:00 +0200582 $c->app->log->debug("Login from user $user");
Akron33f5c672019-06-24 19:40:47 +0200583
584 # <specific>
585
586 # Get OAuth access token
587 my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
588
589 # Korap request for login
590 $c->korap_request('post', $url, {}, form => {
591 grant_type => 'password',
592 username => $user,
593 password => $pwd,
Akron8bbbecf2019-07-01 18:57:30 +0200594 client_id => $client_id,
595 client_secret => $client_secret
Akron33f5c672019-06-24 19:40:47 +0200596 })->then(
597 sub {
Akron8bbbecf2019-07-01 18:57:30 +0200598 # Set the tokens and return a promise
Akroncdfd9d52019-07-23 11:35:00 +0200599 return $c->auth->set_tokens_p(shift->result->json)
Akron33f5c672019-06-24 19:40:47 +0200600 }
Akron3b3c7af2020-05-15 16:23:55 +0200601 )->then(
602 sub {
603 # Set user info
604 $c->session(user => $user);
605 $c->stash(user => $user);
606
607 # Notify on success
608 $c->app->log->debug(qq!Login successful: "$user"!);
609 $c->notify(success => $c->loc('Auth_loginSuccess'));
610 }
Akron33f5c672019-06-24 19:40:47 +0200611 )->catch(
612 sub {
Akron33f5c672019-06-24 19:40:47 +0200613
Akron8bbbecf2019-07-01 18:57:30 +0200614 # Notify the user on login failure
615 unless (@_) {
616 $c->notify(error => $c->loc('Auth_loginFail'));
617 }
Akron33f5c672019-06-24 19:40:47 +0200618
Akron8bbbecf2019-07-01 18:57:30 +0200619 # There are known errors
620 foreach (@_) {
621 if (ref $_ eq 'HASH') {
622 my $err = ($_->{code} ? $_->{code} . ': ' : '') .
623 $_->{message};
624 $c->notify(error => $err);
625 # Log failure
626 $c->app->log->debug($err);
627 }
628 else {
629 $c->notify(error => $_);
630 $c->app->log->debug($_);
631 };
632 };
Akron33f5c672019-06-24 19:40:47 +0200633
634 $c->app->log->debug(qq!Login fail: "$user"!);
Akron8bbbecf2019-07-01 18:57:30 +0200635 }
Akron33f5c672019-06-24 19:40:47 +0200636 )->finally(
637 sub {
Akron33f5c672019-06-24 19:40:47 +0200638 # Redirect to slash
639 return $c->relative_redirect_to($fwd // 'index');
640 }
641 )
642
643 # Start IOLoop
644 ->wait;
645
646 return 1;
647 }
648 )->name('login');
Akroncdfd9d52019-07-23 11:35:00 +0200649
650
651 # Log out of the session
Akron4cefe1f2019-09-04 10:11:28 +0200652 $r->get('/user/logout')->to(
653 cb => sub {
654 my $c = shift;
655
656 # TODO: csrf-protection!
657
658 my $refresh_token = $c->session('auth_r');
659
660 # Revoke the token
661 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke');
662
663 $c->kalamar_ua->post_p($url => {} => form => {
664 client_id => $client_id,
665 client_secret => $client_secret,
666 token => $refresh_token,
667 token_type => 'refresh_token'
668 })->then(
669 sub {
670 my $tx = shift;
671 my $json = $tx->result->json;
672
673 my $promise;
674
675 # Response is fine
676 if ($tx->res->is_success) {
677 $c->app->log->info("Revocation was successful");
678 $c->notify(success => $c->loc('Auth_logoutSuccess'));
679
680 $c->stash(auth => undef);
681 $c->stash(auth_exp => undef);
Akrone208d302020-11-28 11:14:50 +0100682 $c->flash(handle => delete $c->session->{user});
Akron4cefe1f2019-09-04 10:11:28 +0200683 delete $c->session->{auth};
684 delete $c->session->{auth_r};
685 delete $c->session->{auth_exp};
686 return Mojo::Promise->resolve;
687 }
688
689 # Token may be invalid
690 $c->notify('error', $c->loc('Auth_logoutFail'));
691
692 # There is a client error - refresh fails
693 if ($tx->res->is_client_error && $json) {
694
695 return Mojo::Promise->reject(
696 $json->{error_description}
697 );
698 };
699
700 # Resource may not be found (404)
701 return Mojo::Promise->reject
702
703 }
704 )->catch(
705 sub {
706 my $err = shift;
707
708 # Server may be irresponsible
709 $c->notify('error', $c->loc('Auth_logoutFail'));
710 return Mojo::Promise->reject($err);
711 }
712 )->finally(
713 sub {
714 return $c->redirect_to('index');
715 }
716 )->wait;
717 }
718 )->name('logout');
Akron59992122019-10-29 11:28:45 +0100719
720 # If "experimental_registration" is set, open
721 # OAuth registration dialogues.
722 if ($param->{experimental_client_registration}) {
723
724 # Add settings
725 $app->navi->add(settings => (
726 $app->loc('Auth_oauthSettings'), 'oauth'
727 ));
728
729 # Route to oauth settings
730 $r->get('/settings/oauth')->to(
731 cb => sub {
Akron0f1b93b2020-03-17 11:37:19 +0100732 my $c = shift;
733
734 unless ($c->auth->token) {
735 return $c->render(
736 content => 'Unauthorized',
737 status => 401
738 );
739 };
740
741 # Wait for async result
742 $c->render_later;
743
744 $c->auth->client_list_p->then(
745 sub {
746 $c->stash('client_list' => shift);
747 }
748 )->catch(
749 sub {
750 return;
751 }
752 )->finally(
753 sub {
Akron17de86e2020-04-16 16:03:40 +0200754 return $c->render(template => 'auth/clients')
Akron0f1b93b2020-03-17 11:37:19 +0100755 }
756 );
Akron59992122019-10-29 11:28:45 +0100757 }
Akron1a9d5be2020-03-19 17:28:33 +0100758 )->name('oauth-settings');
Akron59992122019-10-29 11:28:45 +0100759
760 # Route to oauth client registration
761 $r->post('/settings/oauth/register')->to(
762 cb => sub {
763 my $c = shift;
764 my $v = $c->validation;
765
766 unless ($c->auth->token) {
Akron0f1b93b2020-03-17 11:37:19 +0100767 return $c->render(
768 content => 'Unauthorized',
769 status => 401
770 );
Akron59992122019-10-29 11:28:45 +0100771 };
772
773 $v->csrf_protect;
774 $v->required('name', 'trim')->size(3, 255);
775 $v->required('type')->in('PUBLIC', 'CONFIDENTIAL');
776 $v->required('desc', 'trim')->size(3, 255);
777 $v->optional('url', 'trim')->like(qr/^(http|$)/i);
Akrondc50c892021-05-05 18:12:02 +0200778 $v->optional('redirect_uri', 'trim')->like(qr/^(http|$)/i);
Akron59992122019-10-29 11:28:45 +0100779
780 # Render with error
781 if ($v->has_error) {
782 if ($v->has_error('csrf_token')) {
783 $c->notify(error => $c->loc('Auth_csrfFail'));
784 }
785 else {
Akronc1aaf932021-06-09 12:19:15 +0200786 $c->notify(error => $c->loc('Auth_paramError'));
Akron59992122019-10-29 11:28:45 +0100787 };
Akron83209f72021-01-29 17:54:15 +0100788 # return $c->redirect_to('oauth-settings');
789 return $c->render(template => 'auth/clients');
Akron59992122019-10-29 11:28:45 +0100790 };
791
792 # Wait for async result
793 $c->render_later;
794
795 # Register on server
796 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/client/register');
797 $c->korap_request('POST', $url => {} => json => {
798 name => $v->param('name'),
799 type => $v->param('type'),
800 description => $v->param('desc'),
801 url => $v->param('url'),
Akrondc50c892021-05-05 18:12:02 +0200802 redirect_uri => $v->param('redirect_uri')
Akron59992122019-10-29 11:28:45 +0100803 })->then(
804 sub {
805 my $tx = shift;
806 my $result = $tx->result;
807
808 if ($result->is_error) {
809 return Mojo::Promise->reject;
810 };
811
812 my $json = $result->json;
813
Akron59992122019-10-29 11:28:45 +0100814 my $client_id = $json->{client_id};
815 my $client_secret = $json->{client_secret};
816
817 $c->stash('client_name' => $v->param('name'));
818 $c->stash('client_desc' => $v->param('desc'));
819 $c->stash('client_type' => $v->param('type'));
820 $c->stash('client_url' => $v->param('url'));
Akrondc50c892021-05-05 18:12:02 +0200821 $c->stash('client_redirect_uri' => $v->param('redirect_uri'));
Akron59992122019-10-29 11:28:45 +0100822 $c->stash('client_id' => $client_id);
823
824 if ($client_secret) {
825 $c->stash('client_secret' => $client_secret);
826 };
827
828 $c->notify(success => $c->loc('Auth_en_registerSuccess'));
829
Akron17de86e2020-04-16 16:03:40 +0200830 return $c->render(template => 'auth/client');
Akron59992122019-10-29 11:28:45 +0100831 }
832 )->catch(
833 sub {
834 # Server may be irresponsible
835 my $err = shift;
836 $c->notify('error' => $c->loc('Auth_en_registerFail'));
837 return Mojo::Promise->reject($err);
838 }
839 )->finally(
840 sub {
841 return $c->redirect_to('settings' => { scope => 'oauth' });
842 }
843 );
844 }
845 )->name('oauth-register');
Akron1a9d5be2020-03-19 17:28:33 +0100846
847
Akron17de86e2020-04-16 16:03:40 +0200848 # Unregister client
Akron1a9d5be2020-03-19 17:28:33 +0100849 $r->get('/settings/oauth/unregister/:client_id')->to(
850 cb => sub {
851 shift->render(template => 'auth/unregister');
852 }
853 )->name('oauth-unregister');
854
Akron17de86e2020-04-16 16:03:40 +0200855
Akron1a9d5be2020-03-19 17:28:33 +0100856 # Unregister client
857 $r->post('/settings/oauth/unregister')->to(
858 cb => sub {
859 my $c = shift;
860
861 my $v = $c->validation;
862
863 unless ($c->auth->token) {
864 return $c->render(
865 content => 'Unauthorized',
866 status => 401
867 );
868 };
869
870 $v->csrf_protect;
871 $v->required('client-name', 'trim')->size(3, 255);
872 $v->required('client-id', 'trim')->size(3, 255);
873 $v->optional('client-secret');
874
875 # Render with error
876 if ($v->has_error) {
877 if ($v->has_error('csrf_token')) {
878 $c->notify(error => $c->loc('Auth_csrfFail'));
879 }
880 else {
Akronc1aaf932021-06-09 12:19:15 +0200881 $c->notify(error => $c->loc('Auth_paramError'));
Akron1a9d5be2020-03-19 17:28:33 +0100882 };
Akron83209f72021-01-29 17:54:15 +0100883 return $c->redirect_to('oauth-settings');
Akron1a9d5be2020-03-19 17:28:33 +0100884 };
885
886 my $client_id = $v->param('client-id');
887 my $client_name = $v->param('client-name');
888 my $client_secret = $v->param('client-secret');
889
890 # Get list of registered clients
891 my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/deregister/')->path(
892 $client_id
893 );
894
895 my $send = {};
896
897 if ($client_secret) {
898 $send->{client_secret} = $client_secret;
899 };
900
901 # Get the list of all clients
902 return $c->korap_request(delete => $r_url, {} => form => $send)->then(
903 sub {
904 my $tx = shift;
905
906 # Response is fine
907 if ($tx->res->is_success) {
908 # Okay
909 $c->notify(success => 'Successfully deleted ' . $client_name);
910 }
911 else {
912
913 # Failure
914 my $json = $tx->result->json;
915 if ($json && $json->{error_description}) {
916 $c->notify(error => $json->{error_description});
917 } else {
918 $c->notify(error => $c->loc('Auth_responseError'));
919 };
920 };
921
922 return $c->redirect_to('oauth-settings');
923 }
924 );
925 }
926 )->name('oauth-unregister-post');
Akron17de86e2020-04-16 16:03:40 +0200927
928
929 # Show information of a client
930 $r->get('/settings/oauth/client/:client_id')->to(
931 cb => sub {
932 my $c = shift;
933
934 $c->render_later;
935
936 $c->auth->client_list_p->then(
937 sub {
938 my $json = shift;
939
940 my ($item) = grep {
Akrondc50c892021-05-05 18:12:02 +0200941 $c->stash('client_id') eq $_->{client_id}
Akron17de86e2020-04-16 16:03:40 +0200942 } @$json;
943
944 unless ($item) {
945 return Mojo::Promise->reject;
946 };
947
Akrondc50c892021-05-05 18:12:02 +0200948 $c->stash(client_name => $item->{client_name});
Akronbc94a9c2021-04-15 00:07:35 +0200949 $c->stash(client_desc => $item->{client_description});
950 $c->stash(client_url => $item->{client_url});
Akron17de86e2020-04-16 16:03:40 +0200951 $c->stash(client_type => 'PUBLIC');
952
Akronbc94a9c2021-04-15 00:07:35 +0200953 $c->auth->token_list_p($c->stash('client_id'));
954 }
955 )->then(
956 sub {
957 my $json = shift;
958
959 $c->stash(tokens => $json);
960
Akron17de86e2020-04-16 16:03:40 +0200961 return Mojo::Promise->resolve;
962 }
963 )->catch(
964 sub {
965 return $c->reply->not_found;
966 }
967 )->finally(
968 sub {
Akron17de86e2020-04-16 16:03:40 +0200969 return $c->render(template => 'auth/client')
970 }
971 );
972
973 return;
974 }
975 )->name('oauth-tokens');
Akron59992122019-10-29 11:28:45 +0100976 };
Akron83209f72021-01-29 17:54:15 +0100977
978
Akronc1aaf932021-06-09 12:19:15 +0200979 # Ask if new token should be issued
980 $r->get('/settings/oauth/client/:client_id/token/issue')->to(
Akron83209f72021-01-29 17:54:15 +0100981 cb => sub {
982 shift->render(template => 'auth/issue-token');
983 }
984 )->name('oauth-issue-token');
985
986
Akronc1aaf932021-06-09 12:19:15 +0200987 # Ask if a token should be revoked
988 $r->post('/settings/oauth/client/:client_id/token/revoke')->to(
989 cb => sub {
990 shift->render(template => 'auth/revoke-token');
991 }
992 )->name('oauth-revoke-token');
993
994
995 # Issue new token
Akron83209f72021-01-29 17:54:15 +0100996 $r->post('/settings/oauth/client/:client_id/token')->to(
997 cb => sub {
998 my $c = shift;
999
1000 my $v = $c->validation;
1001
1002 unless ($c->auth->token) {
1003 return $c->render(
1004 content => 'Unauthorized',
1005 status => 401
1006 );
1007 };
1008
1009 $v->csrf_protect;
Akron83209f72021-01-29 17:54:15 +01001010 $v->optional('client-secret');
1011 $v->required('name', 'trim');
1012
1013 # Render with error
1014 if ($v->has_error) {
1015 if ($v->has_error('csrf_token')) {
1016 $c->notify(error => $c->loc('Auth_csrfFail'));
1017 }
1018 else {
Akronc1aaf932021-06-09 12:19:15 +02001019 $c->notify(error => $c->loc('Auth_paramError'));
Akron83209f72021-01-29 17:54:15 +01001020 };
1021 return $c->redirect_to('oauth-settings')
1022 };
1023
1024 # Get authorization token
1025 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
1026 my $client_id = $c->stash('client_id');
1027 my $name = $v->param('name');
1028 my $redirect_url = $c->url_for->query({name => $name});
1029
1030 return $c->korap_request(post => $r_url, {} => form => {
1031 response_type => 'code',
1032 client_id => $client_id,
1033 redirect_uri => $redirect_url,
1034 # TODO: State
1035 })->then(
1036 sub {
1037 my $tx = shift;
1038
1039 # Strip the token from the location header of the fake redirect
1040 # TODO: Alternatively redirect!
1041 my ($code, $scope, $loc, $name);
1042 foreach (@{$tx->redirects}) {
1043 $loc = $_->res->headers->header('Location');
1044 if (index($loc, 'code') > 0) {
1045 my $q = Mojo::URL->new($loc)->query;
1046 $code = $q->param('code');
1047 $scope = $q->param('scope');
1048 $name = $q->param('name');
1049 last;
1050 };
1051 };
1052
1053 # Fine!
1054 if ($code) {
1055 return Mojo::Promise->resolve(
1056 $client_id,
1057 $redirect_url,
1058 $code,
1059 $scope,
1060 $name
1061 );
1062 };
1063 return Mojo::Promise->reject;
1064 }
1065 )->then(
1066 sub {
1067 my ($client_id, $redirect_url, $code, $scope, $name) = @_;
1068
1069 # Get OAuth access token
1070 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
1071 return $c->kalamar_ua->post_p($r_url, {} => form => {
1072 client_id => $client_id,
1073 # NO CLIENT_SECRET YET SUPPORTED
1074 grant_type => 'authorization_code',
1075 code => $code,
1076 redirect_uri => $redirect_url
1077 })->then(
1078 sub {
1079 my $tx = shift;
1080 my $json = $tx->res->json;
1081
1082 if ($tx->res->is_error) {
1083 $c->notify(error => 'Unable to fetch new token');
1084 return Mojo::Promise->reject;
1085 };
1086
1087 $c->notify(success => 'New access token created');
1088
Akronbc94a9c2021-04-15 00:07:35 +02001089 $c->redirect_to('oauth-tokens' => { client_id => $client_id })
Akron83209f72021-01-29 17:54:15 +01001090 }
1091 )->catch(
1092 sub {
1093 my $err_msg = shift;
1094
1095 # Only raised in case of connection errors
1096 if ($err_msg) {
1097 $c->notify(error => { src => 'Backend' } => $err_msg)
1098 };
1099
1100 $c->render(
1101 status => 400,
1102 template => 'failure'
1103 );
1104 }
1105 )
1106
1107 # Start IOLoop
1108 ->wait;
1109
1110 }
1111 )->catch(
1112 sub {
1113 my $err_msg = shift;
1114
1115 # Only raised in case of connection errors
1116 if ($err_msg) {
1117 $c->notify(error => { src => 'Backend' } => $err_msg)
1118 };
1119
1120 return $c->render(
1121 status => 400,
1122 template => 'failure'
1123 );
1124 }
1125 )
1126
1127 # Start IOLoop
1128 ->wait;
1129
1130 return 1;
1131 }
1132 )->name('oauth-issue-token-post');
Akronc1aaf932021-06-09 12:19:15 +02001133
1134
1135 # Revoke token
1136 $r->delete('/settings/oauth/client/:client_id/token')->to(
1137 cb => sub {
1138 my $c = shift;
1139
1140 my $v = $c->validation;
1141
1142 unless ($c->auth->token) {
1143 return $c->render(
1144 content => 'Unauthorized',
1145 status => 401
1146 );
1147 };
1148
1149 $v->csrf_protect;
1150 $v->required('token', 'trim');
1151 $v->optional('name', 'trim');
1152 my $private_client_id = $c->stash('client_id');
1153
1154 # Render with error
1155 if ($v->has_error) {
1156 if ($v->has_error('csrf_token')) {
1157 $c->notify(error => $c->loc('Auth_csrfFail'));
1158 }
1159 else {
1160 $c->notify(error => $c->loc('Auth_paramError'));
1161 };
1162 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1163 };
1164
1165 # Revoke token using super client privileges
1166 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke/super');
1167
1168 my $token = $v->param('token');
1169
1170 return $c->korap_request(post => $r_url, {} => form => {
1171 super_client_id => $client_id,
1172 super_client_secret => $client_secret,
1173 token => $token
1174 })->then(
1175 sub {
1176 my $tx = shift;
1177
1178 # Response is fine
1179 if ($tx->res->is_success) {
1180 $c->notify(success => $c->loc('Auth_revokeSuccess'));
1181 return Mojo::Promise->resolve;
1182 };
1183
1184 return Mojo::Promise->reject;
1185 }
1186 )->catch(
1187 sub {
1188 my $err_msg = shift;
1189 if ($err_msg) {
1190 $c->notify(error => { src => 'Backend' } => $err_msg );
1191 }
1192 else {
1193 $c->notify(error => $c->loc('Auth_revokeFail'));
1194 };
1195 }
1196 )->finally(
1197 sub {
1198 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1199 }
1200 )
1201
1202 # Start IOLoop
1203 ->wait;
1204 }
1205 )->name('oauth-revoke-token-delete');
Akron33f5c672019-06-24 19:40:47 +02001206 }
Akroncdfd9d52019-07-23 11:35:00 +02001207
Akron33f5c672019-06-24 19:40:47 +02001208 # Use JWT login
Akroncdfd9d52019-07-23 11:35:00 +02001209 # (should be deprecated)
Akron33f5c672019-06-24 19:40:47 +02001210 else {
1211
Akroncdfd9d52019-07-23 11:35:00 +02001212 # Inject authorization to all korap requests
1213 $app->hook(
1214 before_korap_request => sub {
1215 my ($c, $tx) = @_;
1216 my $h = $tx->req->headers;
1217
1218 # If the request already has an Authorization
1219 # header, respect it
1220 unless ($h->authorization) {
1221
1222 # Get valid auth token and set as header
1223 if (my $auth_token = $c->auth->token) {
1224 $h->authorization($auth_token);
1225 };
1226 };
1227 }
1228 );
1229
1230 # Password flow with JWT
Akron33f5c672019-06-24 19:40:47 +02001231 $r->post('/user/login')->to(
1232 cb => sub {
1233 my $c = shift;
1234
1235 # Validate input
1236 my $v = $c->validation;
Akrone208d302020-11-28 11:14:50 +01001237 $v->required('handle', 'trim');
Akron33f5c672019-06-24 19:40:47 +02001238 $v->required('pwd', 'trim');
1239 $v->csrf_protect;
1240 $v->optional('fwd')->closed_redirect;
1241
Akrone208d302020-11-28 11:14:50 +01001242 my $user = $v->param('handle');
Akron33f5c672019-06-24 19:40:47 +02001243 my $fwd = $v->param('fwd');
1244
1245 # Set flash for redirect
Akrone208d302020-11-28 11:14:50 +01001246 $c->flash(handle => $user);
Akron33f5c672019-06-24 19:40:47 +02001247
1248 if ($v->has_error || index($user, ':') >= 0) {
1249 if ($v->has_error('fwd')) {
1250 $c->notify(error => $c->loc('Auth_openRedirectFail'));
1251 }
1252 elsif ($v->has_error('csrf_token')) {
1253 $c->notify(error => $c->loc('Auth_csrfFail'));
1254 }
1255 else {
1256 $c->notify(error => $c->loc('Auth_loginFail'));
1257 };
1258
1259 return $c->relative_redirect_to($fwd // 'index');
1260 }
1261
1262 my $pwd = $v->param('pwd');
1263
Akroncdfd9d52019-07-23 11:35:00 +02001264 $c->app->log->debug("Login from user $user");
Akron33f5c672019-06-24 19:40:47 +02001265
1266 my $url = Mojo::URL->new($c->korap->api)->path('auth/apiToken');
1267
1268 # Korap request for login
1269 $c->korap_request('get', $url, {
1270
1271 # Set authorization header
1272 Authorization => 'Basic ' . b("$user:$pwd")->b64_encode->trim,
1273
1274 })->then(
1275 sub {
1276 my $tx = shift;
1277
1278 # Get the java token
1279 my $jwt = $tx->result->json;
1280
1281 # No java web token
1282 unless ($jwt) {
1283 $c->notify(error => 'Response is no valid JWT (remote)');
1284 return;
1285 };
1286
1287 # There is an error here
1288 # Dealing with errors here
1289 if (my $error = $jwt->{error} // $jwt->{errors}) {
1290 if (ref $error eq 'ARRAY') {
1291 foreach (@$error) {
1292 unless ($_->[1]) {
1293 $c->notify(error => $c->loc('Auth_loginFail'));
1294 }
1295 else {
1296 $c->notify(error => $_->[0] . ($_->[1] ? ': ' . $_->[1] : ''));
1297 };
1298 };
1299 }
1300 else {
1301 $c->notify(error => 'There is an unknown JWT error');
1302 };
1303 return;
1304 };
1305
1306 # TODO: Deal with user return values.
1307 my $auth = $jwt->{token_type} . ' ' . $jwt->{token};
1308
Akroncdfd9d52019-07-23 11:35:00 +02001309 $c->app->log->debug(qq!Login successful: "$user"!);
Akron33f5c672019-06-24 19:40:47 +02001310
1311 $user = $jwt->{username} ? $jwt->{username} : $user;
1312
1313 # Set session info
1314 $c->session(user => $user);
1315 $c->session(auth => $auth);
1316
1317 # Set stash info
1318 $c->stash(user => $user);
1319 $c->stash(auth => $auth);
Akron33f5c672019-06-24 19:40:47 +02001320 $c->notify(success => $c->loc('Auth_loginSuccess'));
1321 }
1322 )->catch(
1323 sub {
1324 my $e = shift;
1325
1326 # Notify the user
1327 $c->notify(
1328 error =>
1329 ($e->{code} ? $e->{code} . ': ' : '') .
1330 $e->{message} . ' for Login (remote)'
1331 );
1332
1333 # Log failure
1334 $c->app->log->debug(
1335 ($e->{code} ? $e->{code} . ' - ' : '') .
1336 $e->{message}
1337 );
1338
1339 $c->app->log->debug(qq!Login fail: "$user"!);
1340 $c->notify(error => $c->loc('Auth_loginFail'));
1341 }
1342 )->finally(
1343 sub {
1344
1345 # Redirect to slash
1346 return $c->relative_redirect_to($fwd // 'index');
1347 }
1348 )
1349
1350 # Start IOLoop
1351 ->wait;
1352
1353 return 1;
1354 }
1355 )->name('login');
Akron4cefe1f2019-09-04 10:11:28 +02001356
1357
1358 # Log out of the session
1359 $r->get('/user/logout')->to(
1360 cb => sub {
1361 my $c = shift;
1362
1363 # TODO: csrf-protection!
1364
1365 # Log out of the system
1366 my $url = Mojo::URL->new($c->korap->api)->path('auth/logout');
1367
1368 $c->korap_request(
1369 'get', $url
1370 )->then(
1371 # Logged out
1372 sub {
1373 my $tx = shift;
1374 # Clear cache
1375 # ?? Necesseary
1376 # $c->chi('user')->remove($c->auth->token);
1377
1378 # TODO:
1379 # Revoke refresh token!
1380 # based on auth token!
1381 # my $refresh_token = $c->chi('user')->get('refr_' . $c->auth->token);
1382 # $c->auth->revoke_token($refresh_token)
1383
1384 # Expire session
1385 $c->session(user => undef);
1386 $c->session(auth => undef);
1387 $c->notify(success => $c->loc('Auth_logoutSuccess'));
1388 }
1389
1390 )->catch(
1391 # Something went wrong
1392 sub {
1393 # my $err_msg = shift;
1394 $c->notify('error', $c->loc('Auth_logoutFail'));
1395 }
1396
1397 )->finally(
1398 # Redirect
1399 sub {
1400 return $c->redirect_to('index');
1401 }
1402 )
1403
1404 # Start IOLoop
1405 ->wait;
1406
1407 return 1;
1408 }
1409 )->name('logout');
Akron33f5c672019-06-24 19:40:47 +02001410 };
Akron59992122019-10-29 11:28:45 +01001411
1412 $app->log->info('Successfully registered Auth plugin');
Akron864c2932018-11-16 17:18:55 +01001413};
1414
Akronb3f33592020-03-16 15:14:44 +01001415
Akron864c2932018-11-16 17:18:55 +010014161;
Akrona9c8b0e2018-11-16 20:20:28 +01001417
Akronc82b1bc2018-11-18 18:06:14 +01001418
Akrona9c8b0e2018-11-16 20:20:28 +01001419__END__
Akron59992122019-10-29 11:28:45 +01001420
1421=pod
1422
1423=encoding utf8
1424
1425=head1 NAME
1426
1427Kalamar::Plugin::Auth - OAuth-2.0-based authorization plugin
1428
1429=head1 DESCRIPTION
1430
1431L<Kalamar::Plugin::Auth> is an OAuth-2.0-based authorization
1432plugin for L<Kalamar>. It requires a C<Kustvakt> full server
1433with OAuth 2.0 capabilities.
1434It is activated by loading C<Auth> as a plugin in the C<Kalamar.plugins>
1435parameter in the Kalamar configuration.
1436
1437=head1 CONFIGURATION
1438
1439L<Kalamar::Plugin::Auth> supports the following parameter for the
1440C<Kalamar-Auth> configuration section in the Kalamar configuration:
1441
1442=over 2
1443
1444=item B<client_id>
1445
1446The client identifier of Kalamar to be send with every OAuth 2.0
1447management request.
1448
1449=item B<client_secret>
1450
1451The client secret of Kalamar to be send with every OAuth 2.0
1452management request.
1453
1454=item B<oauth2>
1455
1456Initially L<Kalamar-Plugin-Auth> was based on JWT. This parameter
1457is historically used to switch between oauth2 and jwt. It is expected
1458to be deprecated in the future, but for the moment it is required
1459to be set to a true value.
1460
1461=item B<experimental_client_registration>
1462
1463Activates the oauth client registration flow.
1464
1465=back
1466
1467=head2 COPYRIGHT AND LICENSE
1468
1469Copyright (C) 2015-2020, L<IDS Mannheim|http://www.ids-mannheim.de/>
1470Author: L<Nils Diewald|http://nils-diewald.de/>
1471
1472Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
1473Corpus Analysis Platform at the
1474L<Leibniz Institute for the German Language (IDS)|http://ids-mannheim.de/>,
1475member of the
1476L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de>
1477and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
1478funded by the
1479L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
1480
1481Kalamar is free software published under the
1482L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
1483
1484=cut