blob: e65972b1a78fd11fcd0b87cb2f838d6c26d09cc5 [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',
103 desc => 'Kurzbeschreibung',
Akronc1aaf932021-06-09 12:19:15 +0200104 revoke => 'Widerrufen',
Akron59992122019-10-29 11:28:45 +0100105 clientCredentials => 'Client Daten',
106 clientType => 'Art der Client-Applikation',
107 clientName => 'Name der Client-Applikation',
108 clientID => 'ID der Client-Applikation',
109 clientSecret => 'Client-Secret',
110 clientRegister => 'Neue Client-Applikation registrieren',
111 registerSuccess => 'Registrierung erfolgreich',
112 registerFail => 'Registrierung fehlgeschlagen',
113 oauthSettings => 'OAuth',
Helge278fbca2022-11-29 18:49:15 +0100114 #for marketplace settings
115 marketplace => 'Marktplatz',
116 mp_regby => "Registriert von",
117 mp_regdate => "Registrierungsdatum",
118
Akrone997bb52021-06-11 16:44:06 +0200119 oauthUnregister => {
120 -long => 'Möchten sie <span class="client-name"><%= $client_name %></span> wirklich löschen?',
121 short => 'Löschen'
122 },
Akron9f2ad342022-05-04 16:16:40 +0200123 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 +0100124 loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
Akrone997bb52021-06-11 16:44:06 +0200125 oauthIssueToken => {
126 -long => 'Stelle einen neuen Token für <span class="client-name"><%= $client_name %></span> aus',
127 short => 'Neuen Token ausstellen'
128 },
Akron9ffb4a32021-06-08 16:11:21 +0200129 accessToken => 'Access Token',
Akrone997bb52021-06-11 16:44:06 +0200130 oauthRevokeToken => {
131 -long => 'Widerrufe einen Token für <span class="client-name"><%= $client_name %></span>',
132 short => 'Widerrufe'
133 },
Akrona8efaa92022-04-09 14:45:43 +0200134 oauthGrantScope => {
135 -long => '<span class="client-name"><%= $client_name %></span> möchte Zugriffsrechte',
136 short => 'Zugriffsrechte erteilen'
137 },
Akron408bc7c2022-04-28 15:46:43 +0200138 oauthGrantPublicWarn => 'Achtung - dies ist ein öffentlicher Client!',
Akron9d826902023-01-25 10:20:52 +0100139 oauthGrantRedirectWarn => 'Die Weiterleitung findet an eine unbekannte Adresse statt',
Akrone997bb52021-06-11 16:44:06 +0200140 createdAt => 'Erstellt am <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
Akron9f2ad342022-05-04 16:16:40 +0200141 expiresIn => 'Läuft in <%= stash("seconds") %> Sekunden ab.',
142 fileSizeExceeded => 'Dateigröße überschritten'
Akron864c2932018-11-16 17:18:55 +0100143 },
144 -en => {
Akrona8efaa92022-04-09 14:45:43 +0200145 loginPlease => 'Please log in!',
Akron864c2932018-11-16 17:18:55 +0100146 loginSuccess => 'Login successful',
147 loginFail => 'Access denied',
148 logoutSuccess => 'Logout successful',
149 logoutFail => 'Logout failed',
Akronff088112021-06-15 15:26:04 +0200150 authenticationFail => 'Not authenticated',
Akron864c2932018-11-16 17:18:55 +0100151 csrfFail => 'Bad CSRF token',
Akron2c142ab2023-01-30 13:21:57 +0100152 scopeFail => 'Scope required',
153 clientIDFail => 'Client ID required',
Akron6a228db2021-10-14 15:57:00 +0200154 invalidChar => 'Invalid character in request',
Akron8bbbecf2019-07-01 18:57:30 +0200155 openRedirectFail => 'Redirect failure',
Akroncdfd9d52019-07-23 11:35:00 +0200156 tokenExpired => 'Access token expired',
157 tokenInvalid => 'Access token invalid',
158 refreshFail => 'Bad refresh token',
Akron59992122019-10-29 11:28:45 +0100159 responseError => 'Unknown authorization error',
Akroncce055c2021-07-02 12:18:03 +0200160 serverError => 'Unknown server error',
Akronc1aaf932021-06-09 12:19:15 +0200161 revokeFail => 'Token can\'t be revoked',
162 revokeSuccess => 'Token was revoked successfully',
Akron59992122019-10-29 11:28:45 +0100163 paramError => 'Some fields are invalid',
164 redirectUri => 'Redirect URI',
Akron9f2ad342022-05-04 16:16:40 +0200165 pluginSrc => 'Declaration of the plugin (*.json file)',
Akron59992122019-10-29 11:28:45 +0100166 homepage => 'Homepage',
167 desc => 'Short description',
Akronc1aaf932021-06-09 12:19:15 +0200168 revoke => 'Revoke',
Akron59992122019-10-29 11:28:45 +0100169 clientCredentials => 'Client Credentials',
170 clientType => 'Type of the client application',
171 clientName => 'Name of the client application',
172 clientID => 'ID of the client application',
173 clientSecret => 'Client secret',
174 clientRegister => 'Register new client application',
175 registerSuccess => 'Registration successful',
176 registerFail => 'Registration denied',
177 oauthSettings => 'OAuth',
Helge278fbca2022-11-29 18:49:15 +0100178 #for marketplace settings
179 marketplace => 'Marketplace',
180 mp_regby =>"Registered by",
181 mp_regdate =>"Registration date",
Akrone997bb52021-06-11 16:44:06 +0200182 oauthUnregister => {
183 -long => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
184 short => 'Unregister'
185 },
Akron9f2ad342022-05-04 16:16:40 +0200186 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 +0100187 loginHint => 'Maybe you need to log in first?',
Akrone997bb52021-06-11 16:44:06 +0200188 oauthIssueToken => {
189 -long => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
190 short => 'Issue new token'
191 },
Akron9ffb4a32021-06-08 16:11:21 +0200192 accessToken => 'Access Token',
Akrone997bb52021-06-11 16:44:06 +0200193 oauthRevokeToken => {
194 -long => 'Revoke a token for <span class="client-name"><%= $client_name %></span>',
195 short => 'Revoke'
196 },
Akrona8efaa92022-04-09 14:45:43 +0200197 oauthGrantScope => {
198 -long => '<span class="client-name"><%= $client_name %></span> wants to have access',
199 short => 'Grant access'
200 },
Akron408bc7c2022-04-28 15:46:43 +0200201 oauthGrantPublicWarn => 'Warning - this is a public client!',
Akron9d826902023-01-25 10:20:52 +0100202 oauthGrantRedirectWarn => 'The redirect points to an unknown location',
Akrone997bb52021-06-11 16:44:06 +0200203 createdAt => 'Created at <time datetime="<%= stash("date") %>"><%= stash("date") %></date>.',
Akron9f2ad342022-05-04 16:16:40 +0200204 expiresIn => 'Expires in <%= stash("seconds") %> seconds.',
205 fileSizeExceeded => 'File size exceeded',
206 confidentialRequired => 'Plugins need to be confidential',
207 jsonRequired => 'Plugin declarations need to be json files',
Akron864c2932018-11-16 17:18:55 +0100208 }
209 }
210 }
211 });
212
213
Akrona9c8b0e2018-11-16 20:20:28 +0100214 # Add login frame to sidebar
215 $app->content_block(
216 sidebar => {
217 template => 'partial/auth/login'
218 }
219 );
220
221
Akronc82b1bc2018-11-18 18:06:14 +0100222 # Add logout button to header button list
223 $app->content_block(
224 headerButtonGroup => {
225 template => 'partial/auth/logout'
226 }
227 );
228
Akron27031aa2020-04-28 14:57:10 +0200229
230 # Add hook after search
231 $app->hook(
232 after_search => sub {
233 my $c = shift;
234
235 # User is not logged in
236 if ($c->stash('results')->size == 0 && !$c->auth->token) {
237 $c->content_for(
238 'after_search_results' =>
239 $c->render_to_string(
240 inline => '<p class="hint"><%= loc "Auth_loginHint" %></p>'
241 )
242 );
243 };
244 }
245 );
246
Akron59992122019-10-29 11:28:45 +0100247 # The plugin path
248 my $path = catdir(dirname(__FILE__), 'Auth');
249
250 # Append "templates"
251 push @{$app->renderer->paths}, catdir($path, 'templates');
Akron864c2932018-11-16 17:18:55 +0100252
Akron4796e002019-07-05 10:13:15 +0200253 # Get or set the user token necessary for authorization
Akron864c2932018-11-16 17:18:55 +0100254 $app->helper(
255 'auth.token' => sub {
Akroncdfd9d52019-07-23 11:35:00 +0200256 my ($c, $token, $expires_in) = @_;
Akron864c2932018-11-16 17:18:55 +0100257
Akroncdfd9d52019-07-23 11:35:00 +0200258 if ($token) {
259 # Set auth token
Akron4796e002019-07-05 10:13:15 +0200260 $c->stash(auth => $token);
Akroncdfd9d52019-07-23 11:35:00 +0200261 $c->session(auth => $token);
262 $c->session(auth_exp => time + $expires_in);
263 return 1;
Akron4796e002019-07-05 10:13:15 +0200264 };
265
Akroncdfd9d52019-07-23 11:35:00 +0200266 # Get token from stash
267 $token = $c->stash('auth');
268
269 return $token if $token;
270
271 # Get auth from session
272 $token = $c->session('auth') or return;
273 $c->stash(auth => $token);
274
275 # Return stashed value
276 return $token;
Akron864c2932018-11-16 17:18:55 +0100277 }
278 );
279
Akron864c2932018-11-16 17:18:55 +0100280 # Log in to the system
281 my $r = $app->routes;
Akron864c2932018-11-16 17:18:55 +0100282
Akrond91a1ca2022-05-20 16:45:01 +0200283 my $client_id = $param->{client_id};
284 my $client_secret = $param->{client_secret};
Akron8bbbecf2019-07-01 18:57:30 +0200285
Akrone3daaeb2023-05-08 09:44:18 +0200286 my $no_redirect_ua = Mojo::UserAgent->new(
287 connect_timeout => 30,
288 inactivity_timeout => 30,
289 max_redirects => 0
290 );
291
292 $no_redirect_ua->server->app($app);
293
Akroncdfd9d52019-07-23 11:35:00 +0200294
Akrond91a1ca2022-05-20 16:45:01 +0200295 # Sets a requested token and returns
296 # an error, if it didn't work
297 $app->helper(
298 'auth.set_tokens_p' => sub {
299 my ($c, $json) = @_;
300 my $promise = Mojo::Promise->new;
Akroncdfd9d52019-07-23 11:35:00 +0200301
Akrond91a1ca2022-05-20 16:45:01 +0200302 # No json object
303 unless ($json) {
304 return $promise->reject({
305 message => 'Response is no valid JSON object (remote)'
306 });
307 };
Akroncdfd9d52019-07-23 11:35:00 +0200308
Akrond91a1ca2022-05-20 16:45:01 +0200309 # There is an error here
310 # Dealing with errors here
311 if ($json->{error} && ref $json->{error} ne 'ARRAY') {
312 return $promise->reject(
313 {
314 message => $json->{error} . ($json->{error_description} ? ': ' . $json->{error_description} : '')
Akron0f1b93b2020-03-17 11:37:19 +0100315 }
316 );
317 }
Akroncdfd9d52019-07-23 11:35:00 +0200318
Akrond91a1ca2022-05-20 16:45:01 +0200319 # There is an array of errors
320 elsif (my $error = $json->{errors} // $json->{error}) {
321 if (ref $error eq 'ARRAY') {
322 my @errors = ();
323 foreach (@{$error}) {
324 if ($_->[1]) {
325 push @errors, { code => $_->[0], message => $_->[1]}
Akronbc94a9c2021-04-15 00:07:35 +0200326 };
Akrond91a1ca2022-05-20 16:45:01 +0200327 };
328 return $promise->reject(@errors);
Akroncdfd9d52019-07-23 11:35:00 +0200329 };
330
Akrond91a1ca2022-05-20 16:45:01 +0200331 return $promise->reject({message => $error});
332 };
Akroncdfd9d52019-07-23 11:35:00 +0200333
Akrond91a1ca2022-05-20 16:45:01 +0200334 # Everything is fine
335 my $access_token = $json->{access_token};
336 my $token_type = $json->{token_type};
337 my $refresh_token = $json->{refresh_token};
338 my $expires_in = $json->{"expires_in"} // $EXPECTED_EXPIRATION_IN;
339 my $auth = $token_type . ' ' . $access_token;
340 # my $scope = $json->{scope};
Akroncdfd9d52019-07-23 11:35:00 +0200341
Akrond91a1ca2022-05-20 16:45:01 +0200342 # Set session info
343 $c->session(auth => $auth);
344
345 # Expiration of the token minus tolerance
346 $c->session(auth_exp => time + $expires_in - 60);
347
348 # Set session info for refresh token
349 # This can be stored in the session, as it is useless
350 # unless the client secret is stolen
351 $c->session(auth_r => $refresh_token) if $refresh_token;
352
353 # Set stash info
354 $c->stash(auth => $auth);
355
356 return $promise->resolve;
357 }
358 );
359
360
361 # Refresh tokens and return a promise
362 $app->helper(
363 'auth.refresh_p' => sub {
364 my $c = shift;
365 my $refresh_token = shift;
366
367 # Get OAuth access token
368 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
369
370 $c->app->log->debug("Refresh at $r_url");
371
372 return $c->kalamar_ua->post_p($r_url, {} => form => {
373 grant_type => 'refresh_token',
374 client_id => $client_id,
375 client_secret => $client_secret,
376 refresh_token => $refresh_token
377 })->then(
378 sub {
379 my $tx = shift;
380 my $json = $tx->result->json;
381
382 # Response is fine
383 if ($tx->res->is_success) {
384
385 $c->app->log->info("Refresh was successful");
386
387 # Set the tokens and return a promise
388 return $c->auth->set_tokens_p($json);
389 };
390
391 # There is a client error - refresh fails
392 if ($tx->res->is_client_error && $json) {
393
Akroncdfd9d52019-07-23 11:35:00 +0200394 $c->stash(auth => undef);
Akrond91a1ca2022-05-20 16:45:01 +0200395 $c->stash(auth_exp => undef);
396 delete $c->session->{user};
397 delete $c->session->{auth};
398 delete $c->session->{auth_r};
399 delete $c->session->{auth_exp};
Akroncdfd9d52019-07-23 11:35:00 +0200400
Akrond91a1ca2022-05-20 16:45:01 +0200401 # Response is 400
402 return Mojo::Promise->reject(
403 $json->{error_description} // $c->loc('Auth_refreshFail')
404 );
405 };
Akroncdfd9d52019-07-23 11:35:00 +0200406
Akrond91a1ca2022-05-20 16:45:01 +0200407 if ($tx->res->is_server_error) {
408 return Mojo::Promise->reject(
409 '600'
410 )
411 };
Akroncdfd9d52019-07-23 11:35:00 +0200412
Akrond91a1ca2022-05-20 16:45:01 +0200413 $c->notify(error => $c->loc('Auth_responseError'));
414 return Mojo::Promise->reject;
415 }
416 )
417 }
418 );
Akroncdfd9d52019-07-23 11:35:00 +0200419
Helge278fbca2022-11-29 18:49:15 +0100420
Akrone3daaeb2023-05-08 09:44:18 +0200421 # Issue new token and return a promise
422 $app->helper(
423 'auth.new_token_p' => sub {
424 my $c = shift;
425 my %param = @_;
426
427 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
428
429 my $client_id = $param{'client_id'};
430
431 return $c->korap_request($no_redirect_ua, post => $r_url, { } => form => {
432 response_type => 'code',
433 client_id => $client_id,
434 redirect_uri => $param{'redirect_uri'},
435 state => $param{'state'},
436 scope => $param{'scope'},
437 })->then(
438 sub {
439 my $tx = shift;
440
441 unless (ref($tx)) {
442 return Mojo::Promise->reject('Something went wrong');
443 };
444
445 # Check for location header with code in redirects
446 my ($code, $scope, $loc, $name);
447
448 # Check for location header with code in current tx
449 # and in redirects.
450 # The loop should not be relevant though, as for now
451 # redirects are not allowed.
452 foreach ($tx, @{$tx->redirects}) {
453 $loc = $_->res->headers->header('Location');
454
455 next unless $loc;
456
457 my $url = Mojo::URL->new($loc);
458
459 if ($url->query->param('code')) {
460 my $q = $url->query;
461 $code = $q->param('code');
462 $scope = $q->param('scope');
463 $name = $q->param('name');
464 last;
465 } elsif (my $err = $url->query->param('error_description')) {
466 return Mojo::Promise->reject($err);
467 }
468 };
469
470 return Mojo::Promise->resolve(
471 $loc,
472 $client_id,
473 $param{'redirect_uri'},
474 $code,
475 $scope,
476 $name
477 ) if $loc;
478
479 # Failed redirect, but location set
480 if ($tx->res->headers->location) {
481 my $url = Mojo::URL->new($tx->res->headers->location);
482 if (my $err = $url->query->param('error_description')) {
483 return Mojo::Promise->reject($err);
484 };
485 }
486
487 $c->stash(redirect_uri => undef);
488
489 # Maybe json
490 my $json = $tx->res->json;
491 if ($json && $json->{error_description}) {
492 return Mojo::Promise->reject($json->{error_description});
493 };
494
495 # No location code
496 return Mojo::Promise->reject('no location response');
497 }
498 )
499 }
500 );
501
Akrond91a1ca2022-05-20 16:45:01 +0200502 # Get a list of registered clients
503 $app->helper(
504 'auth.client_list_p' => sub {
505 my $c = shift;
Akroncdfd9d52019-07-23 11:35:00 +0200506
Helge278fbca2022-11-29 18:49:15 +0100507
Akrond91a1ca2022-05-20 16:45:01 +0200508 # Get list of registered clients
509 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/list');
Akrona8efaa92022-04-09 14:45:43 +0200510
Akrond91a1ca2022-05-20 16:45:01 +0200511 # Get the list of all clients
512 return $c->korap_request(post => $r_url, {} => form => {
513 super_client_id => $client_id,
514 super_client_secret => $client_secret,
515 authorized_only => 'no'
516 })->then(
517 sub {
518 my $tx = shift;
519 my $json = $tx->result->json;
Akroncdfd9d52019-07-23 11:35:00 +0200520
Akrond91a1ca2022-05-20 16:45:01 +0200521 # Response is fine
522 if ($tx->res->is_success) {
523 return Mojo::Promise->resolve($json);
524 };
525
Helge690e94d2023-04-19 16:01:24 +0200526 $c->log->error($tx->res->to_string);
Akrond91a1ca2022-05-20 16:45:01 +0200527
528 # Failure
529 $c->notify(error => $c->loc('Auth_responseError'));
530 return Mojo::Promise->reject($json // 'No response');
531 }
532 );
533 }
534 );
535
Akrondb1f4672023-01-24 12:05:07 +0100536 # Get info for registered client
537 $app->helper(
538 'auth.client_info_p' => sub {
539 my $c = shift;
Akron9d826902023-01-25 10:20:52 +0100540 my $req_client_id = shift;
Akrondb1f4672023-01-24 12:05:07 +0100541
542 # Get list of registered clients
Akron9d826902023-01-25 10:20:52 +0100543 my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/')->path($req_client_id);
Akrondb1f4672023-01-24 12:05:07 +0100544
545 my $form = {
546 super_client_id => $client_id,
547 super_client_secret => $client_secret,
548 };
549
550 # Get the list of all clients
551 return $c->korap_request(POST => $r_url, {} => form => $form)->then(
552 sub {
553 my $tx = shift;
554 my $json = $tx->result->json // {};
555
556 # Response is fine
557 if ($tx->res->is_success) {
558 return Mojo::Promise->resolve($json);
559 };
560
Helge690e94d2023-04-19 16:01:24 +0200561 $c->log->error($tx->res->to_string);
Akrondb1f4672023-01-24 12:05:07 +0100562
563 # Failure
564 return Mojo::Promise->reject($json->{error_description} // 'Client unknown');
565 }
566 );
567 }
568 );
569
Akrond91a1ca2022-05-20 16:45:01 +0200570
571 # Get a list of registered clients
572 $app->helper(
573 'auth.token_list_p' => sub {
574 my $c = shift;
575 my $user_client_id = shift;
576
577 # Revoke the token
578 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token/list');
579
580 my $form = {
581 super_client_id => $client_id,
582 super_client_secret => $client_secret,
583 token_type => 'access_token',
584 };
585
586 if ($user_client_id) {
587 $form->{client_id} = $user_client_id;
588 };
589
590 # Get the list of all clients
591 return $c->korap_request(post => $r_url, {} => form => $form)->then(
592 sub {
593 my $tx = shift;
594 my $json = $tx->result->json;
595
596 # Response is fine
597 if ($tx->res->is_success) {
598 return Mojo::Promise->resolve($json);
599 };
600
Helge690e94d2023-04-19 16:01:24 +0200601 $c->log->error($tx->res->to_string);
Akrond91a1ca2022-05-20 16:45:01 +0200602
603 # Failure
604 $c->notify(error => $c->loc('Auth_responseError'));
605 return Mojo::Promise->reject($json // 'No response');
606 }
607 );
608 }
609 );
610
611
612 # Issue a korap request with "oauth"orization
613 # This will override the core request helper
614 $app->helper(
615 korap_request => sub {
616 my $c = shift;
Akrona8f87cc2023-02-23 12:21:30 +0100617
618 # Get plugin user agent
619 my $ua = $c->kalamar_ua;
620
621 # Override if UA is granted
622 if (ref $_[0] eq 'Mojo::UserAgent') {
623 $ua = shift;
624 };
625
Akrond91a1ca2022-05-20 16:45:01 +0200626 my $method = shift;
627 my $path = shift;
Akrona8f87cc2023-02-23 12:21:30 +0100628
Akrond91a1ca2022-05-20 16:45:01 +0200629 my @param = @_;
630
631 # TODO:
632 # Check if $tx is not leaked!
633
Akrond91a1ca2022-05-20 16:45:01 +0200634 my $url = Mojo::URL->new($path);
635 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
636
637 # Set X-Forwarded for
638 $tx->req->headers->header(
639 'X-Forwarded-For' => $c->client_ip
640 );
641
642 # Emit Hook to alter request
643 $c->app->plugins->emit_hook(
644 before_korap_request => ($c, $tx)
645 );
646
647 my $h = $tx->req->headers;
648
649 # If the request already has an Authorization
650 # header, respect it!
651 if ($h->authorization) {
652 return $ua->start_p($tx);
653 };
654
655 # Get auth token
656 if (my $auth_token = $c->auth->token) {
657
658 # The token is already expired!
659 my $exp = $c->session('auth_exp');
660 if (defined $exp && $exp < time) {
661
662 # Remove auth ...
663 $c->stash(auth => undef);
664
665 # And get refresh token from session
666 if (my $refresh_token = $c->session('auth_r')) {
667
668 $c->app->log->debug("Refresh is required");
669
670 # Refresh
671 return $c->auth->refresh_p($refresh_token)->then(
672 sub {
673 $c->app->log->debug("Search with refreshed tokens");
674
675 # Tokens were set - now send the request the first time!
676 $tx->req->headers->authorization($c->stash('auth'));
677 return $ua->start_p($tx);
678 }
679 );
Akroncdfd9d52019-07-23 11:35:00 +0200680 }
681
Akrond91a1ca2022-05-20 16:45:01 +0200682 # The token is expired and no refresh token is
683 # available - issue an unauthorized request!
Akroncdfd9d52019-07-23 11:35:00 +0200684 else {
685
Akrond91a1ca2022-05-20 16:45:01 +0200686 $c->stash(auth => undef);
687 $c->stash(auth_exp => undef);
688 delete $c->session->{user};
689 delete $c->session->{auth};
690 delete $c->session->{auth_r};
691 delete $c->session->{auth_exp};
692
693 # Warn on Error!
694 $c->notify(warn => $c->loc('Auth_tokenExpired'));
695 return $ua->start_p($tx);
696 };
Akroncdfd9d52019-07-23 11:35:00 +0200697 }
698
Akrond91a1ca2022-05-20 16:45:01 +0200699 # Auth token is fine
Akroncdfd9d52019-07-23 11:35:00 +0200700 else {
701
Akrond91a1ca2022-05-20 16:45:01 +0200702 # Set auth
703 $h->authorization($auth_token);
704 }
705 }
Akroncdfd9d52019-07-23 11:35:00 +0200706
Akrond91a1ca2022-05-20 16:45:01 +0200707 # No token set
708 else {
Akroncdfd9d52019-07-23 11:35:00 +0200709
Akrond91a1ca2022-05-20 16:45:01 +0200710 # Return unauthorized request
711 return $ua->start_p($tx);
712 };
Akroncdfd9d52019-07-23 11:35:00 +0200713
Akrond91a1ca2022-05-20 16:45:01 +0200714 # Issue an authorized request and automatically
715 # refresh the token on expiration!
716 return $ua->start_p($tx)->then(
717 sub {
718 my $tx = shift;
Akroncdfd9d52019-07-23 11:35:00 +0200719
Akrond91a1ca2022-05-20 16:45:01 +0200720 # Response is fine
Akrona8f87cc2023-02-23 12:21:30 +0100721 if ($tx->res->is_success || $tx->res->is_redirect) {
Akrond91a1ca2022-05-20 16:45:01 +0200722 return Mojo::Promise->resolve($tx);
723 }
Akroncdfd9d52019-07-23 11:35:00 +0200724
Akrond91a1ca2022-05-20 16:45:01 +0200725 # There is a client error - maybe refresh!
726 elsif ($tx->res->is_client_error) {
Akroncdfd9d52019-07-23 11:35:00 +0200727
Akrond91a1ca2022-05-20 16:45:01 +0200728 # Check the error
729 my $json = $tx->res->json('/errors/0/1');
730 if ($json && ($json =~ /expired|invalid/)) {
731 $c->stash(auth => undef);
732 $c->stash(auth_exp => undef);
733 delete $c->session->{user};
734 delete $c->session->{auth};
Akroncdfd9d52019-07-23 11:35:00 +0200735
Akrond91a1ca2022-05-20 16:45:01 +0200736 # And get refresh token from session
737 if (my $refresh_token = $c->session('auth_r')) {
Akroncdfd9d52019-07-23 11:35:00 +0200738
Akrond91a1ca2022-05-20 16:45:01 +0200739 # Refresh
740 return $c->auth->refresh_p($refresh_token)->then(
741 sub {
742 $c->app->log->debug("Search with refreshed tokens");
Akroncdfd9d52019-07-23 11:35:00 +0200743
Akrond91a1ca2022-05-20 16:45:01 +0200744 my $tx = $ua->build_tx(uc($method), $url->clone, @param);
Akroncdfd9d52019-07-23 11:35:00 +0200745
Akrond91a1ca2022-05-20 16:45:01 +0200746 # Set X-Forwarded for
747 $tx->req->headers->header(
748 'X-Forwarded-For' => $c->client_ip
749 );
750
751 # Tokens were set - now send the request the first time!
752 $tx->req->headers->authorization($c->stash('auth'));
753 return $ua->start_p($tx);
754 }
755 )
Akroncdfd9d52019-07-23 11:35:00 +0200756 };
757
Akrond91a1ca2022-05-20 16:45:01 +0200758 # Reject the invalid token
759 $c->notify(error => $c->loc('Auth_tokenInvalid'));
760 return Mojo::Promise->reject;
Akroncdfd9d52019-07-23 11:35:00 +0200761 };
762
Akrond91a1ca2022-05-20 16:45:01 +0200763 return Mojo::Promise->resolve($tx);
Akron8bbbecf2019-07-01 18:57:30 +0200764 }
Akron8bbbecf2019-07-01 18:57:30 +0200765
Akrond91a1ca2022-05-20 16:45:01 +0200766 # There is a server error - just report
767 elsif ($tx->res->is_server_error) {
768 my $err = $tx->res->error;
769 if ($err) {
770 return Mojo::Promise->reject($err->{code} . ': ' . $err->{message});
771 }
772 else {
773 $c->notify(error => $c->loc('Auth_serverError'));
774 return Mojo::Promise->reject;
775 };
776 };
777
778 $c->notify(error => $c->loc('Auth_responseError'));
779 return Mojo::Promise->reject;
780 }
781 );
782 }
783 );
784
785 # Password flow for OAuth
786 $r->post('/user/login')->to(
787 cb => sub {
788 my $c = shift;
789
790 # Validate input
791 my $v = $c->validation;
792 $v->required('handle_or_email', 'trim');
793 $v->required('pwd', 'trim');
794 $v->csrf_protect;
795 $v->optional('fwd')->closed_redirect;
796
797 my $user = check_decode($v->param('handle_or_email'));
798 unless ($user) {
799 $c->notify(error => $c->loc('Auth_invalidChar'));
800 $c->param(handle_or_email => '');
801 return $c->relative_redirect_to('index');
802 };
803
804 my $fwd = $v->param('fwd');
805
806 # Set flash for redirect
807 $c->flash(handle_or_email => $user);
808
809 if ($v->has_error || index($user, ':') >= 0) {
810 if ($v->has_error('fwd')) {
811 $c->notify(error => $c->loc('Auth_openRedirectFail'));
812 }
813 elsif ($v->has_error('csrf_token')) {
814 $c->notify(error => $c->loc('Auth_csrfFail'));
815 }
816 else {
817 $c->notify(error => $c->loc('Auth_loginFail'));
818 };
819
820 return $c->relative_redirect_to($fwd // 'index');
821 }
822
823 my $pwd = $v->param('pwd');
824
825 $c->app->log->debug("Login from user $user");
826
827 # <specific>
828
829 # Get OAuth access token
830 my $url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
831
832 # Korap request for login
833 $c->korap_request('post', $url, {}, form => {
834 grant_type => 'password',
835 username => $user,
836 password => $pwd,
837 client_id => $client_id,
838 client_secret => $client_secret
839 })->then(
840 sub {
841 # Set the tokens and return a promise
842 return $c->auth->set_tokens_p(shift->result->json)
843 }
844 )->then(
845 sub {
846 # Set user info
847 $c->session(user => $user);
848 $c->stash(user => $user);
849
850 # Notify on success
851 $c->app->log->debug(qq!Login successful: "$user"!);
852 $c->notify(success => $c->loc('Auth_loginSuccess'));
853 }
854 )->catch(
855 sub {
856
857 # Notify the user on login failure
858 unless (@_) {
859 $c->notify(error => $c->loc('Auth_loginFail'));
860 }
861
862 # There are known errors
863 foreach (@_) {
864 if (ref $_ eq 'HASH') {
865 my $err = ($_->{code} ? $_->{code} . ': ' : '') .
866 $_->{message};
867 $c->notify(error => $err);
868 # Log failure
869 $c->app->log->debug($err);
870 }
871 else {
872 $c->notify(error => $_);
873 $c->app->log->debug($_);
874 };
875 };
876
877 $c->app->log->debug(qq!Login fail: "$user"!);
878 }
879 )->finally(
880 sub {
881 # Redirect to slash
882 return $c->relative_redirect_to($fwd // 'index');
883 }
884 )
885
886 # Start IOLoop
887 ->wait;
888
889 return 1;
890 }
891 )->name('login');
892
893
894 # Log out of the session
895 $r->get('/user/logout')->to(
896 cb => sub {
897 my $c = shift;
898
899 # TODO: csrf-protection!
900
901 my $refresh_token = $c->session('auth_r');
902
903 # Revoke the token
904 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke');
905
906 $c->kalamar_ua->post_p($url => {} => form => {
907 client_id => $client_id,
908 client_secret => $client_secret,
909 token => $refresh_token,
910 token_type => 'refresh_token'
911 })->then(
912 sub {
913 my $tx = shift;
914 my $json = $tx->result->json;
915
916 my $promise;
917
918 # Response is fine
919 if ($tx->res->is_success) {
920 $c->app->log->info("Revocation was successful");
921 $c->notify(success => $c->loc('Auth_logoutSuccess'));
922
923 $c->stash(auth => undef);
924 $c->stash(auth_exp => undef);
925 $c->flash(handle_or_email => delete $c->session->{user});
926 delete $c->session->{auth};
927 delete $c->session->{auth_r};
928 delete $c->session->{auth_exp};
929 return Mojo::Promise->resolve;
930 };
931
932 # Token may be invalid
933 $c->notify('error', $c->loc('Auth_logoutFail'));
934
935 # There is a client error - refresh fails
936 if ($tx->res->is_client_error && $json) {
937
938 return Mojo::Promise->reject(
939 $json->{error_description}
940 );
941 };
942
943 # Resource may not be found (404)
944 return Mojo::Promise->reject
945
946 }
947 )->catch(
948 sub {
949 my $err = shift;
950
951 # Server may be irresponsible
952 $c->notify('error', $c->loc('Auth_logoutFail'));
953 return Mojo::Promise->reject($err);
954 }
955 )->finally(
956 sub {
957 return $c->redirect_to('index');
958 }
959 )->wait;
960 }
961 )->name('logout');
962
963
Helge278fbca2022-11-29 18:49:15 +0100964
965
Akrond91a1ca2022-05-20 16:45:01 +0200966 # If "experimental_registration" is set, open
967 # OAuth registration dialogues.
968 if ($param->{experimental_client_registration}) {
969
970 # Add settings
971 $app->navi->add(settings => (
972 $app->loc('Auth_oauthSettings'), 'oauth'
Helge278fbca2022-11-29 18:49:15 +0100973 )
974 );
975 #$app->navi->add(settings => (
976 # $app->loc('Auth_marketplace'), 'marketplace'
977 #));
978
979
980 # Lists all permitted registered plugins
981 $app->helper(
982 'auth.plugin_list_m' => sub {
983
984 my $c = shift;
985 state $r_url = Mojo::URL->new($c->korap->api)->path('plugins');
986 return $c->korap_request(post => $r_url, {} => form => {
987 super_client_id => $client_id,
988 super_client_secret => $client_secret,
989 #list only permitted plugins
990 permitted_only => 'true'
991 })->then(
992 sub {
993 my $tx = shift;
994 my $json = $tx->result->json;
995
996 # Response is fine
997 if ($tx->res->is_success) {
998 return Mojo::Promise->resolve($json);
999 };
1000
1001 $c->log->error($c->dumper($tx->res->to_string));
1002
1003 # Failure
1004 $c->notify(error => $c->loc('Auth_responseError'));
1005 return Mojo::Promise->reject($json // 'No response');
1006 }
1007 );
1008 }
1009 );
1010
1011 # Route to marketplace settings
1012 $r->get('/settings/marketplace')->to(
1013 cb => sub {
1014 my $c = shift;
1015 _set_no_cache($c->res->headers);
1016
1017 unless ($c->auth->token) {
1018 #TODO: Handle authorization (forward to Login for example)
1019 return $c->render(
1020 template => 'exception',
1021 msg => $c->loc('Auth_authenticationFail'),
1022 status => 401
1023 );
1024 };
1025
1026 $c->render_later;
1027 $c->auth->plugin_list_m->then(
1028 sub {
1029 $c->stash('plugin_list' => shift);
1030 }
1031 )->catch(
1032 sub {
1033 return;
1034 }
1035 )->finally(
1036 sub {
1037 return $c->render(template => 'auth/marketplace');
1038 }
1039 );
1040 }
1041 )->name('marketplace');
1042
1043
1044
1045 # Route to OAuth settings
1046 $r->get('/settings/oauth')->to(
1047 cb => sub {
1048 my $c = shift;
1049
1050 _set_no_cache($c->res->headers);
1051
1052 unless ($c->auth->token) {
1053 return $c->render(
1054 template => 'exception',
1055 msg => $c->loc('Auth_authenticationFail'),
1056 status => 401
1057 );
1058 };
1059
1060 # Wait for async result
1061 $c->render_later;
1062
1063 $c->auth->client_list_p->then(
1064 sub {
1065 $c->stash('client_list' => shift);
1066 }
1067 )->catch(
1068 sub {
1069 return;
1070 }
1071 )->finally(
1072 sub {
1073 return $c->render(template => 'auth/clients')
1074 }
1075 );
1076 }
1077 )->name('oauth-settings');
Akrond91a1ca2022-05-20 16:45:01 +02001078
Akrond91a1ca2022-05-20 16:45:01 +02001079 # Route to oauth client registration
1080 $r->post('/settings/oauth/register')->to(
1081 cb => sub {
1082 my $c = shift;
1083
1084 _set_no_cache($c->res->headers);
1085
1086 my $v = $c->validation;
1087
1088 unless ($c->auth->token) {
1089 return $c->render(
1090 content => 'Unauthorized',
1091 status => 401
1092 );
1093 };
1094
1095 $v->csrf_protect;
1096 $v->required('name', 'trim', 'not_empty')->size(3, 255);
1097 $v->required('type')->in('PUBLIC', 'CONFIDENTIAL');
1098 $v->required('desc', 'trim', 'not_empty')->size(3, 255);
1099 $v->optional('url', 'trim', 'not_empty')->like(qr/^(http|$)/i);
1100 $v->optional('redirect_uri', 'trim', 'not_empty')->like(qr/^(http|$)/i);
1101 $v->optional('src', 'not_empty');
1102
1103 $c->stash(template => 'auth/clients');
1104
1105 # Render with error
1106 if ($v->has_error) {
1107 if ($v->has_error('csrf_token')) {
Akron33f5c672019-06-24 19:40:47 +02001108 $c->notify(error => $c->loc('Auth_csrfFail'));
1109 }
1110 else {
Akrond91a1ca2022-05-20 16:45:01 +02001111 $c->notify(error => $c->loc('Auth_paramError'));
Akron864c2932018-11-16 17:18:55 +01001112 };
Akrond91a1ca2022-05-20 16:45:01 +02001113 return $c->render;
1114 } elsif ($c->req->is_limit_exceeded) {
1115 $c->notify(error => $c->loc('Auth_fileSizeExceeded'));
1116 return $c->render;
1117 };
Akron864c2932018-11-16 17:18:55 +01001118
Akrond91a1ca2022-05-20 16:45:01 +02001119 my $type = $v->param('type');
1120 my $src = $v->param('src');
1121 my $src_json;
Akron864c2932018-11-16 17:18:55 +01001122
Akrond91a1ca2022-05-20 16:45:01 +02001123 my $json_obj = {
1124 name => $v->param('name'),
1125 type => $type,
1126 description => $v->param('desc'),
1127 url => $v->param('url'),
1128 redirect_uri => $v->param('redirect_uri')
1129 };
Akron864c2932018-11-16 17:18:55 +01001130
Akrond91a1ca2022-05-20 16:45:01 +02001131 # Check plugin source
1132 if ($src) {
Akron33f5c672019-06-24 19:40:47 +02001133
Akrond91a1ca2022-05-20 16:45:01 +02001134 # Source need to be a file upload
Akron99968a92022-06-03 12:32:07 +02001135 if (!ref $src || !$src->isa('Mojo::Upload')) {
Akrond91a1ca2022-05-20 16:45:01 +02001136 $c->notify(error => $c->loc('Auth_jsonRequired'));
1137 return $c->render;
1138 };
1139
1140 # Uploads can't be too large
1141 if ($src->size > 1_000_000) {
Akron9f2ad342022-05-04 16:16:40 +02001142 $c->notify(error => $c->loc('Auth_fileSizeExceeded'));
1143 return $c->render;
1144 };
1145
Akrond91a1ca2022-05-20 16:45:01 +02001146 # Check upload is not empty
1147 if ($src->size > 0 && $src->filename ne '') {
Akron9f2ad342022-05-04 16:16:40 +02001148
Akron99968a92022-06-03 12:32:07 +02001149 # Plugins need to be confidential
1150 if ($type ne 'CONFIDENTIAL') {
1151 $c->notify(error => $c->loc('Auth_confidentialRequired'));
1152 return $c->render;
1153 }
1154
Akrond91a1ca2022-05-20 16:45:01 +02001155 my $asset = $src->asset;
Akron9f2ad342022-05-04 16:16:40 +02001156
Akrond91a1ca2022-05-20 16:45:01 +02001157 # Check for json
1158 eval {
1159 $src_json = decode_json($asset->slurp);
1160 };
Akron9f2ad342022-05-04 16:16:40 +02001161
Akrond91a1ca2022-05-20 16:45:01 +02001162 if ($@ || !ref $src_json || ref $src_json ne 'HASH') {
Akron9f2ad342022-05-04 16:16:40 +02001163 $c->notify(error => $c->loc('Auth_jsonRequired'));
1164 return $c->render;
1165 };
1166
Akrond91a1ca2022-05-20 16:45:01 +02001167 $json_obj->{source} = $src_json;
Akron59992122019-10-29 11:28:45 +01001168 };
Akron83209f72021-01-29 17:54:15 +01001169 };
1170
Akrond91a1ca2022-05-20 16:45:01 +02001171 # Wait for async result
1172 $c->render_later;
Akron83209f72021-01-29 17:54:15 +01001173
Akrond91a1ca2022-05-20 16:45:01 +02001174 # Register on server
1175 state $url = Mojo::URL->new($c->korap->api)->path('oauth2/client/register');
1176 $c->korap_request('POST', $url => {} => json => $json_obj)->then(
Akron83209f72021-01-29 17:54:15 +01001177 sub {
1178 my $tx = shift;
Akrond91a1ca2022-05-20 16:45:01 +02001179 my $result = $tx->result;
Akron83209f72021-01-29 17:54:15 +01001180
Akrond91a1ca2022-05-20 16:45:01 +02001181 if ($result->is_error) {
1182 my $json = $result->json;
1183 if ($json && $json->{error}) {
1184 $c->notify(
1185 error => $json->{error} .
1186 ($json->{error_description} ? ': ' . $json->{error_description} : '')
1187 )
Akron83209f72021-01-29 17:54:15 +01001188 };
Akrond91a1ca2022-05-20 16:45:01 +02001189
1190 return Mojo::Promise->reject;
Akron83209f72021-01-29 17:54:15 +01001191 };
1192
Akrond91a1ca2022-05-20 16:45:01 +02001193 my $json = $result->json;
1194
1195 my $client_id = $json->{client_id};
1196 my $client_secret = $json->{client_secret};
1197
1198 $c->stash('client_name' => $v->param('name'));
1199 $c->stash('client_desc' => $v->param('desc'));
1200 $c->stash('client_type' => $v->param('type'));
1201 $c->stash('client_url' => $v->param('url'));
1202 $c->stash('client_src' => $v->param('source'));
1203 $c->stash('client_redirect_uri' => $v->param('redirect_uri'));
1204 $c->stash('client_id' => $client_id);
1205
1206 if ($client_secret) {
1207 $c->stash('client_secret' => $client_secret);
Akron83209f72021-01-29 17:54:15 +01001208 };
Akron83209f72021-01-29 17:54:15 +01001209
Akrond91a1ca2022-05-20 16:45:01 +02001210 $c->notify(success => $c->loc('Auth_en_registerSuccess'));
Akron83209f72021-01-29 17:54:15 +01001211
Akrond91a1ca2022-05-20 16:45:01 +02001212 return $c->render(template => 'auth/client');
Akron83209f72021-01-29 17:54:15 +01001213 }
1214 )->catch(
1215 sub {
Akrond91a1ca2022-05-20 16:45:01 +02001216 $c->notify('error' => $c->loc('Auth_en_registerFail'));
Akron83209f72021-01-29 17:54:15 +01001217 }
Akrond91a1ca2022-05-20 16:45:01 +02001218 )->finally(
1219 sub {
1220 return $c->redirect_to('settings' => { scope => 'oauth' });
1221 }
1222 );
Akron83209f72021-01-29 17:54:15 +01001223 }
Akrond91a1ca2022-05-20 16:45:01 +02001224 )->name('oauth-register');
Akronc1aaf932021-06-09 12:19:15 +02001225
1226
Akrond91a1ca2022-05-20 16:45:01 +02001227 # Unregister client page
1228 $r->get('/settings/oauth/:client_id/unregister')->to(
Akronc1aaf932021-06-09 12:19:15 +02001229 cb => sub {
1230 my $c = shift;
Akrond91a1ca2022-05-20 16:45:01 +02001231 _set_no_cache($c->res->headers);
1232 $c->render(template => 'auth/unregister');
1233 }
1234 )->name('oauth-unregister');
1235
1236
1237 # Unregister client
1238 $r->post('/settings/oauth/:client_id/unregister')->to(
1239 cb => sub {
1240 my $c = shift;
1241 _set_no_cache($c->res->headers);
Akronc1aaf932021-06-09 12:19:15 +02001242
1243 my $v = $c->validation;
1244
1245 unless ($c->auth->token) {
1246 return $c->render(
1247 content => 'Unauthorized',
1248 status => 401
1249 );
1250 };
1251
1252 $v->csrf_protect;
Akrond91a1ca2022-05-20 16:45:01 +02001253 $v->required('client-name', 'trim')->size(3, 255);
Akronc1aaf932021-06-09 12:19:15 +02001254
1255 # Render with error
1256 if ($v->has_error) {
1257 if ($v->has_error('csrf_token')) {
1258 $c->notify(error => $c->loc('Auth_csrfFail'));
1259 }
1260 else {
1261 $c->notify(error => $c->loc('Auth_paramError'));
1262 };
Akrond91a1ca2022-05-20 16:45:01 +02001263 return $c->redirect_to('oauth-settings');
Akronc1aaf932021-06-09 12:19:15 +02001264 };
1265
Akrond91a1ca2022-05-20 16:45:01 +02001266 my $client_id = $c->stash('client_id');
1267 my $client_name = $v->param('client-name');
1268 my $client_secret = $v->param('client-secret');
Akronc1aaf932021-06-09 12:19:15 +02001269
Akrond91a1ca2022-05-20 16:45:01 +02001270 # Get list of registered clients
1271 my $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/client/deregister/')->path(
1272 $client_id
1273 );
Akronc1aaf932021-06-09 12:19:15 +02001274
Akrond91a1ca2022-05-20 16:45:01 +02001275 my $send = {};
1276
1277 if ($client_secret) {
1278 $send->{client_secret} = $client_secret;
1279 };
1280
1281 # Get the list of all clients
1282 return $c->korap_request(delete => $r_url, {} => form => $send)->then(
Akronc1aaf932021-06-09 12:19:15 +02001283 sub {
1284 my $tx = shift;
1285
1286 # Response is fine
1287 if ($tx->res->is_success) {
Akrond91a1ca2022-05-20 16:45:01 +02001288 # Okay
1289 $c->notify(success => 'Successfully deleted ' . $client_name);
Akronc1aaf932021-06-09 12:19:15 +02001290 }
1291 else {
Akrond91a1ca2022-05-20 16:45:01 +02001292
1293 # Failure
1294 my $json = $tx->result->json;
1295 if ($json && $json->{error_description}) {
1296 $c->notify(error => $json->{error_description});
1297 } else {
1298 $c->notify(error => $c->loc('Auth_responseError'));
1299 };
Akronc1aaf932021-06-09 12:19:15 +02001300 };
Akrond91a1ca2022-05-20 16:45:01 +02001301
1302 return $c->redirect_to('oauth-settings');
Akronc1aaf932021-06-09 12:19:15 +02001303 }
Akrond91a1ca2022-05-20 16:45:01 +02001304 );
Akronc1aaf932021-06-09 12:19:15 +02001305 }
Akrond91a1ca2022-05-20 16:45:01 +02001306 )->name('oauth-unregister-post');
Akroncdfd9d52019-07-23 11:35:00 +02001307
Akron33f5c672019-06-24 19:40:47 +02001308
Akrond91a1ca2022-05-20 16:45:01 +02001309 # OAuth Client authorization
1310 $r->get('/settings/oauth/authorize')->to(
Akron33f5c672019-06-24 19:40:47 +02001311 cb => sub {
1312 my $c = shift;
1313
Akrond91a1ca2022-05-20 16:45:01 +02001314 _set_no_cache($c->res->headers);
Akron33f5c672019-06-24 19:40:47 +02001315
Akrond91a1ca2022-05-20 16:45:01 +02001316 my $v = $c->validation;
Akron9d826902023-01-25 10:20:52 +01001317 $v->required('client_id', 'trim');
1318 $v->required('scope', 'trim');
1319 $v->optional('state', 'trim');
1320 $v->optional('redirect_uri', 'trim', 'not_empty')->like(qr/^(http|$)/i);
Akrond91a1ca2022-05-20 16:45:01 +02001321
1322 # Redirect with error
1323 if ($v->has_error) {
Akron2c142ab2023-01-30 13:21:57 +01001324
1325 if ($v->has_error('client_id')) {
1326 $c->notify(error => $c->loc('Auth_clientIDFail'));
1327 }
1328 elsif ($v->has_error('scope')) {
1329 $c->notify(error => $c->loc('Auth_scopeFail'));
1330 }
1331 else {
1332 $c->notify(error => $c->loc('Auth_paramError'));
1333 };
Akron001dcd22023-02-07 08:38:11 +01001334
1335 # If logged in, go to oauth settings - otherwise to index
1336 return $c->redirect_to($c->auth->token ? 'oauth-settings' : 'index');
Akron6a228db2021-10-14 15:57:00 +02001337 };
1338
Akrond91a1ca2022-05-20 16:45:01 +02001339 foreach (qw!scope client_id state redirect_uri!) {
1340 $c->stash($_, $v->param($_));
1341 };
Akron33f5c672019-06-24 19:40:47 +02001342
Akrond91a1ca2022-05-20 16:45:01 +02001343 # Wait for async result
1344 $c->render_later;
Akron33f5c672019-06-24 19:40:47 +02001345
Akrond91a1ca2022-05-20 16:45:01 +02001346 my $client_id = $v->param('client_id');
1347
Akrondb1f4672023-01-24 12:05:07 +01001348 my $client_information = $c->auth->client_info_p($client_id)->then(
Akrond91a1ca2022-05-20 16:45:01 +02001349 sub {
Akrondb1f4672023-01-24 12:05:07 +01001350 my $cl = shift;
1351 $c->stash(client_name => $cl->{'client_name'});
1352 $c->stash(client_type => $cl->{'client_type'});
1353 $c->stash(client_desc => $cl->{'client_description'});
1354 $c->stash(client_url => $cl->{'client_url'});
1355 $c->stash(redirect_uri_server => $cl->{'client_redirect_uri'});
Akron33f5c672019-06-24 19:40:47 +02001356 }
Akrondb1f4672023-01-24 12:05:07 +01001357 )->then(
Akrond91a1ca2022-05-20 16:45:01 +02001358 sub {
1359
Akron9d826902023-01-25 10:20:52 +01001360 # Check, if the client redirect_uri is valid
1361 my $redirect_uri_server = $c->stash('redirect_uri_server');
1362 my $redirect_uri = $v->param('redirect_uri');
1363 my ($server_o, $client_o);
1364
1365 # Both exist
1366 if ($redirect_uri_server && $redirect_uri) {
1367 $server_o = Mojo::URL->new($redirect_uri_server);
1368 $client_o = Mojo::URL->new($redirect_uri);
1369
1370 # Host not valid - take registered URI
1371 if ($server_o->host ne $client_o->host) {
1372 $c->notify(warn => 'redirect_uri host differs from registered host');
1373 $client_o = $server_o;
1374 }
1375
1376 # Port not valid - take registered URI
1377 elsif (($server_o->port // '') ne ($client_o->port // '')) {
1378 $c->notify(warn => 'redirect_uri port differs from registered port');
1379 $client_o = $server_o;
1380 };
1381 }
1382
1383 # Client sent exists
1384 elsif ($redirect_uri) {
1385 $client_o = Mojo::URL->new($redirect_uri);
1386 $c->stash(redirect_warning => $c->loc('oauthGrantRedirectWarn'))
1387 }
1388
1389 # Server registered exists
1390 elsif ($redirect_uri_server) {
1391 $client_o = Mojo::URL->new($redirect_uri_server);
1392 }
1393
1394 # Redirect unknown
1395 else {
1396 $c->notify(error => 'redirect_uri not set');
Akron001dcd22023-02-07 08:38:11 +01001397
1398 # If logged in, go to oauth settings - otherwise to index
1399 return $c->redirect_to($c->auth->token ? 'oauth-settings' : 'index');
Akron9d826902023-01-25 10:20:52 +01001400 };
1401
1402 # No userinfo allowed
1403 if ($client_o->userinfo) {
1404 $c->notify(warn => 'redirect_uri contains userinfo');
1405 $client_o->userinfo('');
1406 };
1407
1408 # HTTPS warning
1409 # if ($client_o->scheme ne 'https') {
1410 # $c->notify(warn => 'redirect_uri is not HTTPS');
1411 # };
1412
1413 # Sign redirection URL
1414 $c->stash(redirect_uri => $client_o->to_string);
1415 $c->stash(close_redirect_uri => '' . $c->close_redirect_to($client_o));
1416
Akrond91a1ca2022-05-20 16:45:01 +02001417 # Get auth token
1418 my $auth_token = $c->auth->token;
1419
1420 # User is not logged in - log in before!
1421 unless ($auth_token) {
1422 return $c->render(template => 'auth/login');
1423 };
1424
1425 # Grant authorization
1426 return $c->render(template => 'auth/grant_scope');
1427 }
Akrondb1f4672023-01-24 12:05:07 +01001428 )->catch(
1429 sub {
1430 my $error = shift;
1431 $c->notify(error => $error);
Akron001dcd22023-02-07 08:38:11 +01001432
1433 # If logged in, go to oauth settings - otherwise to index
1434 return $c->redirect_to($c->auth->token ? 'oauth-settings' : 'index');
Akrondb1f4672023-01-24 12:05:07 +01001435 }
Akrond91a1ca2022-05-20 16:45:01 +02001436 );
1437 }
1438 )->name('oauth-grant-scope');
1439
1440
1441 # OAuth Client authorization
1442 # This will return a location information including some info
1443 $r->post('/settings/oauth/authorize')->to(
1444 cb => sub {
1445 my $c = shift;
1446
1447 _set_no_cache($c->res->headers);
1448
1449 # It's necessary that it's clear this was triggered by
1450 # KorAP and not by the client!
1451 my $v = $c->validation;
1452 $v->csrf_protect;
Akron9d826902023-01-25 10:20:52 +01001453 $v->required('client_id', 'trim');
1454 $v->required('scope', 'trim');
1455 $v->optional('state', 'trim');
Akrond91a1ca2022-05-20 16:45:01 +02001456
Akron9d826902023-01-25 10:20:52 +01001457 # Only signed redirects are allowed
1458 $v->required('redirect_uri', 'trim', 'not_empty')->closed_redirect;
Akrond91a1ca2022-05-20 16:45:01 +02001459
1460 # Render with error
1461 if ($v->has_error) {
Akron9d826902023-01-25 10:20:52 +01001462
Akron001dcd22023-02-07 08:38:11 +01001463 # If logged in, go to oauth settings - otherwise to index
1464 my $url = $c->url_for($c->auth->token ? 'oauth-settings' : 'index');
Akrond91a1ca2022-05-20 16:45:01 +02001465
Akron2c142ab2023-01-30 13:21:57 +01001466 if ($v->has_error('client_id')) {
1467 $url->query([error_description => $c->loc('Auth_clientIDFail')]);
1468 }
1469 elsif ($v->has_error('scope')) {
1470 $url->query([error_description => $c->loc('Auth_scopeFail')]);
1471 }
1472 elsif ($v->has_error('csrf_token')) {
Akrond91a1ca2022-05-20 16:45:01 +02001473 $url->query([error_description => $c->loc('Auth_csrfFail')]);
Akron33f5c672019-06-24 19:40:47 +02001474 }
1475 else {
Akrond91a1ca2022-05-20 16:45:01 +02001476 $url->query([error_description => $c->loc('Auth_paramError')]);
Akron33f5c672019-06-24 19:40:47 +02001477 };
1478
Akrond91a1ca2022-05-20 16:45:01 +02001479 return $c->redirect_to($url);
1480 };
Akron33f5c672019-06-24 19:40:47 +02001481
Akron9d826902023-01-25 10:20:52 +01001482 $c->stash(redirect_uri => Mojo::URL->new($v->param('redirect_uri')));
Akron33f5c672019-06-24 19:40:47 +02001483
Akrone3daaeb2023-05-08 09:44:18 +02001484 return $c->auth->new_token_p(
Akrond91a1ca2022-05-20 16:45:01 +02001485 client_id => $v->param('client_id'),
Akron9d826902023-01-25 10:20:52 +01001486 redirect_uri => $c->stash('redirect_uri'),
Akrond91a1ca2022-05-20 16:45:01 +02001487 state => $v->param('state'),
1488 scope => $v->param('scope'),
Akron33f5c672019-06-24 19:40:47 +02001489 )->catch(
1490 sub {
Akrond91a1ca2022-05-20 16:45:01 +02001491 my $err_msg = shift;
Akron9d826902023-01-25 10:20:52 +01001492 my $url = $c->stash('redirect_uri');
Akron9ccf69a2023-01-31 14:21:37 +01001493
1494 # Redirect!
1495 if ($url) {
1496 if ($err_msg) {
1497 $url = $url->query([error_description => $err_msg]);
1498 };
1499 }
1500
1501 # Do not redirect!
1502 else {
1503 $c->notify(error => $err_msg);
Akron001dcd22023-02-07 08:38:11 +01001504
1505 # If logged in, go to oauth settings - otherwise to index
1506 $url = $c->url_for($c->auth->token ? 'oauth-settings' : 'index');
Akrond91a1ca2022-05-20 16:45:01 +02001507 };
Akron9ccf69a2023-01-31 14:21:37 +01001508
Akrond91a1ca2022-05-20 16:45:01 +02001509 return Mojo::Promise->resolve($url);
Akron33f5c672019-06-24 19:40:47 +02001510 }
Akrond91a1ca2022-05-20 16:45:01 +02001511 )->then(
Akron33f5c672019-06-24 19:40:47 +02001512 sub {
Akrond91a1ca2022-05-20 16:45:01 +02001513 my $loc = shift;
1514 return $c->redirect_to($loc);
Akron33f5c672019-06-24 19:40:47 +02001515 }
Akrond91a1ca2022-05-20 16:45:01 +02001516 )->wait;
1517 return $c->rendered;
Akron33f5c672019-06-24 19:40:47 +02001518 }
Akrond91a1ca2022-05-20 16:45:01 +02001519 )->name('oauth-grant-scope-post');
Akron4cefe1f2019-09-04 10:11:28 +02001520
1521
Akrond91a1ca2022-05-20 16:45:01 +02001522 # Show information of a client
1523 $r->get('/settings/oauth/:client_id')->to(
Akron4cefe1f2019-09-04 10:11:28 +02001524 cb => sub {
1525 my $c = shift;
1526
Akrond91a1ca2022-05-20 16:45:01 +02001527 _set_no_cache($c->res->headers);
Akron4cefe1f2019-09-04 10:11:28 +02001528
Akrond91a1ca2022-05-20 16:45:01 +02001529 $c->render_later;
Akron4cefe1f2019-09-04 10:11:28 +02001530
Akrond91a1ca2022-05-20 16:45:01 +02001531 $c->auth->client_list_p->then(
1532 sub {
1533 my $json = shift;
1534
1535 my ($item) = grep {
1536 $c->stash('client_id') eq $_->{client_id}
1537 } @$json;
1538
1539 unless ($item) {
1540 return Mojo::Promise->reject;
1541 };
1542
1543 $c->stash(client_name => $item->{client_name});
1544 $c->stash(client_desc => $item->{client_description});
1545 $c->stash(client_url => $item->{client_url});
1546 $c->stash(client_type => ($item->{client_type} // 'PUBLIC'));
Akroneb39cf32023-04-03 14:40:48 +02001547 $c->stash(client_redirect_uri => $item->{client_redirect_uri});
Akrond91a1ca2022-05-20 16:45:01 +02001548 $c->stash(client_src => encode_json($item->{source})) if $item->{source};
1549
1550 $c->auth->token_list_p($c->stash('client_id'));
1551 }
Akron4cefe1f2019-09-04 10:11:28 +02001552 )->then(
Akron4cefe1f2019-09-04 10:11:28 +02001553 sub {
Akrond91a1ca2022-05-20 16:45:01 +02001554 my $json = shift;
Akron4cefe1f2019-09-04 10:11:28 +02001555
Akrond91a1ca2022-05-20 16:45:01 +02001556 $c->stash(tokens => $json);
Akron4cefe1f2019-09-04 10:11:28 +02001557
Akrond91a1ca2022-05-20 16:45:01 +02001558 return Mojo::Promise->resolve;
Akron4cefe1f2019-09-04 10:11:28 +02001559 }
Akron4cefe1f2019-09-04 10:11:28 +02001560 )->catch(
Akron4cefe1f2019-09-04 10:11:28 +02001561 sub {
Akrond91a1ca2022-05-20 16:45:01 +02001562 return $c->reply->not_found;
Akron4cefe1f2019-09-04 10:11:28 +02001563 }
Akron4cefe1f2019-09-04 10:11:28 +02001564 )->finally(
Akron4cefe1f2019-09-04 10:11:28 +02001565 sub {
Akrond91a1ca2022-05-20 16:45:01 +02001566 return $c->render(template => 'auth/client')
Akron4cefe1f2019-09-04 10:11:28 +02001567 }
Akrond91a1ca2022-05-20 16:45:01 +02001568 );
Akron4cefe1f2019-09-04 10:11:28 +02001569
Akrond91a1ca2022-05-20 16:45:01 +02001570 return;
Akron4cefe1f2019-09-04 10:11:28 +02001571 }
Akrond91a1ca2022-05-20 16:45:01 +02001572 )->name('oauth-tokens');
Akron33f5c672019-06-24 19:40:47 +02001573 };
Akron59992122019-10-29 11:28:45 +01001574
Akrond91a1ca2022-05-20 16:45:01 +02001575
1576 # Ask if new token should be issued
1577 $r->get('/settings/oauth/:client_id/token')->to(
1578 cb => sub {
1579 my $c = shift;
1580 _set_no_cache($c->res->headers);
1581 $c->render(template => 'auth/issue-token');
1582 }
1583 )->name('oauth-issue-token');
1584
1585
1586 # Ask if a token should be revoked
1587 $r->post('/settings/oauth/:client_id/token/revoke')->to(
1588 cb => sub {
1589 shift->render(template => 'auth/revoke-token');
1590 }
1591 )->name('oauth-revoke-token');
1592
1593
1594 # Issue new token
1595 $r->post('/settings/oauth/:client_id/token')->to(
1596 cb => sub {
1597 my $c = shift;
1598 _set_no_cache($c->res->headers);
1599
1600 my $v = $c->validation;
1601
1602 unless ($c->auth->token) {
1603 return $c->render(
1604 content => 'Unauthorized',
1605 status => 401
1606 );
1607 };
1608
1609 $v->csrf_protect;
1610 $v->optional('client-secret');
1611 $v->required('name', 'trim');
1612
1613 # Render with error
1614 if ($v->has_error) {
1615 if ($v->has_error('csrf_token')) {
1616 $c->notify(error => $c->loc('Auth_csrfFail'));
1617 }
1618 else {
1619 $c->notify(error => $c->loc('Auth_paramError'));
1620 };
1621 return $c->redirect_to('oauth-settings')
1622 };
1623
1624 # Get authorization token
Akrond91a1ca2022-05-20 16:45:01 +02001625 my $client_id = $c->stash('client_id');
1626 my $name = $v->param('name');
1627 my $redirect_url = $c->url_for->query({name => $name});
1628
Akrone3daaeb2023-05-08 09:44:18 +02001629 $c->auth->new_token_p(
Akrond91a1ca2022-05-20 16:45:01 +02001630 client_id => $client_id,
1631 redirect_uri => $redirect_url,
Akrone3daaeb2023-05-08 09:44:18 +02001632 # TODO: State, scope
Akrond91a1ca2022-05-20 16:45:01 +02001633 )->then(
1634 sub {
Akrone3daaeb2023-05-08 09:44:18 +02001635 my ($loc, $client_id, $redirect_url, $code, $scope, $name) = @_;
Akrond91a1ca2022-05-20 16:45:01 +02001636
1637 # Get OAuth access token
1638 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
1639 return $c->kalamar_ua->post_p($r_url, {} => form => {
1640 client_id => $client_id,
1641 # NO CLIENT_SECRET YET SUPPORTED
1642 grant_type => 'authorization_code',
1643 code => $code,
1644 redirect_uri => $redirect_url
1645 })->then(
1646 sub {
1647 my $tx = shift;
1648 my $json = $tx->res->json;
1649
1650 if ($tx->res->is_error) {
1651 $c->notify(error => 'Unable to fetch new token');
1652 return Mojo::Promise->reject;
1653 };
1654
1655 $c->notify(success => 'New access token created');
1656
1657 $c->redirect_to('oauth-tokens' => { client_id => $client_id })
1658 }
1659 )->catch(
1660 sub {
1661 my $err_msg = shift;
1662
1663 # Only raised in case of connection errors
1664 if ($err_msg) {
1665 $c->notify(error => { src => 'Backend' } => $err_msg)
1666 };
1667
1668 $c->render(
1669 status => 400,
1670 template => 'failure'
1671 );
1672 }
1673 )
1674
1675 # Start IOLoop
1676 ->wait;
1677
1678 }
1679 )->catch(
1680 sub {
1681 my $err_msg = shift;
1682
1683 # Only raised in case of connection errors
1684 if ($err_msg) {
1685 $c->notify(error => { src => 'Backend' } => $err_msg)
1686 };
1687
1688 return $c->render(
1689 status => 400,
1690 template => 'failure'
1691 );
1692 }
1693 )
1694
1695 # Start IOLoop
1696 ->wait;
1697
1698 return 1;
1699 }
1700 )->name('oauth-issue-token-post');
1701
1702
1703 # Revoke token
1704 $r->delete('/settings/oauth/:client_id/token')->to(
1705 cb => sub {
1706 my $c = shift;
1707
1708 my $v = $c->validation;
1709
1710 unless ($c->auth->token) {
1711 return $c->render(
1712 content => 'Unauthorized',
1713 status => 401
1714 );
1715 };
1716
1717 $v->csrf_protect;
1718 $v->required('token', 'trim');
1719 $v->optional('name', 'trim');
1720 my $private_client_id = $c->stash('client_id');
1721
1722 # Render with error
1723 if ($v->has_error) {
1724 if ($v->has_error('csrf_token')) {
1725 $c->notify(error => $c->loc('Auth_csrfFail'));
1726 }
1727 else {
1728 $c->notify(error => $c->loc('Auth_paramError'));
1729 };
1730 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1731 };
1732
1733 # Revoke token using super client privileges
1734 state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke/super');
1735
1736 my $token = $v->param('token');
1737
1738 return $c->korap_request(post => $r_url, {} => form => {
1739 super_client_id => $client_id,
1740 super_client_secret => $client_secret,
1741 token => $token
1742 })->then(
1743 sub {
1744 my $tx = shift;
1745
1746 # Response is fine
1747 if ($tx->res->is_success) {
1748 $c->notify(success => $c->loc('Auth_revokeSuccess'));
1749 return Mojo::Promise->resolve;
1750 };
1751
1752 return Mojo::Promise->reject;
1753 }
1754 )->catch(
1755 sub {
1756 my $err_msg = shift;
1757 if ($err_msg) {
1758 $c->notify(error => { src => 'Backend' } => $err_msg );
1759 }
1760 else {
1761 $c->notify(error => $c->loc('Auth_revokeFail'));
1762 };
1763 }
1764 )->finally(
1765 sub {
1766 return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
1767 }
1768 )
1769
1770 # Start IOLoop
1771 ->wait;
1772 }
1773 )->name('oauth-revoke-token-delete');
1774
Akron59992122019-10-29 11:28:45 +01001775 $app->log->info('Successfully registered Auth plugin');
Akron864c2932018-11-16 17:18:55 +01001776};
1777
Akronb3f33592020-03-16 15:14:44 +01001778
Akronad011bb2021-06-10 12:16:36 +02001779# Set 'no caching' headers
1780sub _set_no_cache {
1781 my $h = shift;
1782 $h->cache_control('max-age=0, no-cache, no-store, must-revalidate');
1783 $h->expires('Thu, 01 Jan 1970 00:00:00 GMT');
1784 $h->header('Pragma','no-cache');
1785};
1786
1787
Akron6a228db2021-10-14 15:57:00 +02001788sub check_decode {
1789 no warnings 'uninitialized';
1790 my $str = shift;
1791 my $str2 = is_utf8($str) ? b($str)->decode : $str;
1792 if (defined($str2) && $str2 && length($str2) > 1) {
1793 return $str2
1794 };
1795 return;
1796};
1797
1798
Akron864c2932018-11-16 17:18:55 +010017991;
Akrona9c8b0e2018-11-16 20:20:28 +01001800
Akronc82b1bc2018-11-18 18:06:14 +01001801
Akrona9c8b0e2018-11-16 20:20:28 +01001802__END__
Akron59992122019-10-29 11:28:45 +01001803
1804=pod
1805
1806=encoding utf8
1807
1808=head1 NAME
1809
1810Kalamar::Plugin::Auth - OAuth-2.0-based authorization plugin
1811
1812=head1 DESCRIPTION
1813
1814L<Kalamar::Plugin::Auth> is an OAuth-2.0-based authorization
1815plugin for L<Kalamar>. It requires a C<Kustvakt> full server
1816with OAuth 2.0 capabilities.
1817It is activated by loading C<Auth> as a plugin in the C<Kalamar.plugins>
1818parameter in the Kalamar configuration.
1819
1820=head1 CONFIGURATION
1821
1822L<Kalamar::Plugin::Auth> supports the following parameter for the
1823C<Kalamar-Auth> configuration section in the Kalamar configuration:
1824
1825=over 2
1826
1827=item B<client_id>
1828
1829The client identifier of Kalamar to be send with every OAuth 2.0
1830management request.
1831
1832=item B<client_secret>
1833
1834The client secret of Kalamar to be send with every OAuth 2.0
1835management request.
1836
Akron59992122019-10-29 11:28:45 +01001837=item B<experimental_client_registration>
1838
1839Activates the oauth client registration flow.
1840
1841=back
1842
1843=head2 COPYRIGHT AND LICENSE
1844
Akrond91a1ca2022-05-20 16:45:01 +02001845Copyright (C) 2015-2022, L<IDS Mannheim|http://www.ids-mannheim.de/>
Akron59992122019-10-29 11:28:45 +01001846Author: L<Nils Diewald|http://nils-diewald.de/>
1847
1848Kalamar is developed as part of the L<KorAP|http://korap.ids-mannheim.de/>
1849Corpus Analysis Platform at the
1850L<Leibniz Institute for the German Language (IDS)|http://ids-mannheim.de/>,
1851member of the
1852L<Leibniz-Gemeinschaft|http://www.leibniz-gemeinschaft.de>
1853and supported by the L<KobRA|http://www.kobra.tu-dortmund.de> project,
1854funded by the
1855L<Federal Ministry of Education and Research (BMBF)|http://www.bmbf.de/en/>.
1856
1857Kalamar is free software published under the
1858L<BSD-2 License|https://raw.githubusercontent.com/KorAP/Kalamar/master/LICENSE>.
1859
1860=cut