Allow to revoke tokens
Change-Id: I2e55935441b108740a164385923c5b7b994a24ed
diff --git a/Changes b/Changes
index f79dfe9..706386a 100755
--- a/Changes
+++ b/Changes
@@ -21,6 +21,7 @@
- Use AutoSecrets plugin to improve security.
- Fixed bug where missing documentation pages raise
exceptions in Mojo >= 9.0.
+ - Support revocation of tokens.
WARNING: Upgrading to Mojolicious 9.19 will
invalidate all sessions. This is a security update.
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 458a290..66e4d2f 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -62,10 +62,13 @@
tokenInvalid => 'Zugriffstoken ungültig',
refreshFail => 'Fehlerhafter Refresh-Token',
responseError => 'Unbekannter Autorisierungsfehler',
+ revokeFail => 'Der Token kann nicht widerrufen werden',
+ revokeSuccess => 'Der Token wurde erfolgreich widerrufen',
paramError => 'Einige Eingaben sind fehlerhaft',
redirectUri => 'Weiterleitungs-Adresse',
homepage => 'Webseite',
desc => 'Kurzbeschreibung',
+ revoke => 'Widerrufen',
clientCredentials => 'Client Daten',
clientType => 'Art der Client-Applikation',
clientName => 'Name der Client-Applikation',
@@ -79,6 +82,7 @@
loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $client_name %></span>',
accessToken => 'Access Token',
+ oauthRevokeToken => 'Widerrufe einen Token für <span class="client-name"><%= $client_name %></span>',
},
-en => {
loginSuccess => 'Login successful',
@@ -91,10 +95,13 @@
tokenInvalid => 'Access token invalid',
refreshFail => 'Bad refresh token',
responseError => 'Unknown authorization error',
+ revokeFail => 'Token can\'t be revoked',
+ revokeSuccess => 'Token was revoked successfully',
paramError => 'Some fields are invalid',
redirectUri => 'Redirect URI',
homepage => 'Homepage',
desc => 'Short description',
+ revoke => 'Revoke',
clientCredentials => 'Client Credentials',
clientType => 'Type of the client application',
clientName => 'Name of the client application',
@@ -106,8 +113,9 @@
oauthSettings => 'OAuth',
oauthUnregister => 'Do you really want to unregister <span class="client-name"><%= $client_name %></span>?',
loginHint => 'Maybe you need to log in first?',
- oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $client_name %></span>',
+ oauthIssueToken => 'Issue a new token for <span class="client-name"><%= $client_name %></span>',
accessToken => 'Access Token',
+ oauthRevokeToken => 'Revoka a token for <span class="client-name"><%= $client_name %></span>',
}
}
}
@@ -775,7 +783,7 @@
$c->notify(error => $c->loc('Auth_csrfFail'));
}
else {
- $c->notify(warn => $c->loc('Auth_paramError'));
+ $c->notify(error => $c->loc('Auth_paramError'));
};
# return $c->redirect_to('oauth-settings');
return $c->render(template => 'auth/clients');
@@ -870,7 +878,7 @@
$c->notify(error => $c->loc('Auth_csrfFail'));
}
else {
- $c->notify(warn => $c->loc('Auth_paramError'));
+ $c->notify(error => $c->loc('Auth_paramError'));
};
return $c->redirect_to('oauth-settings');
};
@@ -968,14 +976,23 @@
};
- # Show information of a client
- $r->get('/settings/oauth/client/:client_id/token')->to(
+ # Ask if new token should be issued
+ $r->get('/settings/oauth/client/:client_id/token/issue')->to(
cb => sub {
shift->render(template => 'auth/issue-token');
}
)->name('oauth-issue-token');
+ # Ask if a token should be revoked
+ $r->post('/settings/oauth/client/:client_id/token/revoke')->to(
+ cb => sub {
+ shift->render(template => 'auth/revoke-token');
+ }
+ )->name('oauth-revoke-token');
+
+
+ # Issue new token
$r->post('/settings/oauth/client/:client_id/token')->to(
cb => sub {
my $c = shift;
@@ -990,7 +1007,6 @@
};
$v->csrf_protect;
- # $v->required('client-id', 'trim')->size(3, 255);
$v->optional('client-secret');
$v->required('name', 'trim');
@@ -1000,7 +1016,7 @@
$c->notify(error => $c->loc('Auth_csrfFail'));
}
else {
- $c->notify(warn => $c->loc('Auth_paramError'));
+ $c->notify(error => $c->loc('Auth_paramError'));
};
return $c->redirect_to('oauth-settings')
};
@@ -1114,6 +1130,79 @@
return 1;
}
)->name('oauth-issue-token-post');
+
+
+ # Revoke token
+ $r->delete('/settings/oauth/client/:client_id/token')->to(
+ cb => sub {
+ my $c = shift;
+
+ my $v = $c->validation;
+
+ unless ($c->auth->token) {
+ return $c->render(
+ content => 'Unauthorized',
+ status => 401
+ );
+ };
+
+ $v->csrf_protect;
+ $v->required('token', 'trim');
+ $v->optional('name', 'trim');
+ my $private_client_id = $c->stash('client_id');
+
+ # Render with error
+ if ($v->has_error) {
+ if ($v->has_error('csrf_token')) {
+ $c->notify(error => $c->loc('Auth_csrfFail'));
+ }
+ else {
+ $c->notify(error => $c->loc('Auth_paramError'));
+ };
+ return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
+ };
+
+ # Revoke token using super client privileges
+ state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/revoke/super');
+
+ my $token = $v->param('token');
+
+ return $c->korap_request(post => $r_url, {} => form => {
+ super_client_id => $client_id,
+ super_client_secret => $client_secret,
+ token => $token
+ })->then(
+ sub {
+ my $tx = shift;
+
+ # Response is fine
+ if ($tx->res->is_success) {
+ $c->notify(success => $c->loc('Auth_revokeSuccess'));
+ return Mojo::Promise->resolve;
+ };
+
+ return Mojo::Promise->reject;
+ }
+ )->catch(
+ sub {
+ my $err_msg = shift;
+ if ($err_msg) {
+ $c->notify(error => { src => 'Backend' } => $err_msg );
+ }
+ else {
+ $c->notify(error => $c->loc('Auth_revokeFail'));
+ };
+ }
+ )->finally(
+ sub {
+ return $c->redirect_to('oauth-tokens', client_id => $private_client_id);
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+ }
+ )->name('oauth-revoke-token-delete');
}
# Use JWT login
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
index 60f7870..2fbbd80 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
@@ -2,7 +2,7 @@
%= page_title
-<form class="form-table">
+<div class="form-table">
<fieldset>
<legend><%= loc 'Auth_clientCredentials' %></legend>
<ul class="client-list">
@@ -46,10 +46,18 @@
% if ($_->{scope}) {
<p name="scope">Scope: <tt><%= join ',', @{$_->{scope}} %></tt></p>
% };
+ <span class="button-group button-panel">
+ %= form_for 'oauth-revoke-token' => {} => ( class => 'token-revoke' ), begin
+ %= csrf_field
+ <input type="hidden" name="name" value="<%= stash('client_name') %>" />
+ <input type="hidden" name="token" value="<%= $_->{token} %>" />
+ <input type="submit" value="<%= loc 'Auth_revoke' %>" />
+ % end
+ </span>
</li>
% }
</ul>
% };
</fieldset>
-</form>
+</div>
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
new file mode 100644
index 0000000..5c53c83
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/revoke-token.html.ep
@@ -0,0 +1,13 @@
+% extends 'settings', title => 'KorAP: ' . loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+<p><%== loc('Auth_oauthRevokeToken', client_name => param('name')) %></p>
+
+%= form_for url_for('oauth-revoke-token-delete', client_id => stash('client_id'))->query('_method' => 'DELETE'), id => 'revoke-token', class => 'form-table', method => "POST", begin
+ %= csrf_field
+ %= hidden_field 'name' => param('name')
+ %= hidden_field 'token' => param('token')
+ <input type="submit" value="<%= loc 'Auth_revoke' %>" />
+ %= link_to 'Abort' => url_for('oauth-tokens', client_id => stash('client_id')) => {} => (class => 'form-button button-abort')
+% end
diff --git a/t/plugin/auth-oauth.t b/t/plugin/auth-oauth.t
index 4f0d67a..4bdfe54 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -467,8 +467,8 @@
;
$t->get_ok('/settings/oauth')
- ->text_is('form.form-table legend', 'Register new client application')
- ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+ ->text_is('.form-table legend', 'Register new client application')
+ ->attr_is('.oauth-register','action', '/settings/oauth/register')
->text_is('ul.client-list > li > span.client-name a', 'MyApp')
->text_is('ul.client-list > li > span.client-desc', 'This is my application')
->text_is('ul.client-list > li > span.client-url a', '')
@@ -476,16 +476,16 @@
$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
->status_is(200)
- ->text_is('form ul.client-list > li.client > span.client-name', 'MyApp')
- ->text_is('form ul.client-list > li.client > span.client-desc', 'This is my application')
+ ->text_is('ul.client-list > li.client > span.client-name', 'MyApp')
+ ->text_is('ul.client-list > li.client > span.client-desc', 'This is my application')
->text_is('a.client-unregister', 'Unregister')
->attr_is('a.client-unregister', 'href', '/settings/oauth/unregister/fCBbQkA2NDA3MzM1Yw==?name=MyApp')
;
$csrf = $t->get_ok('/settings/oauth/unregister/fCBbQkA2NDA3MzM1Yw==?name=MyApp')
->content_like(qr!Do you really want to unregister \<span class="client-name"\>MyApp\<\/span\>?!)
- ->attr_is('form.form-table input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
- ->attr_is('form.form-table input[name=client-name]', 'value', 'MyApp')
+ ->attr_is('.form-table input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
+ ->attr_is('.form-table input[name=client-name]', 'value', 'MyApp')
->tx->res->dom->at('input[name="csrf_token"]')
->attr('value')
;
@@ -500,8 +500,8 @@
;
$t->get_ok('/settings/oauth')
- ->text_is('form.form-table legend', 'Register new client application')
- ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+ ->text_is('.form-table legend', 'Register new client application')
+ ->attr_is('.oauth-register','action', '/settings/oauth/register')
->element_exists('ul.client-list > li')
->text_is('div.notify', 'Unknown client with xxxx==.')
;
@@ -516,8 +516,8 @@
;
$t->get_ok('/settings/oauth')
- ->text_is('form.form-table legend', 'Register new client application')
- ->attr_is('form.oauth-register','action', '/settings/oauth/register')
+ ->text_is('.form-table legend', 'Register new client application')
+ ->attr_is('.oauth-register','action', '/settings/oauth/register')
->element_exists_not('ul.client-list > li')
->text_is('div.notify-success', 'Successfully deleted MyApp')
;
@@ -539,10 +539,10 @@
->text_is('.client-name', 'MyApp2')
->text_is('.client-desc', 'This is my application')
->text_is('.client-issue-token', 'IssueToken')
- ->attr_is('.client-issue-token', 'href', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
+ ->attr_is('.client-issue-token', 'href', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/issue?name=MyApp2')
;
-$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
+$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/issue?name=MyApp2')
->status_is(200)
->attr_is('#issue-token','action', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token')
->attr_is('input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
@@ -560,10 +560,71 @@
->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
;
+
$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
->text_is('div.notify-success', 'New access token created')
;
+$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ->status_is(200)
+ ->attr_is('form.token-revoke', 'action', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/revoke')
+ ->attr_is('form.token-revoke input[name=token]', 'value', 'jhkhkjhk_hjgjsfz67i')
+ ->attr_is('form.token-revoke input[name=name]', 'value', 'MyApp2')
+ ->tx->res->dom->at('input[name="csrf_token"]')
+ ->attr('value')
+ ;
+
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token/revoke' => form => {
+ csrf_token => $csrf,
+ name => 'MyApp2',
+ token => 'jhkhkjhk_hjgjsfz67i'
+})
+ ->status_is(200)
+ ->attr_is('form#revoke-token','action','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE')
+ ->attr_is('form#revoke-token','method','POST')
+ ->attr_is('form#revoke-token input[name=token]','value','jhkhkjhk_hjgjsfz67i')
+;
+
+
+# CSRF missing
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE' => form => {
+ name => 'MyApp2',
+ token => 'jhkhkjhk_hjgjsfz67i'
+})->status_is(302)
+ ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ;
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ->element_exists_not('div.notify-success')
+ ->text_is('div.notify-error', 'Bad CSRF token')
+ ;
+
+# Token missing
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE' => form => {
+ name => 'MyApp2',
+ csrf_token => $csrf,
+})->status_is(302)
+ ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ;
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ->element_exists_not('div.notify-success')
+ ->text_is('div.notify-error', 'Some fields are invalid')
+ ;
+
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?_method=DELETE' => form => {
+ name => 'MyApp2',
+ csrf_token => $csrf,
+ token => 'jhkhkjhk_hjgjsfz67i'
+})->status_is(302)
+ ->header_is('Location','/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ;
+
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ->element_exists_not('div.notify-error')
+ ->text_is('div.notify-success', 'Token was revoked successfully')
+ ;
done_testing;
__END__
diff --git a/t/server/mock.pl b/t/server/mock.pl
index c3808f6..6c9318f 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -771,6 +771,16 @@
);
};
+post '/v1.0/oauth2/revoke/super' => sub {
+ my $c = shift;
+
+ my $s_client_id = $c->param('super_client_id');
+ my $s_client_secret = $c->param('super_client_secret');
+ my $token = $c->param('token');
+
+ return $c->render(text => 'SUCCESS');
+};
+
app->start;