blob: bdb988d973230113a664a623527b56e84a18e63d [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';
Akron66ef3b52022-11-22 14:25:15 +01006use Mojo::File qw!path!;
Akrond91a1ca2022-05-20 16:45:01 +02007use Mojo::Util qw!b64_encode encode!;
Akron6b75d122022-05-12 17:39:05 +02008use Mojo::JSON qw'decode_json encode_json';
Akron6a228db2021-10-14 15:57:00 +02009use Encode 'is_utf8';
Akron864c2932018-11-16 17:18:55 +010010
Akroncdfd9d52019-07-23 11:35:00 +020011# This is a plugin to deal with the Kustvakt OAuth server.
Akroncdfd9d52019-07-23 11:35:00 +020012# All tokens are stored in the session. Access tokens are short-lived,
13# which limits the effects of misuse.
14# Refresh tokens are bound to client id and client secret,
15# which again limits the effects of misuse.
16
17# TODO:
18# Establish a plugin 'OAuth' that works independent of 'Auth'.
19
Akron864c2932018-11-16 17:18:55 +010020# TODO:
Akronc82b1bc2018-11-18 18:06:14 +010021# CSRF-protect logout!
Akron864c2932018-11-16 17:18:55 +010022
Akroncdfd9d52019-07-23 11:35:00 +020023# TODO:
24# Remove the Bearer prefix from auth.
25
26# In case no expiration time is returned by the server,
27# take this time.
Akron8bbbecf2019-07-01 18:57:30 +020028our $EXPECTED_EXPIRATION_IN = 259200;
29
Akron864c2932018-11-16 17:18:55 +010030# Register the plugin
31sub register {
32 my ($plugin, $app, $param) = @_;
33
Akron864c2932018-11-16 17:18:55 +010034 # Load parameter from config file
35 if (my $config_param = $app->config('Kalamar-Auth')) {
36 $param = { %$param, %$config_param };
37 };
38
Akrond91a1ca2022-05-20 16:45:01 +020039 if ($param->{jwt}) {
40 $app->log->error('JWT flow is no longer supported');
41 return;
42 };
43
Akron864c2932018-11-16 17:18:55 +010044 # Load 'notifications' plugin
45 unless (exists $app->renderer->helpers->{notify}) {
46 $app->plugin(Notifications => {
47 HTML => 1
48 });
49 };
50
Akron86276092022-05-20 15:36:05 +020051 # Set session default timeout
52 for ($app->sessions) {
53 $_->default_expiration(60*60*24*3); # Session expires after 3 days of non-use
54 };
55
Akron66ef3b52022-11-22 14:25:15 +010056 # Get client_id and client_secret from client file
Akron53a171e2022-12-05 18:22:58 +010057 if ($param->{client_file} || $main::ENV{KALAMAR_CLIENT_FILE}) {
58 $param->{client_file} ||= $main::ENV{KALAMAR_CLIENT_FILE};
Akron66ef3b52022-11-22 14:25:15 +010059 my $client_json = decode_json(path($param->{client_file})->slurp);
60 $param->{client_id} //= $client_json->{client_id};
61 $param->{client_secret} //= $client_json->{client_secret};
62 };
63
Akron33f5c672019-06-24 19:40:47 +020064 # Get the client id and the client_secret as a requirement
65 unless ($param->{client_id} && $param->{client_secret}) {
66 $app->log->error('client_id or client_secret not defined');
67 };
Akron864c2932018-11-16 17:18:55 +010068
Akron59992122019-10-29 11:28:45 +010069 # Load localize
Akron864c2932018-11-16 17:18:55 +010070 $app->plugin('Localize' => {
71 dict => {
Akrone997bb52021-06-11 16:44:06 +020072 de => {
73 abort => 'Abbrechen'
74 },
75 -en => {
76 abort => 'Abort'
77 },
Akron864c2932018-11-16 17:18:55 +010078 Auth => {
79 _ => sub { $_->locale },
80 de => {
Akrona8efaa92022-04-09 14:45:43 +020081 loginPlease => 'Bitte melden Sie sich an!',
Akron864c2932018-11-16 17:18:55 +010082 loginSuccess => 'Anmeldung erfolgreich',
83 loginFail => 'Anmeldung fehlgeschlagen',
84 logoutSuccess => 'Abmeldung erfolgreich',
85 logoutFail => 'Abmeldung fehlgeschlagen',
Akronff088112021-06-15 15:26:04 +020086 authenticationFail => 'Nicht authentifiziert',
Akron864c2932018-11-16 17:18:55 +010087 csrfFail => 'Fehlerhafter CSRF Token',
Akron2c142ab2023-01-30 13:21:57 +010088 scopeFail => 'Scope nicht definiert',
89 clientIDFail => 'Client ID nicht definiert',
Akron6a228db2021-10-14 15:57:00 +020090 invalidChar => 'Ungültiges Zeichen in Anfrage',
Akron8bbbecf2019-07-01 18:57:30 +020091 openRedirectFail => 'Weiterleitungsfehler',
Akroncdfd9d52019-07-23 11:35:00 +020092 tokenExpired => 'Zugriffstoken abgelaufen',
93 tokenInvalid => 'Zugriffstoken ungültig',
94 refreshFail => 'Fehlerhafter Refresh-Token',
Akron59992122019-10-29 11:28:45 +010095 responseError => 'Unbekannter Autorisierungsfehler',
Akroncce055c2021-07-02 12:18:03 +020096 serverError => 'Unbekannter Serverfehler',
Akronc1aaf932021-06-09 12:19:15 +020097 revokeFail => 'Der Token kann nicht widerrufen werden',
98 revokeSuccess => 'Der Token wurde erfolgreich widerrufen',
Akron59992122019-10-29 11:28:45 +010099 paramError => 'Einige Eingaben sind fehlerhaft',
Akroneb39cf32023-04-03 14:40:48 +0200100 redirectUri => 'Weiterleitungsadresse',
Akron9f2ad342022-05-04 16:16:40 +0200101 pluginSrc => 'Beschreibung des Plugins (*.json-Datei)',
Akron59992122019-10-29 11:28:45 +0100102 homepage => 'Webseite',
Helge9f9d4852024-12-09 16:06:34 +0100103 homepageReq => '*(Plugins)',
Akron59992122019-10-29 11:28:45 +0100104 desc => 'Kurzbeschreibung',
Akronc1aaf932021-06-09 12:19:15 +0200105 revoke => 'Widerrufen',
Akron59992122019-10-29 11:28:45 +0100106 clientCredentials => 'Client Daten',
107 clientType => 'Art der Client-Applikation',
108 clientName => 'Name der Client-Applikation',
109 clientID => 'ID der Client-Applikation',
110 clientSecret => 'Client-Secret',
111 clientRegister => 'Neue Client-Applikation registrieren',
112 registerSuccess => 'Registrierung erfolgreich',
113 registerFail => 'Registrierung fehlgeschlagen',
Akronb0a7a0f2025-02-27 09:51:06 +0100114 oauthSettings => 'API Tokens',
Helge278fbca2022-11-29 18:49:15 +0100115 #for marketplace settings
116 marketplace => 'Marktplatz',
Helgedb720ea2023-03-20 09:39:36 +0100117 plugins => 'Plugins',
118 instplugins => 'Bereits installierte Plugins',
119 regby => 'Registriert von',
120 regdate => 'Registrierungsdatum',
121 instdate=> 'Installationsdatum',
122 install => 'Installieren',
123 installFail => 'Plugin konnte nicht installiert werden',
Helged36478d2023-06-08 17:43:01 +0200124 uninstallFail => 'Plugin konnte nicht deinstalliert werden',
125 marketplaceFail => {
126 -long => 'Die Plugins konnten leider nicht angezeigt werden.',
127 short => 'Erneut versuchen'
128 },
Akrone997bb52021-06-11 16:44:06 +0200129 oauthUnregister => {
130 -long => 'Möchten sie <span class="client-name"><%= $client_name %></span> wirklich löschen?',
131 short => 'Löschen'
132 },
Akron9f2ad342022-05-04 16:16:40 +0200133 oauthHint => 'Die folgende Registrierung (und alle Angaben) für API-Clients folgen der <a href="https://oauth.net/" class="external">OAuth-2.0-Spezifikation</a>.',
Akron83209f72021-01-29 17:54:15 +0100134 loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
Akrone997bb52021-06-11 16:44:06 +0200135 oauthIssueToken => {
136 -long => 'Stelle einen neuen Token für <span class="client-name"><%= $client_name %></span> aus',
137 short => 'Neuen Token ausstellen'
138 },
Akron9ffb4a32021-06-08 16:11:21 +0200139 accessToken => 'Access Token',
Akrone997bb52021-06-11 16:44:06 +0200140 oauthRevokeToken => {
141 -long => 'Widerrufe einen Token für <span class="client-name"><%= $client_name %></span>',
142 short => 'Widerrufe'
143 },
Akrona8efaa92022-04-09 14:45:43 +0200144 oauthGrantScope => {
145 -long => '<span class="client-name"><%= $client_name %></span> möchte Zugriffsrechte',
146 short => 'Zugriffsrechte erteilen'
147 },
Akron408bc7c2022-04-28 15:46:43 +0200148 oauthGrantPublicWarn => 'Achtung - dies ist ein öffentlicher Client!',
Akron9d826902023-01-25 10:20:52 +0100149 oauthGrantRedirectWarn => 'Die Weiterleitung findet an eine unbekannte Adresse statt',
Akrone997bb52021-06-11 16:44:06 +0200150 createdAt => 'Erstellt am <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
Akron9f2ad342022-05-04 16:16:40 +0200151 expiresIn => 'Läuft in <%= stash("seconds") %> Sekunden ab.',
152 fileSizeExceeded => 'Dateigröße überschritten'
Akron864c2932018-11-16 17:18:55 +0100153 },
154 -en => {
Akrona8efaa92022-04-09 14:45:43 +0200155 loginPlease => 'Please log in!',
Akron864c2932018-11-16 17:18:55 +0100156 loginSuccess => 'Login successful',
157 loginFail => 'Access denied',
158 logoutSuccess => 'Logout successful',
159 logoutFail => 'Logout failed',
Akronff088112021-06-15 15:26:04 +0200160 authenticationFail => 'Not authenticated',
Akron864c2932018-11-16 17:18:55 +0100161 csrfFail => 'Bad CSRF token',
Akron2c142ab2023-01-30 13:21:57 +0100162 scopeFail => 'Scope required',
163 clientIDFail => 'Client ID required',
Akron6a228db2021-10-14 15:57:00 +0200164 invalidChar => 'Invalid character in request',
Akron8bbbecf2019-07-01 18:57:30 +0200165 openRedirectFail => 'Redirect failure',
Akroncdfd9d52019-07-23 11:35:00 +0200166 tokenExpired => 'Access token expired',
167 tokenInvalid => 'Access token invalid',
168 refreshFail => 'Bad refresh token',
Akron59992122019-10-29 11:28:45 +0100169 responseError => 'Unknown authorization error',
Akroncce055c2021-07-02 12:18:03 +0200170 serverError => 'Unknown server error',
Akronc1aaf932021-06-09 12:19:15 +0200171 revokeFail => 'Token can\'t be revoked',
172 revokeSuccess => 'Token was revoked successfully',
Akron59992122019-10-29 11:28:45 +0100173 paramError => 'Some fields are invalid',
174 redirectUri => 'Redirect URI',
Akron9f2ad342022-05-04 16:16:40 +0200175 pluginSrc => 'Declaration of the plugin (*.json file)',
Akron59992122019-10-29 11:28:45 +0100176 homepage => 'Homepage',
Helge9f9d4852024-12-09 16:06:34 +0100177 homepageReq =>'*(Plugins)',
Akron59992122019-10-29 11:28:45 +0100178 desc => 'Short description',
Akronc1aaf932021-06-09 12:19:15 +0200179 revoke => 'Revoke',
Akron59992122019-10-29 11:28:45 +0100180 clientCredentials => 'Client Credentials',
181 clientType => 'Type of the client application',
182 clientName => 'Name of the client application',
183 clientID => 'ID of the client application',
184 clientSecret => 'Client secret',
185 clientRegister => 'Register new client application',
186 registerSuccess => 'Registration successful',
187 registerFail => 'Registration denied',
Akronb0a7a0f2025-02-27 09:51:06 +0100188 oauthSettings => 'API tokens',
Helge278fbca2022-11-29 18:49:15 +0100189 #for marketplace settings
190 marketplace => 'Marketplace',
Helgedb720ea2023-03-20 09:39:36 +0100191 plugins => 'Plugins',
192 instplugins => 'Installed Plugins',
193 regby =>'Registered by',
Helge216a4822024-06-17 12:02:34 +0200194 regdate =>'Date of registration',
195 instdate =>'Installation date',
Helgedb720ea2023-03-20 09:39:36 +0100196 install => 'Install',
197 uninstall => 'Uninstall',
198 installFail => 'Plugin could not be installed',
Helged36478d2023-06-08 17:43:01 +0200199 uninstallFail => 'Plugin could not be uninstalled',
200 uninstallFail => 'Plugin could not be uninstalled',
201 marketplaceFail => {
202 -long => 'Plugins could not be displayed.',
203 short => 'Try again'
204 },
Akrone997bb52021-06-11 16:44:06 +0200205 oauthUnregister => {
206 -long => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
207 short => 'Unregister'
208 },
Akron9f2ad342022-05-04 16:16:40 +0200209 oauthHint => 'The following registration of API clients follows the <a href="https://oauth.net/" class="external">OAuth 2.0 specification</a>.',
Akron83209f72021-01-29 17:54:15 +0100210 loginHint => 'Maybe you need to log in first?',
Akrone997bb52021-06-11 16:44:06 +0200211 oauthIssueToken => {
212 -long => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
213 short => 'Issue new token'
214 },
Akron9ffb4a32021-06-08 16:11:21 +0200215 accessToken => 'Access Token',
Akrone997bb52021-06-11 16:44:06 +0200216 oauthRevokeToken => {
217 -long => 'Revoke a token for <span class="client-name"><%= $client_name %></span>',
218 short => 'Revoke'
219 },
Akrona8efaa92022-04-09 14:45:43 +0200220 oauthGrantScope => {
221 -long => '<span class="client-name"><%= $client_name %></span> wants to have access',
222 short => 'Grant access'
223 },
Akron408bc7c2022-04-28 15:46:43 +0200224 oauthGrantPublicWarn => 'Warning - this is a public client!',
Akron9d826902023-01-25 10:20:52 +0100225 oauthGrantRedirectWarn => 'The redirect points to an unknown location',
Akrone997bb52021-06-11 16:44:06 +0200226 createdAt => 'Created at <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
Akron9f2ad342022-05-04 16:16:40 +0200227 expiresIn => 'Expires in <%= stash("seconds") %> seconds.',
228 fileSizeExceeded => 'File size exceeded',
229 confidentialRequired => 'Plugins need to be confidential',
230 jsonRequired => 'Plugin declarations need to be json files',
Akron864c2932018-11-16 17:18:55 +0100231 }
232 }
233 }
234 });
235
236
Akrona9c8b0e2018-11-16 20:20:28 +0100237 # Add login frame to sidebar
Akron4d17d0f2025-03-05 20:58:44 +0100238 # $app->content_block(
239 # sidebar => {
240 # template => 'partial/auth/login'
241 # }
242 # );
Akrona9c8b0e2018-11-16 20:20:28 +0100243
244
Akronc82b1bc2018-11-18 18:06:14 +0100245 # Add logout button to header button list
Uyen-Nhu Trana17b08d2025-02-18 19:14:55 +0100246 # $app->content_block(
247 # headerButtonGroup => {
248 # template => 'partial/auth/logout'
249 # }
250 # );
Akronc82b1bc2018-11-18 18:06:14 +0100251
Akron27031aa2020-04-28 14:57:10 +0200252
253 # Add hook after search
254 $app->hook(
255 after_search => sub {
256 my $c = shift;
257
258 # User is not logged in
259 if ($c->stash('results')->size == 0 && !$c->auth->token) {
260 $c->content_for(
261 'after_search_results' =>
262 $c->render_to_string(
263 inline => '<p class="hint"><%= loc "Auth_loginHint" %></p>'
264 )
265 );
266 };
267 }
268 );
269
Akron59992122019-10-29 11:28:45 +0100270 # The plugin path
271 my $path = catdir(dirname(__FILE__), 'Auth');
272
273 # Append "templates"
274 push @{$app->renderer->paths}, catdir($path, 'templates');
Akron864c2932018-11-16 17:18:55 +0100275
Akron4796e002019-07-05 10:13:15 +0200276 # Get or set the user token necessary for authorization
Akron864c2932018-11-16 17:18:55 +0100277 $app->helper(
278 'auth.token' => sub {
Akroncdfd9d52019-07-23 11:35:00 +0200279 my ($c, $token, $expires_in) = @_;
Akron864c2932018-11-16 17:18:55 +0100280
Akroncdfd9d52019-07-23 11:35:00 +0200281 if ($token) {
282 # Set auth token
Akron4796e002019-07-05 10:13:15 +0200283 $c->stash(auth => $token);
Akroncdfd9d52019-07-23 11:35:00 +0200284 $c->session(auth => $token);
285 $c->session(auth_exp => time + $expires_in);
286 return 1;
Akron4796e002019-07-05 10:13:15 +0200287 };
288
Akroncdfd9d52019-07-23 11:35:00 +0200289 # Get token from stash
290 $token = $c->stash('auth');
291
292 return $token if $token;
293
294 # Get auth from session
295 $token = $c->session('auth') or return;
296 $c->stash(auth => $token);
297
298 # Return stashed value
299 return $token;
Akron864c2932018-11-16 17:18:55 +0100300 }
301 );
302
Akron864c2932018-11-16 17:18:55 +0100303 # Log in to the system
304 my $r = $app->routes;
Akron864c2932018-11-16 17:18:55 +0100305
Akrond91a1ca2022-05-20 16:45:01 +0200306 my $client_id = $param->{client_id};
307 my $client_secret = $param->{client_secret};
Akron8bbbecf2019-07-01 18:57:30 +0200308
Akrone3daaeb2023-05-08 09:44:18 +0200309 my $no_redirect_ua = Mojo::UserAgent->new(
310 connect_timeout => 30,
311 inactivity_timeout => 30,
312 max_redirects => 0
313 );
314
315 $no_redirect_ua->server->app($app);
316
Akroncdfd9d52019-07-23 11:35:00 +0200317
Akrond91a1ca2022-05-20 16:45:01 +0200318 # Sets a requested token and returns
319 # an error, if it didn't work
320 $app->helper(
321 'auth.set_tokens_p' => sub {
322 my ($c, $json) = @_;
323 my $promise = Mojo::Promise->new;
Akroncdfd9d52019-07-23 11:35:00 +0200324
Akrond91a1ca2022-05-20 16:45:01 +0200325 # No json object
326 unless ($json) {
327 return $promise->reject({
328 message => 'Response is no valid JSON object (remote)'
329 });
330 };
Akroncdfd9d52019-07-23 11:35:00 +0200331
Akrond91a1ca2022-05-20 16:45:01 +0200332 # There is an error here
333 # Dealing with errors here
334 if ($json->{error} && ref $json->{error} ne 'ARRAY') {
335 return $promise->reject(
336 {
337 message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
Akron0f1b93b2020-03-17 11:37:19 +0100338 }
339 );
340 }
Akroncdfd9d52019-07-23 11:35:00 +0200341
Akrond91a1ca2022-05-20 16:45:01 +0200342 # There is an array of errors
343 elsif (my $error = $json->{errors} // $json->{error}) {
344 if (ref $error eq 'ARRAY') {
345 my @errors = ();
346 foreach (@{$error}) {
347 if ($_->[1]) {
348 push @errors, { code => $_->[0], message => $_->[1]}
Akronbc94a9c2021-04-15 00:07:35 +0200349 };
Akrond91a1ca2022-05-20 16:45:01 +0200350 };
351 return $promise->reject(@errors);
Akroncdfd9d52019-07-23 11:35:00 +0200352 };
353
Akrond91a1ca2022-05-20 16:45:01 +0200354 return $promise->reject({message => $error});
355 };
Akroncdfd9d52019-07-23 11:35:00 +0200356
Akrond91a1ca2022-05-20 16:45:01 +0200357 # Everything is fine
358 my $access_token = $json->{access_token};
359 my $token_type = $json->{token_type};
360 my $refresh_token = $json->{refresh_token};
361 my $expires_in = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
362 my $auth = $token_type . ' ' . $access_token;
363 # my $scope = $json->{scope};
Akroncdfd9d52019-07-23 11:35:00 +0200364
Akrond91a1ca2022-05-20 16:45:01 +0200365 # Set session info
366 $c->session(auth => $auth);
367
368 # Expiration of the token minus tolerance
369 $c->session(auth_exp => time + $expires_in - 60);
370
371 # Set session info for refresh token
372 # This can be stored in the session, as it is useless
373 # unless the client secret is stolen
374 $c->session(auth_r => $refresh_token) if $refresh_token;
375
376 # Set stash info
377 $c->stash(auth => $auth);
378
379 return $promise->resolve;
380 }
381 );
382
383
384 # Refresh tokens and return a promise
385 $app->helper(
386 'auth.refresh_p' => sub {
387 my $c = shift;
388 my $refresh_token = shift;
389
390 # Get OAuth access token
391 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
392
393 $c->app->log->debug("Refresh at $r_url");
394
395 return $c->kalamar_ua->post_p($r_url, {} => form => {
396 grant_type => 'refresh_token',
397 client_id => $client_id,
398 client_secret => $client_secret,
399 refresh_token => $refresh_token
400 })->then(
401 sub {
402 my $tx = shift;
403 my $json = $tx->result->json;
404
405 # Response is fine
406 if ($tx->res->is_success) {
407
408 $c->app->log->info("Refresh was successful");
409
410 # Set the tokens and return a promise
411 return $c->auth->set_tokens_p($json);
412 };
413
414 # There is a client error - refresh fails
415 if ($tx->res->is_client_error && $json) {
416
Akroncdfd9d52019-07-23 11:35:00 +0200417 $c->stash(auth => undef);
Akrond91a1ca2022-05-20 16:45:01 +0200418 $c->stash(auth_exp => undef);
419 delete $c->session->{user};
420 delete $c->session->{auth};
421 delete $c->session->{auth_r};
422 delete $c->session->{auth_exp};
Akroncdfd9d52019-07-23 11:35:00 +0200423
Akrond91a1ca2022-05-20 16:45:01 +0200424 # Response is 400
425 return Mojo::Promise->reject(
426 $json->{error_description} // $c->loc('Auth_refreshFail')
427 );
428 };
Akroncdfd9d52019-07-23 11:35:00 +0200429
Akrond91a1ca2022-05-20 16:45:01 +0200430 if ($tx->res->is_server_error) {
431 return Mojo::Promise->reject(
432 '600'
433 )
434 };
Akroncdfd9d52019-07-23 11:35:00 +0200435
Akrond91a1ca2022-05-20 16:45:01 +0200436 $c->notify(error => $c->loc('Auth_responseError'));
437 return Mojo::Promise->reject;
438 }
439 )
440 }
441 );
Akroncdfd9d52019-07-23 11:35:00 +0200442
Helge278fbca2022-11-29 18:49:15 +0100443
Akrone3daaeb2023-05-08 09:44:18 +0200444 # Issue new token and return a promise
445 $app->helper(
446 'auth.new_token_p' => sub {
447 my $c = shift;
448 my %param = @_;
449
450 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
451
452 my $client_id = $param{'client_id'};
453
454 return $c->korap_request($no_redirect_ua, post => $r_url, { } => form => {
455 response_type => 'code',
456 client_id => $client_id,
457 redirect_uri => $param{'redirect_uri'},
458 state => $param{'state'},
459 scope => $param{'scope'},
460 })->then(
461 sub {
462 my $tx = shift;
463
464 unless (ref($tx)) {
465 return Mojo::Promise->reject('Something went wrong');
466 };
467
468 # Check for location header with code in redirects
469 my ($code, $scope, $loc, $name);
470
471 # Check for location header with code in current tx
472 # and in redirects.
473 # The loop should not be relevant though, as for now
474 # redirects are not allowed.
475 foreach ($tx, @{$tx->redirects}) {
476 $loc = $_->res->headers->header('Location');
477
478 next unless $loc;
479
480 my $url = Mojo::URL->new($loc);
481
482 if ($url->query->param('code')) {
483 my $q = $url->query;
484 $code = $q->param('code');
485 $scope = $q->param('scope');
486 $name = $q->param('name');
487 last;
488 } elsif (my $err = $url->query->param('error_description')) {
489 return Mojo::Promise->reject($err);
490 }
491 };
492
493 return Mojo::Promise->resolve(
494 $loc,
495 $client_id,
496 $param{'redirect_uri'},
497 $code,
498 $scope,
499 $name
500 ) if $loc;
501
502 # Failed redirect, but location set
503 if ($tx->res->headers->location) {
504 my $url = Mojo::URL->new($tx->res->headers->location);
505 if (my $err = $url->query->param('error_description')) {
506 return Mojo::Promise->reject($err);
507 };
508 }
509
510 $c->stash(redirect_uri => undef);
511
512 # Maybe json
513 my $json = $tx->res->json;
514 if ($json && $json->{error_description}) {
515 return Mojo::Promise->reject($json->{error_description});
516 };
517
518 # No location code
519 return Mojo::Promise->reject('no location response');
520 }
521 )
522 }
523 );
524
Akrond91a1ca2022-05-20 16:45:01 +0200525 # Get a list of registered clients
526 $app->helper(
527 'auth.client_list_p' => sub {
528 my $c = shift;
Akroncdfd9d52019-07-23 11:35:00 +0200529
Helge278fbca2022-11-29 18:49:15 +0100530
Akrond91a1ca2022-05-20 16:45:01 +0200531 # Get list of registered clients
532 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/list');
Akrona8efaa92022-04-09 14:45:43 +0200533
Akrond91a1ca2022-05-20 16:45:01 +0200534 # Get the list of all clients
535 return $c->korap_request(post => $r_url, {} => form => {
536 super_client_id => $client_id,
537 super_client_secret => $client_secret,
Helge05436702024-08-05 17:08:44 +0200538 filter_by => 'owned_only'
Akrond91a1ca2022-05-20 16:45:01 +0200539 })->then(
540 sub {
541 my $tx = shift;
542 my $json = $tx->result->json;
Akroncdfd9d52019-07-23 11:35:00 +0200543
Akrond91a1ca2022-05-20 16:45:01 +0200544 # Response is fine
545 if ($tx->res->is_success) {
546 return Mojo::Promise->resolve($json);
547 };
548
Helge690e94d2023-04-19 16:01:24 +0200549 $c->log->error($tx->res->to_string);
Akrond91a1ca2022-05-20 16:45:01 +0200550
551 # Failure
552 $c->notify(error => $c->loc('Auth_responseError'));
553 return Mojo::Promise->reject($json // 'No response');
554 }
555 );
556 }
557 );
558
Akrondb1f4672023-01-24 12:05:07 +0100559 # Get info for registered client
560 $app->helper(
561 'auth.client_info_p' => sub {
562 my $c = shift;
Akron9d826902023-01-25 10:20:52 +0100563 my $req_client_id = shift;
Akrondb1f4672023-01-24 12:05:07 +0100564
565 # Get list of registered clients
Akron9d826902023-01-25 10:20:52 +0100566 my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/')->path($req_client_id);
Akrondb1f4672023-01-24 12:05:07 +0100567
568 my $form = {
569 super_client_id => $client_id,
570 super_client_secret => $client_secret,
571 };
572
573 # Get the list of all clients
574 return $c->korap_request(POST => $r_url, {} => form => $form)->then(
575 sub {
576 my $tx = shift;
577 my $json = $tx->result->json // {};
578
579 # Response is fine
580 if ($tx->res->is_success) {
581 return Mojo::Promise->resolve($json);
582 };
583
Helge690e94d2023-04-19 16:01:24 +0200584 $c->log->error($tx->res->to_string);
Akrondb1f4672023-01-24 12:05:07 +0100585
586 # Failure
587 return Mojo::Promise->reject($json->{error_description} // 'Client unknown');
588 }
589 );
590 }
591 );
592
Akrond91a1ca2022-05-20 16:45:01 +0200593
594 # Get a list of registered clients
595 $app->helper(
596 'auth.token_list_p' => sub {
597 my $c = shift;
598 my $user_client_id = shift;
599
600 # Revoke the token
601 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token/list');
602
603 my $form = {
604 super_client_id => $client_id,
605 super_client_secret => $client_secret,
606 token_type => 'access_token',
607 };
608
609 if ($user_client_id) {
610 $form->{client_id} = $user_client_id;
611 };
612
613 # Get the list of all clients
614 return $c->korap_request(post => $r_url, {} => form => $form)->then(
615 sub {
616 my $tx = shift;
617 my $json = $tx->result->json;
618
619 # Response is fine
620 if ($tx->res->is_success) {
621 return Mojo::Promise->resolve($json);
622 };
623
Helge690e94d2023-04-19 16:01:24 +0200624 $c->log->error($tx->res->to_string);
Akrond91a1ca2022-05-20 16:45:01 +0200625
626 # Failure
627 $c->notify(error => $c->loc('Auth_responseError'));
628 return Mojo::Promise->reject($json // 'No response');
629 }
630 );
631 }
632 );
633
634
635 # Issue a korap request with "oauth"orization
636 # This will override the core request helper
637 $app->helper(
638 korap_request => sub {
639 my $c = shift;
Akrona8f87cc2023-02-23 12:21:30 +0100640
641 # Get plugin user agent
642 my $ua = $c->kalamar_ua;
643
644 # Override if UA is granted
645 if (ref $_[0] eq 'Mojo::UserAgent') {
646 $ua = shift;
647 };
648
Akrond91a1ca2022-05-20 16:45:01 +0200649 my $method = shift;
650 my $path = shift;
Akrona8f87cc2023-02-23 12:21:30 +0100651
Akrond91a1ca2022-05-20 16:45:01 +0200652 my @param = @_;
653
654 # TODO:
655 # Check if $tx is not leaked!
656
Akrond91a1ca2022-05-20 16:45:01 +0200657 my $url = Mojo::URL->new($path);
658 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
659
660 # Set X-Forwarded for
661 $tx->req->headers->header(
662 'X-Forwarded-For' => $c->client_ip
663 );
664
665 # Emit Hook to alter request
666 $c->app->plugins->emit_hook(
667 before_korap_request => ($c, $tx)
668 );
669
670 my $h = $tx->req->headers;
671
672 # If the request already has an Authorization
673 # header, respect it!
674 if ($h->authorization) {
675 return $ua->start_p($tx);
676 };
677
678 # Get auth token
679 if (my $auth_token = $c->auth->token) {
680
681 # The token is already expired!
682 my $exp = $c->session('auth_exp');
683 if (defined $exp && $exp < time) {
684
685 # Remove auth ...
686 $c->stash(auth => undef);
687
688 # And get refresh token from session
689 if (my $refresh_token = $c->session('auth_r')) {
690
691 $c->app->log->debug("Refresh is required");
692
693 # Refresh
694 return $c->auth->refresh_p($refresh_token)->then(
695 sub {
696 $c->app->log->debug("Search with refreshed tokens");
697
698 # Tokens were set - now send the request the first time!
699 $tx->req->headers->authorization($c->stash('auth'));
700 return $ua->start_p($tx);
701 }
702 );
Akroncdfd9d52019-07-23 11:35:00 +0200703 }
704
Akrond91a1ca2022-05-20 16:45:01 +0200705 # The token is expired and no refresh token is
706 # available - issue an unauthorized request!
Akroncdfd9d52019-07-23 11:35:00 +0200707 else {
708
Akrond91a1ca2022-05-20 16:45:01 +0200709 $c->stash(auth => undef);
710 $c->stash(auth_exp => undef);
711 delete $c->session->{user};
712 delete $c->session->{auth};
713 delete $c->session->{auth_r};
714 delete $c->session->{auth_exp};
715
716 # Warn on Error!
717 $c->notify(warn => $c->loc('Auth_tokenExpired'));
718 return $ua->start_p($tx);
719 };
Akroncdfd9d52019-07-23 11:35:00 +0200720 }
721
Akrond91a1ca2022-05-20 16:45:01 +0200722 # Auth token is fine
Akroncdfd9d52019-07-23 11:35:00 +0200723 else {
724
Akrond91a1ca2022-05-20 16:45:01 +0200725 # Set auth
726 $h->authorization($auth_token);
727 }
728 }
Akroncdfd9d52019-07-23 11:35:00 +0200729
Akrond91a1ca2022-05-20 16:45:01 +0200730 # No token set
731 else {
Akroncdfd9d52019-07-23 11:35:00 +0200732
Akrond91a1ca2022-05-20 16:45:01 +0200733 # Return unauthorized request
734 return $ua->start_p($tx);
735 };
Akroncdfd9d52019-07-23 11:35:00 +0200736
Akrond91a1ca2022-05-20 16:45:01 +0200737 # Issue an authorized request and automatically
738 # refresh the token on expiration!
739 return $ua->start_p($tx)->then(
740 sub {
741 my $tx = shift;
Akroncdfd9d52019-07-23 11:35:00 +0200742
Akrond91a1ca2022-05-20 16:45:01 +0200743 # Response is fine
Akrona8f87cc2023-02-23 12:21:30 +0100744 if ($tx->res->is_success || $tx->res->is_redirect) {
Akrond91a1ca2022-05-20 16:45:01 +0200745 return Mojo::Promise->resolve($tx);
746 }
Akroncdfd9d52019-07-23 11:35:00 +0200747
Akrond91a1ca2022-05-20 16:45:01 +0200748 # There is a client error - maybe refresh!
749 elsif ($tx->res->is_client_error) {
Akroncdfd9d52019-07-23 11:35:00 +0200750
Akrond91a1ca2022-05-20 16:45:01 +0200751 # Check the error
752 my $json = $tx->res->json('/errors/0/1');
753 if ($json && ($json =~ /expired|invalid/)) {
754 $c->stash(auth => undef);
755 $c->stash(auth_exp => undef);
756 delete $c->session->{user};
757 delete $c->session->{auth};
Akroncdfd9d52019-07-23 11:35:00 +0200758
Akrond91a1ca2022-05-20 16:45:01 +0200759 # And get refresh token from session
760 if (my $refresh_token = $c->session('auth_r')) {
Akroncdfd9d52019-07-23 11:35:00 +0200761
Akrond91a1ca2022-05-20 16:45:01 +0200762 # Refresh
763 return $c->auth->refresh_p($refresh_token)->then(
764 sub {
765 $c->app->log->debug("Search with refreshed tokens");
Akroncdfd9d52019-07-23 11:35:00 +0200766
Akrond91a1ca2022-05-20 16:45:01 +0200767 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
Akroncdfd9d52019-07-23 11:35:00 +0200768
Akrond91a1ca2022-05-20 16:45:01 +0200769 # Set X-Forwarded for
770 $tx->req->headers->header(
771 'X-Forwarded-For' => $c->client_ip
772 );
773
774 # Tokens were set - now send the request the first time!
775 $tx->req->headers->authorization($c->stash('auth'));
776 return $ua->start_p($tx);
777 }
778 )
Akroncdfd9d52019-07-23 11:35:00 +0200779 };
780
Akrond91a1ca2022-05-20 16:45:01 +0200781 # Reject the invalid token
782 $c->notify(error => $c->loc('Auth_tokenInvalid'));
783 return Mojo::Promise->reject;
Akroncdfd9d52019-07-23 11:35:00 +0200784 };
785
Akrond91a1ca2022-05-20 16:45:01 +0200786 return Mojo::Promise->resolve($tx);
Akron8bbbecf2019-07-01 18:57:30 +0200787 }
Akron8bbbecf2019-07-01 18:57:30 +0200788
Akrond91a1ca2022-05-20 16:45:01 +0200789 # There is a server error - just report
790 elsif ($tx->res->is_server_error) {
791 my $err = $tx->res->error;
792 if ($err) {
793 return Mojo::Promise->reject($err->{code} . ': ' . $err->{message});
794 }
795 else {
796 $c->notify(error => $c->loc('Auth_serverError'));
797 return Mojo::Promise->reject;
798 };
799 };
800
801 $c->notify(error => $c->loc('Auth_responseError'));
802 return Mojo::Promise->reject;
803 }
804 );
805 }
806 );
807
808 # Password flow for OAuth
809 $r->post('/user/login')->to(
810 cb => sub {
811 my $c = shift;
812
813 # Validate input
814 my $v = $c->validation;
815 $v->required('handle_or_email', 'trim');
816 $v->required('pwd', 'trim');
817 $v->csrf_protect;
818 $v->optional('fwd')->closed_redirect;
819
820 my $user = check_decode($v->param('handle_or_email'));
821 unless ($user) {
822 $c->notify(error => $c->loc('Auth_invalidChar'));
823 $c->param(handle_or_email => '');
824 return $c->relative_redirect_to('index');
825 };
826
827 my $fwd = $v->param('fwd');
828
829 # Set flash for redirect
830 $c->flash(handle_or_email => $user);
831
832 if ($v->has_error || index($user, ':') >= 0) {
833 if ($v->has_error('fwd')) {
834 $c->notify(error => $c->loc('Auth_openRedirectFail'));
835 }
836 elsif ($v->has_error('csrf_token')) {
837 $c->notify(error => $c->loc('Auth_csrfFail'));
838 }
839 else {
840 $c->notify(error => $c->loc('Auth_loginFail'));
841 };
842
843 return $c->relative_redirect_to($fwd // 'index');
844 }
845
846 my $pwd = $v->param('pwd');
847
848 $c->app->log->debug("Login from user $user");
849
850 # <specific>
851
852 # Get OAuth access token
853 my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
854
855 # Korap request for login
856 $c->korap_request('post', $url, {}, form => {
857 grant_type => 'password',
858 username => $user,
859 password => $pwd,
860 client_id => $client_id,
861 client_secret => $client_secret
862 })->then(
863 sub {
864 # Set the tokens and return a promise
865 return $c->auth->set_tokens_p(shift->result->json)
866 }
867 )->then(
868 sub {
869 # Set user info
870 $c->session(user => $user);
871 $c->stash(user => $user);
872
873 # Notify on success
874 $c->app->log->debug(qq!Login successful: "$user"!);
875 $c->notify(success => $c->loc('Auth_loginSuccess'));
876 }
877 )->catch(
878 sub {
879
880 # Notify the user on login failure
881 unless (@_) {
882 $c->notify(error => $c->loc('Auth_loginFail'));
883 }
884
885 # There are known errors
886 foreach (@_) {
887 if (ref $_ eq 'HASH') {
888 my $err = ($_->{code} ? $_->{code} . ': ' : '') .
889 $_->{message};
890 $c->notify(error => $err);
891 # Log failure
892 $c->app->log->debug($err);
893 }
894 else {
895 $c->notify(error => $_);
896 $c->app->log->debug($_);
897 };
898 };
899
900 $c->app->log->debug(qq!Login fail: "$user"!);
901 }
902 )->finally(
903 sub {
904 # Redirect to slash
905 return $c->relative_redirect_to($fwd // 'index');
906 }
907 )
908
909 # Start IOLoop
910 ->wait;
911
912 return 1;
913 }
914 )->name('login');
915
916
917 # Log out of the session
918 $r->get('/user/logout')->to(
919 cb => sub {
920 my $c = shift;
921
922 # TODO: csrf-protection!
923
924 my $refresh_token = $c->session('auth_r');
925
926 # Revoke the token
927 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke');
928
929 $c->kalamar_ua->post_p($url => {} => form => {
930 client_id => $client_id,
931 client_secret => $client_secret,
932 token => $refresh_token,
933 token_type => 'refresh_token'
934 })->then(
935 sub {
936 my $tx = shift;
937 my $json = $tx->result->json;
938
939 my $promise;
940
941 # Response is fine
942 if ($tx->res->is_success) {
943 $c->app->log->info("Revocation was successful");
944 $c->notify(success => $c->loc('Auth_logoutSuccess'));
945
946 $c->stash(auth => undef);
947 $c->stash(auth_exp => undef);
948 $c->flash(handle_or_email => delete $c->session->{user});
949 delete $c->session->{auth};
950 delete $c->session->{auth_r};
951 delete $c->session->{auth_exp};
952 return Mojo::Promise->resolve;
953 };
954
955 # Token may be invalid
956 $c->notify('error', $c->loc('Auth_logoutFail'));
957
958 # There is a client error - refresh fails
959 if ($tx->res->is_client_error && $json) {
960
961 return Mojo::Promise->reject(
962 $json->{error_description}
963 );
964 };
965
966 # Resource may not be found (404)
967 return Mojo::Promise->reject
968
969 }
970 )->catch(
971 sub {
972 my $err = shift;
973
974 # Server may be irresponsible
975 $c->notify('error', $c->loc('Auth_logoutFail'));
976 return Mojo::Promise->reject($err);
977 }
978 )->finally(
979 sub {
980 return $c->redirect_to('index');
981 }
982 )->wait;
983 }
984 )->name('logout');
985
986
Akrona4f1a972023-06-09 10:36:14 +0200987 # Add settings
988 $app->navi->add(settings => (
989 $app->loc('Auth_oauthSettings'), 'oauth'
990 ));
Helge278fbca2022-11-29 18:49:15 +0100991
Helge4a53a9b2023-07-18 18:40:49 +0200992 # Get configuration for marketplace settings
993 if ($param->{marketplace}) {
994 $app->navi->add(settings => (
995 $app->loc('Auth_marketplace'), 'marketplace'
996 ));
997 }
Helge278fbca2022-11-29 18:49:15 +0100998
999
Akrona4f1a972023-06-09 10:36:14 +02001000 # Helper: Returns lists of registered plugins (of all users), which are permitted
1001 $app->helper(
Helgedb720ea2023-03-20 09:39:36 +01001002 'auth.plugin_list_p' => sub {
Helge278fbca2022-11-29 18:49:15 +01001003 my $c = shift;
Helgedb720ea2023-03-20 09:39:36 +01001004 state $l_url = Mojo::URL->new($c->korap->api)->path('plugins');
1005 return $c->korap_request(post => $l_url, {} => form => {
Helge278fbca2022-11-29 18:49:15 +01001006 super_client_id => $client_id,
1007 super_client_secret => $client_secret,
1008 #list only permitted plugins
1009 permitted_only => 'true'
Akrona4f1a972023-06-09 10:36:14 +02001010 })->then(
1011 sub {
1012 my $tx = shift;
1013 my $json = $tx->result->json;
1014 # Response is fine
1015 if ($tx->res->is_success) {
Helge278fbca2022-11-29 18:49:15 +01001016 return Mojo::Promise->resolve($json);
1017 };
Akrona4f1a972023-06-09 10:36:14 +02001018 $c->log->error($tx->res->to_string);
Helge278fbca2022-11-29 18:49:15 +01001019 # Failure
1020 $c->notify(error => $c->loc('Auth_responseError'));
1021 return Mojo::Promise->reject($json // 'No response');
1022 }
1023 );
Akrona4f1a972023-06-09 10:36:14 +02001024 }
1025 );
Helge278fbca2022-11-29 18:49:15 +01001026
Akrona4f1a972023-06-09 10:36:14 +02001027
1028 #Helper: Returns list of all plugins, which are already installed
1029 $app->helper(
1030 'auth.plugin_listin_p' => sub {
1031 my $c = shift;
1032 state $i_url = Mojo::URL->new($c->korap->api)->path('plugins/installed');
1033 return $c->korap_request(post => $i_url, {} => form => {
1034 super_client_id => $client_id,
1035 super_client_secret => $client_secret,
1036 })->then(
1037 sub {
1038 my $tx = shift;
1039 my $json = $tx->result->json;
1040 # Response is fine
1041 if ($tx->res->is_success) {
1042 return Mojo::Promise->resolve($json);
Helgedb720ea2023-03-20 09:39:36 +01001043 };
Akrona4f1a972023-06-09 10:36:14 +02001044
Helgedb720ea2023-03-20 09:39:36 +01001045 $c->log->error($tx->res->to_string);
Akrona278b312023-05-08 11:19:13 +02001046
Helgedb720ea2023-03-20 09:39:36 +01001047 # Failure
1048 $c->notify(error => $c->loc('Auth_responseError'));
1049 return Mojo::Promise->reject($json // 'No response');
1050 }
1051 );
Akrona4f1a972023-06-09 10:36:14 +02001052 }
1053 );
Helgedb720ea2023-03-20 09:39:36 +01001054
Akrona4f1a972023-06-09 10:36:14 +02001055 # Route to marketplace (for installation and deinstallation of plugins)
1056 $r->get('/settings/marketplace')->to(
1057 cb => sub {
1058 my $c = shift;
1059 _set_no_cache($c->res->headers);
1060 unless ($c->auth->token) {
1061 return $c->render(
1062 template => 'exception',
1063 msg => $c->loc('Auth_authenticationFail'),
1064 status => 401
1065 );
1066 };
Helge278fbca2022-11-29 18:49:15 +01001067
Akrona4f1a972023-06-09 10:36:14 +02001068 $c->render_later;
Helgedb720ea2023-03-20 09:39:36 +01001069 my $promiselist = $c->auth->plugin_list_p;
1070 my $promiseinlist = $c->auth->plugin_listin_p;
Helged36478d2023-06-08 17:43:01 +02001071 my $fl = 0;
Helgedb720ea2023-03-20 09:39:36 +01001072 Mojo::Promise->all($promiselist, $promiseinlist)-> then(
Akrona4f1a972023-06-09 10:36:14 +02001073 sub {
Helgedb720ea2023-03-20 09:39:36 +01001074 my ($promiselist, $promiseinlist) = @_;
1075 my $plist = ref($promiselist->[0]) eq 'ARRAY' ? $promiselist->[0] : [];
1076 my $plinlist = ref($promiseinlist->[0]) eq 'ARRAY' ? $promiseinlist->[0] : [];
1077 my $clean_pllist = $plist;
1078 $c->stash('pluginsin_list', $plinlist);
1079 if($plinlist){
1080 foreach my $entry (@$plinlist){
1081 @$clean_pllist = grep{!($_->{client_id} eq $entry->{client_id})} @$clean_pllist ;
Helgedb720ea2023-03-20 09:39:36 +01001082 }
Helge278fbca2022-11-29 18:49:15 +01001083 }
Helged36478d2023-06-08 17:43:01 +02001084 $c->stash('plugin_list', $clean_pllist);
1085 }
1086 )
1087 ->catch(
1088 sub {
1089 $fl = 1;
1090 }
1091 )
1092 ->finally(
1093 sub {
1094 if($fl){
1095 return $c->render(template => 'auth/marketplace-fail')
1096 }
1097 return $c->render(template => 'auth/marketplace');
1098 }
1099 )->wait;
1100 }
1101 )->name('marketplace');
Helge278fbca2022-11-29 18:49:15 +01001102
1103
Helged36478d2023-06-08 17:43:01 +02001104 # Route to install plugin
1105 $r->post('/settings/marketplace/install')->to(
1106 cb => sub {
1107 my $c = shift;
1108 _set_no_cache($c->res->headers);
1109 my $v = $c->validation;
1110 $v->required('client-id');
1111
1112 if ($v->has_error) {
1113 return $c->render(
Helgedb720ea2023-03-20 09:39:36 +01001114 json => [],
1115 status => 400
Akrona4f1a972023-06-09 10:36:14 +02001116 );
1117 };
1118
1119 unless ($c->auth->token) {
1120 return $c->render(
Helgedb720ea2023-03-20 09:39:36 +01001121 content => 'Unauthorized',
1122 status => 401
Helgedb720ea2023-03-20 09:39:36 +01001123 );
Akrona4f1a972023-06-09 10:36:14 +02001124 };
Helgedb720ea2023-03-20 09:39:36 +01001125
Akrona4f1a972023-06-09 10:36:14 +02001126 my $mclient_id = $v->param('client-id');
1127 $c->render_later;
Helge278fbca2022-11-29 18:49:15 +01001128
Helged36478d2023-06-08 17:43:01 +02001129 state $p_url = Mojo::URL->new($c->korap->api)->path('plugins/install');
1130
1131 return $c->korap_request(post => $p_url, {} => form => {
1132 super_client_id => $client_id,
1133 super_client_secret => $client_secret,
1134 client_id => $mclient_id
1135 })->then(
1136 sub {
1137 my $tx = shift;
1138 my $json = $tx->result->json;
1139 # Response is fine
1140 if ($tx->res->is_success) {
1141 return Mojo::Promise->resolve($json);
1142 };
1143 #Log errors
1144 $c->log->error($tx->res->to_string);
1145 # Failure
1146 return Mojo::Promise->reject;
1147 }
1148 )
1149 ->catch(
1150 sub {
1151 $c->notify('error' => $c->loc('Auth_installFail'));
1152 }
1153 )
1154 ->finally(
1155 sub {
1156 return $c->redirect_to('marketplace');
1157 }
Akrona4f1a972023-06-09 10:36:14 +02001158 );
Helged36478d2023-06-08 17:43:01 +02001159 }
1160 )->name('install-plugin');
Akrona4f1a972023-06-09 10:36:14 +02001161
Helged36478d2023-06-08 17:43:01 +02001162 # Route to plugin deinstallation
1163 $r->post('/settings/marketplace/uninstall')->to(
1164 cb => sub {
1165 my $c = shift;
1166 _set_no_cache($c->res->headers);
1167 my $v = $c->validation;
1168 $v->required('client-id');
1169
1170 if ($v->has_error) {
1171 return $c->render(
1172 json => [],
1173 status => 400
1174 );
1175 };
Akrona4f1a972023-06-09 10:36:14 +02001176
Helged36478d2023-06-08 17:43:01 +02001177 unless ($c->auth->token) {
1178 return $c->render(
1179 content => 'Unauthorized',
1180 status => 401
1181 );
1182 };
1183
1184 my $uclient_id = $v->param('client-id');
1185
1186 $c->render_later;
1187 state $s_url = Mojo::URL->new($c->korap->api)->path('plugins/uninstall');
1188 return $c->korap_request(post => $s_url, {} => form => {
1189 super_client_id => $client_id,
1190 super_client_secret => $client_secret,
1191 client_id => $uclient_id
1192 })->then(
1193 sub {
1194 my $tx = shift;
1195 my $json = $tx->result->json;
1196 # Response is fine
1197 if ($tx->res->is_success) {
1198 return Mojo::Promise->resolve($json);
1199 };
1200 $c->log->error($tx->res->to_string);
1201 # Failure
1202 return Mojo::Promise->reject($json // 'No response');
1203 }
1204 )
1205 ->catch(
1206 sub {
1207 $c->notify('error' => $c->loc('Auth_uninstallFail'));
1208 }
1209 )
1210 ->finally(
1211 sub {
1212 return $c->redirect_to('marketplace');
1213 }
1214 );
1215 }
1216 )->name('uninstall-plugin');
1217
1218 # Route to OAuth settings
1219 $r->get('/settings/oauth')->to(
1220 cb => sub {
1221 my $c = shift;
1222 _set_no_cache($c->res->headers);
1223 unless ($c->auth->token) {
1224 return $c->render(
1225 template => 'exception',
1226 msg => $c->loc('Auth_authenticationFail'),
1227 status => 401
1228 );
1229 };
1230 # Wait for async result
1231 $c->render_later;
1232 $c->auth->client_list_p->then(
1233 sub {
1234 $c->stash('client_list' => shift);
1235 }
1236 )->catch(
1237 sub {
1238 return;
1239 }
1240 )->finally(
1241 sub {
1242 return $c->render(template => 'auth/clients')
1243 }
1244 );
1245 }
1246 )->name('oauth-settings');
1247
Akrona4f1a972023-06-09 10:36:14 +02001248
1249 # Route to oauth client registration
1250 $r->post('/settings/oauth/register')->to(
1251 cb => sub {
1252 my $c = shift;
1253
1254 _set_no_cache($c->res->headers);
1255
1256 my $v = $c->validation;
1257
1258 unless ($c->auth->token) {
1259 return $c->render(
1260 content => 'Unauthorized',
1261 status => 401
1262 );
1263 };
1264
1265 $v->csrf_protect;
1266 $v->required('name', 'trim', 'not_empty')->size(3, 255);
1267 $v->required('type')->in('PUBLIC', 'CONFIDENTIAL');
1268 $v->required('desc', 'trim', 'not_empty')->size(3, 255);
Akrona4f1a972023-06-09 10:36:14 +02001269 $v->optional('redirect_uri', 'trim', 'not_empty')->like(qr/^(http|$)/i);
1270 $v->optional('src', 'not_empty');
Helge9f9d4852024-12-09 16:06:34 +01001271
1272 my $src = $v->param('src');
1273 if ($src && ref $src && $src->size > 0){
1274 $v->required('url', 'trim', 'not_empty')->like(qr/^(http|$)/i);
1275 }
1276 else{
1277 $v->optional('url', 'trim', 'not_empty')->like(qr/^(http|$)/i);
1278 }
Akrona4f1a972023-06-09 10:36:14 +02001279
1280 $c->stash(template => 'auth/clients');
1281
1282 # Render with error
1283 if ($v->has_error) {
1284 if ($v->has_error('csrf_token')) {
1285 $c->notify(error => $c->loc('Auth_csrfFail'));
1286 }
1287 else {
1288 $c->notify(error => $c->loc('Auth_paramError'));
1289 };
1290 return $c->render;
1291 } elsif ($c->req->is_limit_exceeded) {
1292 $c->notify(error => $c->loc('Auth_fileSizeExceeded'));
1293 return $c->render;
1294 };
1295
1296 my $type = $v->param('type');
Akrona4f1a972023-06-09 10:36:14 +02001297 my $src_json;
Helge9f9d4852024-12-09 16:06:34 +01001298
1299
Akrona4f1a972023-06-09 10:36:14 +02001300
1301 my $json_obj = {
1302 name => $v->param('name'),
1303 type => $type,
1304 description => $v->param('desc'),
1305 url => $v->param('url'),
1306 redirect_uri => $v->param('redirect_uri')
1307 };
1308
1309 # Check plugin source
1310 if ($src) {
1311
1312 # Source need to be a file upload
1313 if (!ref $src || !$src->isa('Mojo::Upload')) {
1314 $c->notify(error => $c->loc('Auth_jsonRequired'));
Akrond91a1ca2022-05-20 16:45:01 +02001315 return $c->render;
Akrona4f1a972023-06-09 10:36:14 +02001316 };
1317
1318 # Uploads can't be too large
1319 if ($src->size > 1_000_000) {
Akrond91a1ca2022-05-20 16:45:01 +02001320 $c->notify(error => $c->loc('Auth_fileSizeExceeded'));
1321 return $c->render;
1322 };
Akron864c2932018-11-16 17:18:55 +01001323
Akrona4f1a972023-06-09 10:36:14 +02001324 # Check upload is not empty
1325 if ($src->size > 0 && $src->filename ne '') {
Helge9f9d4852024-12-09 16:06:34 +01001326
Akrona4f1a972023-06-09 10:36:14 +02001327 # Plugins need to be confidential
1328 if ($type ne 'CONFIDENTIAL') {
1329 $c->notify(error => $c->loc('Auth_confidentialRequired'));
1330 return $c->render;
1331 };
Akron864c2932018-11-16 17:18:55 +01001332
Akrona4f1a972023-06-09 10:36:14 +02001333 my $asset = $src->asset;
Akron33f5c672019-06-24 19:40:47 +02001334
Akrona4f1a972023-06-09 10:36:14 +02001335 # Check for json
1336 eval {
1337 $src_json = decode_json($asset->slurp);
1338 };
1339
1340 if ($@ || !ref $src_json || ref $src_json ne 'HASH') {
Akrond91a1ca2022-05-20 16:45:01 +02001341 $c->notify(error => $c->loc('Auth_jsonRequired'));
1342 return $c->render;
1343 };
1344
Akrona4f1a972023-06-09 10:36:14 +02001345 $json_obj->{source} = $src_json;
Akron83209f72021-01-29 17:54:15 +01001346 };
Akrona4f1a972023-06-09 10:36:14 +02001347 };
Akron83209f72021-01-29 17:54:15 +01001348
Akrona4f1a972023-06-09 10:36:14 +02001349 # Wait for async result
1350 $c->render_later;
Akron83209f72021-01-29 17:54:15 +01001351
Akrona4f1a972023-06-09 10:36:14 +02001352 # Register on server
1353 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/client/register');
1354 $c->korap_request('POST', $url => {} => json => $json_obj)->then(
1355 sub {
1356 my $tx = shift;
1357 my $result = $tx->result;
Akron83209f72021-01-29 17:54:15 +01001358
Akrona4f1a972023-06-09 10:36:14 +02001359 if ($result->is_error) {
Akrond91a1ca2022-05-20 16:45:01 +02001360 my $json = $result->json;
Akrona4f1a972023-06-09 10:36:14 +02001361 if ($json && $json->{error}) {
1362 $c->notify(
1363 error => $json->{error} .
1364 ($json->{error_description} ? ': ' . $json->{error_description} : '')
1365 )
Akron83209f72021-01-29 17:54:15 +01001366 };
Akron83209f72021-01-29 17:54:15 +01001367
Akrona4f1a972023-06-09 10:36:14 +02001368 return Mojo::Promise->reject;
1369 };
Akron83209f72021-01-29 17:54:15 +01001370
Akrona4f1a972023-06-09 10:36:14 +02001371 my $json = $result->json;
1372
1373 my $client_id = $json->{client_id};
1374 my $client_secret = $json->{client_secret};
1375
1376 $c->stash('client_name' => $v->param('name'));
1377 $c->stash('client_desc' => $v->param('desc'));
1378 $c->stash('client_type' => $v->param('type'));
1379 $c->stash('client_url' => $v->param('url'));
1380 $c->stash('client_src' => $v->param('source'));
1381 $c->stash('client_redirect_uri' => $v->param('redirect_uri'));
1382 $c->stash('client_id' => $client_id);
1383
1384 if ($client_secret) {
1385 $c->stash('client_secret' => $client_secret);
1386 };
1387
1388 $c->notify(success => $c->loc('Auth_en_registerSuccess'));
1389
1390 return $c->render(template => 'auth/client');
1391 }
1392 )->catch(
1393 sub {
1394 $c->notify('error' => $c->loc('Auth_en_registerFail'));
1395 }
1396 )->finally(
1397 sub {
1398 return $c->redirect_to('settings' => { scope => 'oauth' });
1399 }
1400 );
1401 }
1402 )->name('oauth-register');
1403
1404
1405 # Unregister client page
1406 $r->get('/settings/oauth/:client_id/unregister')->to(
1407 cb => sub {
1408 my $c = shift;
1409 _set_no_cache($c->res->headers);
1410 $c->render(template => 'auth/unregister');
1411 }
1412 )->name('oauth-unregister');
1413
1414
1415 # Unregister client
1416 $r->post('/settings/oauth/:client_id/unregister')->to(
1417 cb => sub {
1418 my $c = shift;
1419 _set_no_cache($c->res->headers);
1420
1421 my $v = $c->validation;
1422
1423 unless ($c->auth->token) {
1424 return $c->render(
1425 content => 'Unauthorized',
1426 status => 401
Akrond91a1ca2022-05-20 16:45:01 +02001427 );
Akrona4f1a972023-06-09 10:36:14 +02001428 };
Akronc1aaf932021-06-09 12:19:15 +02001429
Akrona4f1a972023-06-09 10:36:14 +02001430 $v->csrf_protect;
1431 $v->required('client-name', 'trim')->size(3, 255);
Akronc1aaf932021-06-09 12:19:15 +02001432
Akrona4f1a972023-06-09 10:36:14 +02001433 # Render with error
1434 if ($v->has_error) {
1435 if ($v->has_error('csrf_token')) {
1436 $c->notify(error => $c->loc('Auth_csrfFail'));
1437 }
1438 else {
1439 $c->notify(error => $c->loc('Auth_paramError'));
Akronc1aaf932021-06-09 12:19:15 +02001440 };
Akrona4f1a972023-06-09 10:36:14 +02001441 return $c->redirect_to('oauth-settings');
1442 };
Akronc1aaf932021-06-09 12:19:15 +02001443
Akrona4f1a972023-06-09 10:36:14 +02001444 my $client_id = $c->stash('client_id');
1445 my $client_name = $v->param('client-name');
1446 my $client_secret = $v->param('client-secret');
Akronc1aaf932021-06-09 12:19:15 +02001447
Akrona4f1a972023-06-09 10:36:14 +02001448 # Get list of registered clients
1449 my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/deregister/')->path(
1450 $client_id
1451 );
1452
1453 my $send = {};
1454
1455 if ($client_secret) {
1456 $send->{client_secret} = $client_secret;
1457 };
1458
1459 # Get the list of all clients
1460 return $c->korap_request(delete => $r_url, {} => form => $send)->then(
1461 sub {
1462 my $tx = shift;
1463
1464 # Response is fine
1465 if ($tx->res->is_success) {
1466 # Okay
1467 $c->notify(success => 'Successfully deleted ' . $client_name);
Akronc1aaf932021-06-09 12:19:15 +02001468 }
1469 else {
Akrona4f1a972023-06-09 10:36:14 +02001470
1471 # Failure
1472 my $json = $tx->result->json;
1473 if ($json && $json->{error_description}) {
1474 $c->notify(error => $json->{error_description});
1475 } else {
1476 $c->notify(error => $c->loc('Auth_responseError'));
1477 };
Akronc1aaf932021-06-09 12:19:15 +02001478 };
Akrona4f1a972023-06-09 10:36:14 +02001479
Akrond91a1ca2022-05-20 16:45:01 +02001480 return $c->redirect_to('oauth-settings');
Akrona4f1a972023-06-09 10:36:14 +02001481 }
1482 );
1483 }
1484 )->name('oauth-unregister-post');
1485
1486
1487 # OAuth Client authorization
1488 $r->get('/settings/oauth/authorize')->to(
1489 cb => sub {
1490 my $c = shift;
1491
1492 _set_no_cache($c->res->headers);
1493
1494 my $v = $c->validation;
1495 $v->required('client_id', 'trim');
1496 $v->required('scope', 'trim');
1497 $v->optional('state', 'trim');
1498 $v->optional('redirect_uri', 'trim', 'not_empty')->like(qr/^(http|$)/i);
1499
1500 # Redirect with error
1501 if ($v->has_error) {
1502
1503 if ($v->has_error('client_id')) {
1504 $c->notify(error => $c->loc('Auth_clientIDFail'));
1505 }
1506 elsif ($v->has_error('scope')) {
1507 $c->notify(error => $c->loc('Auth_scopeFail'));
1508 }
1509 else {
1510 $c->notify(error => $c->loc('Auth_paramError'));
Akronc1aaf932021-06-09 12:19:15 +02001511 };
1512
Akrona4f1a972023-06-09 10:36:14 +02001513 # If logged in, go to oauth settings - otherwise to index
1514 return $c->redirect_to($c->auth->token ? 'oauth-settings' : 'index');
1515 };
Akronc1aaf932021-06-09 12:19:15 +02001516
Akrona4f1a972023-06-09 10:36:14 +02001517 foreach (qw!scope client_id state redirect_uri!) {
1518 $c->stash($_, $v->param($_));
1519 };
Akronc1aaf932021-06-09 12:19:15 +02001520
Akrona4f1a972023-06-09 10:36:14 +02001521 # Wait for async result
1522 $c->render_later;
Akrond91a1ca2022-05-20 16:45:01 +02001523
Akrona4f1a972023-06-09 10:36:14 +02001524 my $client_id = $v->param('client_id');
Akrond91a1ca2022-05-20 16:45:01 +02001525
Akrona4f1a972023-06-09 10:36:14 +02001526 my $client_information = $c->auth->client_info_p($client_id)->then(
1527 sub {
1528 my $cl = shift;
1529 $c->stash(client_name => $cl->{'client_name'});
1530 $c->stash(client_type => $cl->{'client_type'});
1531 $c->stash(client_desc => $cl->{'client_description'});
1532 $c->stash(client_url => $cl->{'client_url'});
1533 $c->stash(redirect_uri_server => $cl->{'client_redirect_uri'});
1534 }
1535 )->then(
1536 sub {
Akronc1aaf932021-06-09 12:19:15 +02001537
Akrona4f1a972023-06-09 10:36:14 +02001538 # Check, if the client redirect_uri is valid
1539 my $redirect_uri_server = $c->stash('redirect_uri_server');
1540 my $redirect_uri = $v->param('redirect_uri');
1541 my ($server_o, $client_o);
1542
1543 # Both exist
1544 if ($redirect_uri_server && $redirect_uri) {
1545 $server_o = Mojo::URL->new($redirect_uri_server);
1546 $client_o = Mojo::URL->new($redirect_uri);
1547
1548 # Host not valid - take registered URI
1549 if ($server_o->host ne $client_o->host) {
1550 $c->notify(warn => 'redirect_uri host differs from registered host');
1551 $client_o = $server_o;
Akronc1aaf932021-06-09 12:19:15 +02001552 }
Akrond91a1ca2022-05-20 16:45:01 +02001553
Akrona4f1a972023-06-09 10:36:14 +02001554 # Port not valid - take registered URI
1555 elsif (($server_o->port // '') ne ($client_o->port // '')) {
1556 $c->notify(warn => 'redirect_uri port differs from registered port');
1557 $client_o = $server_o;
Akronc1aaf932021-06-09 12:19:15 +02001558 };
1559 }
Akroncdfd9d52019-07-23 11:35:00 +02001560
Akrona4f1a972023-06-09 10:36:14 +02001561 # Client sent exists
1562 elsif ($redirect_uri) {
1563 $client_o = Mojo::URL->new($redirect_uri);
1564 $c->stash(redirect_warning => $c->loc('oauthGrantRedirectWarn'))
Akron2c142ab2023-01-30 13:21:57 +01001565 }
Akrona4f1a972023-06-09 10:36:14 +02001566
1567 # Server registered exists
1568 elsif ($redirect_uri_server) {
1569 $client_o = Mojo::URL->new($redirect_uri_server);
Akron2c142ab2023-01-30 13:21:57 +01001570 }
Akrona4f1a972023-06-09 10:36:14 +02001571
1572 # Redirect unknown
Akron2c142ab2023-01-30 13:21:57 +01001573 else {
Akrona4f1a972023-06-09 10:36:14 +02001574 $c->notify(error => 'redirect_uri not set');
Akron001dcd22023-02-07 08:38:11 +01001575
1576 # If logged in, go to oauth settings - otherwise to index
1577 return $c->redirect_to($c->auth->token ? 'oauth-settings' : 'index');
Akron33f5c672019-06-24 19:40:47 +02001578 };
1579
Akrona4f1a972023-06-09 10:36:14 +02001580 # No userinfo allowed
1581 if ($client_o->userinfo) {
1582 $c->notify(warn => 'redirect_uri contains userinfo');
1583 $client_o->userinfo('');
1584 };
1585
1586 # HTTPS warning
1587 # if ($client_o->scheme ne 'https') {
1588 # $c->notify(warn => 'redirect_uri is not HTTPS');
1589 # };
1590
1591 # Sign redirection URL
1592 $c->stash(redirect_uri => $client_o->to_string);
1593 $c->stash(close_redirect_uri => '' . $c->close_redirect_to($client_o));
1594
1595 # Get auth token
1596 my $auth_token = $c->auth->token;
1597
1598 # User is not logged in - log in before!
1599 unless ($auth_token) {
1600 return $c->render(template => 'auth/login');
1601 };
1602
1603 # Grant authorization
1604 return $c->render(template => 'auth/grant_scope');
1605 }
1606 )->catch(
1607 sub {
1608 my $error = shift;
1609 $c->notify(error => $error);
1610
1611 # If logged in, go to oauth settings - otherwise to index
1612 return $c->redirect_to($c->auth->token ? 'oauth-settings' : 'index');
1613 }
1614 );
1615 }
1616 )->name('oauth-grant-scope');
1617
1618
1619 # OAuth Client authorization
1620 # This will return a location information including some info
1621 $r->post('/settings/oauth/authorize')->to(
1622 cb => sub {
1623 my $c = shift;
1624
1625 _set_no_cache($c->res->headers);
1626
1627 # It's necessary that it's clear this was triggered by
1628 # KorAP and not by the client!
1629 my $v = $c->validation;
1630 $v->csrf_protect;
1631 $v->required('client_id', 'trim');
1632 $v->required('scope', 'trim');
1633 $v->optional('state', 'trim');
1634
1635 # Only signed redirects are allowed
1636 $v->required('redirect_uri', 'trim', 'not_empty')->closed_redirect;
1637
1638 # Render with error
1639 if ($v->has_error) {
1640
1641 # If logged in, go to oauth settings - otherwise to index
1642 my $url = $c->url_for($c->auth->token ? 'oauth-settings' : 'index');
1643
1644 if ($v->has_error('client_id')) {
1645 $url->query([error_description => $c->loc('Auth_clientIDFail')]);
1646 }
1647 elsif ($v->has_error('scope')) {
1648 $url->query([error_description => $c->loc('Auth_scopeFail')]);
1649 }
1650 elsif ($v->has_error('csrf_token')) {
1651 $url->query([error_description => $c->loc('Auth_csrfFail')]);
1652 }
1653 else {
1654 $url->query([error_description => $c->loc('Auth_paramError')]);
Akrond91a1ca2022-05-20 16:45:01 +02001655 };
Akron33f5c672019-06-24 19:40:47 +02001656
Akrona4f1a972023-06-09 10:36:14 +02001657 return $c->redirect_to($url);
1658 };
Akron33f5c672019-06-24 19:40:47 +02001659
Akrona4f1a972023-06-09 10:36:14 +02001660 $c->stash(redirect_uri => Mojo::URL->new($v->param('redirect_uri')));
Akron9ccf69a2023-01-31 14:21:37 +01001661
Akrona4f1a972023-06-09 10:36:14 +02001662 return $c->auth->new_token_p(
1663 client_id => $v->param('client_id'),
1664 redirect_uri => $c->stash('redirect_uri'),
1665 state => $v->param('state'),
1666 scope => $v->param('scope'),
1667 )->catch(
1668 sub {
1669 my $err_msg = shift;
1670 my $url = $c->stash('redirect_uri');
Akron9ccf69a2023-01-31 14:21:37 +01001671
Akrona4f1a972023-06-09 10:36:14 +02001672 # Redirect!
1673 if ($url) {
1674 if ($err_msg) {
1675 $url = $url->query([error_description => $err_msg]);
Akrond91a1ca2022-05-20 16:45:01 +02001676 };
Akron33f5c672019-06-24 19:40:47 +02001677 }
Akrona4f1a972023-06-09 10:36:14 +02001678
1679 # Do not redirect!
1680 else {
1681 $c->notify(error => $err_msg);
1682
1683 # If logged in, go to oauth settings - otherwise to index
1684 $url = $c->url_for($c->auth->token ? 'oauth-settings' : 'index');
1685 };
1686
1687 return Mojo::Promise->resolve($url);
1688 }
1689 )->then(
1690 sub {
1691 my $loc = shift;
1692 return $c->redirect_to($loc);
1693 }
1694 )->wait;
1695 return $c->rendered;
1696 }
1697 )->name('oauth-grant-scope-post');
Akron4cefe1f2019-09-04 10:11:28 +02001698
1699
Akrona4f1a972023-06-09 10:36:14 +02001700 # Show information of a client
1701 $r->get('/settings/oauth/:client_id')->to(
1702 cb => sub {
1703 my $c = shift;
Akron4cefe1f2019-09-04 10:11:28 +02001704
Akrona4f1a972023-06-09 10:36:14 +02001705 _set_no_cache($c->res->headers);
Akron4cefe1f2019-09-04 10:11:28 +02001706
Akrona4f1a972023-06-09 10:36:14 +02001707 $c->render_later;
Akron4cefe1f2019-09-04 10:11:28 +02001708
Akrona4f1a972023-06-09 10:36:14 +02001709 $c->auth->client_list_p->then(
1710 sub {
1711 my $json = shift;
Akrond91a1ca2022-05-20 16:45:01 +02001712
Akrona4f1a972023-06-09 10:36:14 +02001713 my ($item) = grep {
1714 $c->stash('client_id') eq $_->{client_id}
1715 } @$json;
Akrond91a1ca2022-05-20 16:45:01 +02001716
Akrona4f1a972023-06-09 10:36:14 +02001717 unless ($item) {
1718 return Mojo::Promise->reject;
1719 };
Akrond91a1ca2022-05-20 16:45:01 +02001720
Akrona4f1a972023-06-09 10:36:14 +02001721 $c->stash(client_name => $item->{client_name});
1722 $c->stash(client_desc => $item->{client_description});
1723 $c->stash(client_url => $item->{client_url});
1724 $c->stash(client_type => ($item->{client_type} // 'PUBLIC'));
1725 $c->stash(client_redirect_uri => $item->{client_redirect_uri});
1726 $c->stash(client_src => encode_json($item->{source})) if $item->{source};
Akrond91a1ca2022-05-20 16:45:01 +02001727
Akrona4f1a972023-06-09 10:36:14 +02001728 $c->auth->token_list_p($c->stash('client_id'));
1729 }
1730 )->then(
1731 sub {
1732 my $json = shift;
Akron4cefe1f2019-09-04 10:11:28 +02001733
Akrona4f1a972023-06-09 10:36:14 +02001734 $c->stash(tokens => $json);
Akron4cefe1f2019-09-04 10:11:28 +02001735
Akrona4f1a972023-06-09 10:36:14 +02001736 return Mojo::Promise->resolve;
1737 }
1738 )->catch(
1739 sub {
1740 return $c->reply->not_found;
1741 }
1742 )->finally(
1743 sub {
1744 return $c->render(template => 'auth/client')
1745 }
1746 );
Akron4cefe1f2019-09-04 10:11:28 +02001747
Akrona4f1a972023-06-09 10:36:14 +02001748 return;
1749 }
1750 )->name('oauth-tokens');
Akron59992122019-10-29 11:28:45 +01001751
Akrond91a1ca2022-05-20 16:45:01 +02001752
1753 # Ask if new token should be issued
1754 $r->get('/settings/oauth/:client_id/token')->to(
1755 cb => sub {
1756 my $c = shift;
1757 _set_no_cache($c->res->headers);
1758 $c->render(template => 'auth/issue-token');
1759 }
1760 )->name('oauth-issue-token');
1761
1762
1763 # Ask if a token should be revoked
1764 $r->post('/settings/oauth/:client_id/token/revoke')->to(
1765 cb => sub {
1766 shift->render(template => 'auth/revoke-token');
1767 }
1768 )->name('oauth-revoke-token');
1769
1770
1771 # Issue new token
1772 $r->post('/settings/oauth/:client_id/token')->to(
1773 cb => sub {
1774 my $c = shift;
1775 _set_no_cache($c->res->headers);
1776
1777 my $v = $c->validation;
1778
1779 unless ($c->auth->token) {
1780 return $c->render(
1781 content => 'Unauthorized',
1782 status => 401
1783 );
1784 };
1785
1786 $v->csrf_protect;
1787 $v->optional('client-secret');
1788 $v->required('name', 'trim');
1789
1790 # Render with error
1791 if ($v->has_error) {
1792 if ($v->has_error('csrf_token')) {
1793 $c->notify(error => $c->loc('Auth_csrfFail'));
1794 }
1795 else {
1796 $c->notify(error => $c->loc('Auth_paramError'));
1797 };
1798 return $c->redirect_to('oauth-settings')
1799 };
1800
1801 # Get authorization token
Akrond91a1ca2022-05-20 16:45:01 +02001802 my $client_id = $c->stash('client_id');
1803 my $name = $v->param('name');
Akron409f6b82023-05-08 10:42:36 +02001804 my $redirect_url = $c->url_for->query({name => $name})->to_abs;
Akrond91a1ca2022-05-20 16:45:01 +02001805
Akrone3daaeb2023-05-08 09:44:18 +02001806 $c->auth->new_token_p(
Akrond91a1ca2022-05-20 16:45:01 +02001807 client_id => $client_id,
1808 redirect_uri => $redirect_url,
Akrona278b312023-05-08 11:19:13 +02001809 # TODO: State
1810 scope => 'search match_info',
Akrond91a1ca2022-05-20 16:45:01 +02001811 )->then(
1812 sub {
Akrone3daaeb2023-05-08 09:44:18 +02001813 my ($loc, $client_id, $redirect_url, $code, $scope, $name) = @_;
Akrond91a1ca2022-05-20 16:45:01 +02001814
1815 # Get OAuth access token
1816 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
1817 return $c->kalamar_ua->post_p($r_url, {} => form => {
1818 client_id => $client_id,
1819 # NO CLIENT_SECRET YET SUPPORTED
1820 grant_type => 'authorization_code',
1821 code => $code,
1822 redirect_uri => $redirect_url
1823 })->then(
1824 sub {
1825 my $tx = shift;
1826 my $json = $tx->res->json;
1827
1828 if ($tx->res->is_error) {
1829 $c->notify(error => 'Unable to fetch new token');
1830 return Mojo::Promise->reject;
1831 };
1832
1833 $c->notify(success => 'New access token created');
1834
1835 $c->redirect_to('oauth-tokens' => { client_id => $client_id })
1836 }
1837 )->catch(
1838 sub {
1839 my $err_msg = shift;
1840
1841 # Only raised in case of connection errors
1842 if ($err_msg) {
1843 $c->notify(error => { src => 'Backend' } => $err_msg)
1844 };
1845
1846 $c->render(
1847 status => 400,
1848 template => 'failure'
1849 );
1850 }
1851 )
1852
1853 # Start IOLoop
1854 ->wait;
1855
1856 }
1857 )->catch(
1858 sub {
1859 my $err_msg = shift;
1860
1861 # Only raised in case of connection errors
1862 if ($err_msg) {
1863 $c->notify(error => { src => 'Backend' } => $err_msg)
1864 };
1865
1866 return $c->render(
1867 status => 400,
1868 template => 'failure'
1869 );
1870 }
1871 )
1872
1873 # Start IOLoop
1874 ->wait;
1875
1876 return 1;
1877 }
1878 )->name('oauth-issue-token-post');
1879
1880
1881 # Revoke token
1882 $r->delete('/settings/oauth/:client_id/token')->to(
1883 cb => sub {
1884 my $c = shift;
1885
1886 my $v = $c->validation;
1887
1888 unless ($c->auth->token) {
1889 return $c->render(
1890 content => 'Unauthorized',
1891 status => 401
1892 );
1893 };
1894
1895 $v->csrf_protect;
1896 $v->required('token', 'trim');
1897 $v->optional('name', 'trim');
1898 my $private_client_id = $c->stash('client_id');
1899
1900 # Render with error
1901 if ($v->has_error) {
1902 if ($v->has_error('csrf_token')) {
1903 $c->notify(error => $c->loc('Auth_csrfFail'));
1904 }
1905 else {
1906 $c->notify(error => $c->loc('Auth_paramError'));
1907 };
1908 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1909 };
1910
1911 # Revoke token using super client privileges
1912 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke/super');
1913
1914 my $token = $v->param('token');
1915
1916 return $c->korap_request(post => $r_url, {} => form => {
1917 super_client_id => $client_id,
1918 super_client_secret => $client_secret,
1919 token => $token
1920 })->then(
1921 sub {
1922 my $tx = shift;
1923
1924 # Response is fine
1925 if ($tx->res->is_success) {
1926 $c->notify(success => $c->loc('Auth_revokeSuccess'));
1927 return Mojo::Promise->resolve;
1928 };
1929
1930 return Mojo::Promise->reject;
1931 }
1932 )->catch(
1933 sub {
1934 my $err_msg = shift;
1935 if ($err_msg) {
1936 $c->notify(error => { src => 'Backend' } => $err_msg );
1937 }
1938 else {
1939 $c->notify(error => $c->loc('Auth_revokeFail'));
1940 };
1941 }
1942 )->finally(
1943 sub {
1944 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1945 }
1946 )
1947
1948 # Start IOLoop
1949 ->wait;
1950 }
1951 )->name('oauth-revoke-token-delete');
1952
Akron59992122019-10-29 11:28:45 +01001953 $app->log->info('Successfully registered Auth plugin');
Akron864c2932018-11-16 17:18:55 +01001954};
1955
Akronb3f33592020-03-16 15:14:44 +01001956
Akronad011bb2021-06-10 12:16:36 +02001957# Set 'no caching' headers
1958sub _set_no_cache {
1959 my $h = shift;
1960 $h->cache_control('max-age=0, no-cache, no-store, must-revalidate');
1961 $h->expires('Thu, 01 Jan 1970 00:00:00 GMT');
1962 $h->header('Pragma','no-cache');
1963};
1964
1965
Akron6a228db2021-10-14 15:57:00 +02001966sub check_decode {
1967 no warnings 'uninitialized';
1968 my $str = shift;
1969 my $str2 = is_utf8($str) ? b($str)->decode : $str;
1970 if (defined($str2) && $str2 && length($str2) > 1) {
1971 return $str2
1972 };
1973 return;
1974};
1975
1976
Akron864c2932018-11-16 17:18:55 +010019771;
Akrona9c8b0e2018-11-16 20:20:28 +01001978
Akronc82b1bc2018-11-18 18:06:14 +01001979
Akrona9c8b0e2018-11-16 20:20:28 +01001980__END__
Akron59992122019-10-29 11:28:45 +01001981
1982=pod
1983
1984=encoding utf8
1985
1986=head1 NAME
1987
1988Kalamar::Plugin::Auth - OAuth-2.0-based authorization plugin
1989
1990=head1 DESCRIPTION
1991
1992L<Kalamar::Plugin::Auth> is an OAuth-2.0-based authorization
1993plugin for L<Kalamar>. It requires a C<Kustvakt> full server
1994with OAuth 2.0 capabilities.
1995It is activated by loading C<Auth> as a plugin in the C<Kalamar.plugins>
1996parameter in the Kalamar configuration.
1997
1998=head1 CONFIGURATION
1999
2000L<Kalamar::Plugin::Auth> supports the following parameter for the
2001C<Kalamar-Auth> configuration section in the Kalamar configuration:
2002
2003=over 2
2004
2005=item B<client_id>
2006
2007The client identifier of Kalamar to be send with every OAuth 2.0
2008management request.
2009
2010=item B<client_secret>
2011
2012The client secret of Kalamar to be send with every OAuth 2.0
2013management request.
2014
Akron59992122019-10-29 11:28:45 +01002015=item B<experimental_client_registration>
2016
2017Activates the oauth client registration flow.
2018
2019=back
2020
2021=head2 COPYRIGHT AND LICENSE
2022
Akrond91a1ca2022-05-20 16:45:01 +02002023Copyright (C) 2015-2022, L<IDS Mannheim|http://www.ids-mannheim.de/>
Akron59992122019-10-29 11:28:45 +01002024Author: L<Nils Diewald|http://nils-diewald.de/>
2025
2026Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
2027Corpus Analysis Platform at the
2028L<Leibniz Institute for the German Language (IDS)|http://ids-mannheim.de/>,
2029member of the
2030L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de>
2031and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
2032funded by the
2033L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
2034
2035Kalamar is free software published under the
2036L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
2037
2038=cut