Issue a new token for a public client
Change-Id: Id44501d46aff4fd540339c0b2901879ab8a77734
diff --git a/Changes b/Changes
index b5a4db7..2b2bfa5 100755
--- a/Changes
+++ b/Changes
@@ -1,8 +1,9 @@
-0.42 2021-03-08
+0.42 2021-03-15
- Added GitHub based CI for perl.
- Added further methods for communicating JSON Files
with the server to the APIs (lerepp).
- Remove ruby-sass requirement (fixes #123).
+ - Added support to issue new OAuth2 tokens.
0.41 2021-03-01
- Introduce CORS headers to the proxy.
diff --git a/dev/scss/base/base.scss b/dev/scss/base/base.scss
index be195e3..c245cd9 100644
--- a/dev/scss/base/base.scss
+++ b/dev/scss/base/base.scss
@@ -44,6 +44,10 @@
}
a {
+ &:visited {
+ color: $darkest-orange;
+ }
+
&:link {
text-decoration: none;
color: $dark-orange;
diff --git a/dev/scss/main/buttongroup.scss b/dev/scss/main/buttongroup.scss
index db3462a..6a89297 100644
--- a/dev/scss/main/buttongroup.scss
+++ b/dev/scss/main/buttongroup.scss
@@ -5,6 +5,7 @@
* Define the base layout of horizontal button groups
*/
.button-group {
+ font-size: 0;
> span {
cursor: pointer;
}
diff --git a/lib/Kalamar/Plugin/Auth.pm b/lib/Kalamar/Plugin/Auth.pm
index 885be07..380593d 100644
--- a/lib/Kalamar/Plugin/Auth.pm
+++ b/lib/Kalamar/Plugin/Auth.pm
@@ -76,7 +76,8 @@
registerFail => 'Registrierung fehlgeschlagen',
oauthSettings => 'OAuth',
oauthUnregister => 'Möchten sie <span class="client-name"><%= $clientName %></span> wirklich löschen?',
- loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.'
+ loginHint => 'Möglicherweise müssen sie sich zunächst einloggen.',
+ oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $clientName %></span>',
},
-en => {
loginSuccess => 'Login successful',
@@ -103,7 +104,8 @@
registerFail => 'Registration denied',
oauthSettings => 'OAuth',
oauthUnregister => 'Do you really want to unregister <span class="client-name"><%= $clientName %></span>?',
- loginHint => 'Maybe you need to log in first?'
+ loginHint => 'Maybe you need to log in first?',
+ oauthIssueToken => 'Erzeuge einen neuen Token für <span class="client-name"><%= $clientName %></span>',
}
}
}
@@ -732,7 +734,8 @@
else {
$c->notify(warn => $c->loc('Auth_paramError'));
};
- return $c->render(template => 'auth/clients')
+ # return $c->redirect_to('oauth-settings');
+ return $c->render(template => 'auth/clients');
};
# Wait for async result
@@ -826,7 +829,7 @@
else {
$c->notify(warn => $c->loc('Auth_paramError'));
};
- return $c->render(template => 'auth/clients')
+ return $c->redirect_to('oauth-settings');
};
my $client_id = $v->param('client-id');
@@ -906,7 +909,9 @@
sub {
# return $c->render(text => 'hui');
-
+ # TODO:
+ # This would better be a redirect to the client page, but in case there is a client_secret
+ # this wouldn't work (unless we flash that).
return $c->render(template => 'auth/client')
}
);
@@ -915,6 +920,158 @@
}
)->name('oauth-tokens');
};
+
+
+ # Show information of a client
+ $r->get('/settings/oauth/client/:client_id/token')->to(
+ cb => sub {
+ shift->render(template => 'auth/issue-token');
+ }
+ )->name('oauth-issue-token');
+
+
+ $r->post('/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('client-id', 'trim')->size(3, 255);
+ $v->optional('client-secret');
+ $v->required('name', 'trim');
+
+ # Render with error
+ if ($v->has_error) {
+ if ($v->has_error('csrf_token')) {
+ $c->notify(error => $c->loc('Auth_csrfFail'));
+ }
+ else {
+ $c->notify(warn => $c->loc('Auth_paramError'));
+ };
+ return $c->redirect_to('oauth-settings')
+ };
+
+ # Get authorization token
+ state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/authorize');
+ my $client_id = $c->stash('client_id');
+ my $name = $v->param('name');
+ my $redirect_url = $c->url_for->query({name => $name});
+
+ return $c->korap_request(post => $r_url, {} => form => {
+ response_type => 'code',
+ client_id => $client_id,
+ redirect_uri => $redirect_url,
+ # TODO: State
+ })->then(
+ sub {
+ my $tx = shift;
+
+ # Strip the token from the location header of the fake redirect
+ # TODO: Alternatively redirect!
+ my ($code, $scope, $loc, $name);
+ foreach (@{$tx->redirects}) {
+ $loc = $_->res->headers->header('Location');
+ if (index($loc, 'code') > 0) {
+ my $q = Mojo::URL->new($loc)->query;
+ $code = $q->param('code');
+ $scope = $q->param('scope');
+ $name = $q->param('name');
+ last;
+ };
+ };
+
+ # Fine!
+ if ($code) {
+ return Mojo::Promise->resolve(
+ $client_id,
+ $redirect_url,
+ $code,
+ $scope,
+ $name
+ );
+ };
+ return Mojo::Promise->reject;
+ }
+ )->then(
+ sub {
+ my ($client_id, $redirect_url, $code, $scope, $name) = @_;
+
+ # Get OAuth access token
+ state $r_url = Mojo::URL->new($c->korap->api)->path('oauth2/token');
+ return $c->kalamar_ua->post_p($r_url, {} => form => {
+ client_id => $client_id,
+ # NO CLIENT_SECRET YET SUPPORTED
+ grant_type => 'authorization_code',
+ code => $code,
+ redirect_uri => $redirect_url
+ })->then(
+ sub {
+ my $tx = shift;
+ my $json = $tx->res->json;
+
+ if ($tx->res->is_error) {
+ $c->notify(error => 'Unable to fetch new token');
+ return Mojo::Promise->reject;
+ };
+
+ $c->notify(success => 'New access token created');
+
+ return $c->render(
+ template => 'auth/client',
+ client_name => $name,
+ %$json
+ );
+ }
+ )->catch(
+ sub {
+ my $err_msg = shift;
+
+ # Only raised in case of connection errors
+ if ($err_msg) {
+ $c->notify(error => { src => 'Backend' } => $err_msg)
+ };
+
+ $c->render(
+ status => 400,
+ template => 'failure'
+ );
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+
+ }
+ )->catch(
+ sub {
+ my $err_msg = shift;
+
+ # Only raised in case of connection errors
+ if ($err_msg) {
+ $c->notify(error => { src => 'Backend' } => $err_msg)
+ };
+
+ return $c->render(
+ status => 400,
+ template => 'failure'
+ );
+ }
+ )
+
+ # Start IOLoop
+ ->wait;
+
+ return 1;
+ }
+ )->name('oauth-issue-token-post');
}
# 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 38fa13d..8900777 100644
--- a/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/client.html.ep
@@ -16,15 +16,26 @@
% };
</li>
</ul>
- <span class="button-group button-panel"><%= link_to Unregister => url_for('oauth-unregister', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-unregister' ) %></span>
+ <span class="button-group button-panel">
+ %= link_to Unregister => url_for('oauth-unregister', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-unregister' )
+ %= link_to IssueToken => url_for('oauth-issue-token', client_id => stash('client_id'))->query('name' => stash('client_name')) => {} => ( class => 'client-issue-token' )
+ </span>
<p><%= loc 'Auth_clientType' %>: <tt><%= stash 'client_type' %></tt></p>
%= label_for 'client_id' => loc('Auth_clientID')
%= text_field 'client_id', stash('client_id'), readonly => 'readonly'
- % if (stash('client_type') ne 'PUBLIC') {
+ % if (stash('client_type') && stash('client_type') ne 'PUBLIC') {
<div>
%= label_for 'client_secret' => loc('Auth_clientSecret')
%= password_field 'client_secret', value => stash('client_secret'), readonly => 'readonly'
</div>
% };
+
+ % if (stash('access_token')) {
+ %= label_for 'access_token' => 'Access Token'
+ %= text_field 'access_token', stash('access_token'), readonly => 'readonly'
+ <p name="expires">Expires in: <tt><%= stash 'expires_in' %></tt></p>
+ <p name="scope">Scope: <tt><%= stash 'scope' %></tt></p>
+ <p name="type">Token-Type: <tt><%= stash 'token_type' %></tt></p>
+ % }
</fieldset>
</form>
diff --git a/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep b/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
new file mode 100644
index 0000000..ea1e66c
--- /dev/null
+++ b/lib/Kalamar/Plugin/Auth/templates/auth/issue-token.html.ep
@@ -0,0 +1,14 @@
+% extends 'settings', title => 'KorAP: ' . loc('Auth_oauthSettings'), page => 'oauth';
+
+%= page_title
+
+<p><%== loc('Auth_oauthIssueToken', clientName => param('name')) %></p>
+
+%= form_for 'oauth-issue-token-post', id => 'issue-token', class => 'form-table', begin
+ %= csrf_field
+ %= hidden_field 'client-id' => stash('client_id')
+ %= hidden_field 'name' => param('name')
+ %#= hidden_field 'client-secret'
+ <input type="submit" value="issue" />
+ %= 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 22054d2..adb7378 100644
--- a/t/plugin/auth-oauth.t
+++ b/t/plugin/auth-oauth.t
@@ -522,5 +522,47 @@
->text_is('div.notify-success', 'Successfully deleted MyApp')
;
+$t->post_ok('/settings/oauth/register' => form => {
+ name => 'MyApp2',
+ type => 'PUBLIC',
+ desc => 'This is my application',
+ csrf_token => $csrf
+})->status_is(200)
+ ->element_exists('div.notify-success')
+ ->text_is('legend', 'Client Credentials')
+ ->text_is('label[for=client_id]', 'ID of the client application')
+ ->element_exists('input[name=client_id][readonly][value]')
+ ->element_exists_not('input[name=client_secret][readonly][value]')
+ ;
+
+$t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==')
+ ->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')
+ ;
+
+$csrf = $t->get_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token?name=MyApp2')
+ ->status_is(200)
+ ->attr_is('#issue-token','action', '/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token')
+ ->attr_is('input[name=client-id]', 'value', 'fCBbQkA2NDA3MzM1Yw==')
+ ->attr_is('input[name=name]', 'value', 'MyApp2')
+ ->tx->res->dom->at('input[name="csrf_token"]')
+ ->attr('value')
+ ;
+
+$t->post_ok('/settings/oauth/client/fCBbQkA2NDA3MzM1Yw==/token' => form => {
+ csrf_token => $csrf,
+ name => 'MyApp2',
+ 'client-id' => 'fCBbQkA2NDA3MzM1Yw=='
+})
+ ->status_is(200)
+ ->attr_is('input[name=access_token]', 'value', 'jvgjbvjgzucgdwuiKHJK')
+ ->text_is('p[name=expires] tt', '31536000')
+ ->text_is('p[name=scope] tt', 'match_info search openid')
+ ->text_is('p[name=type] tt', 'Bearer')
+ ;
+
+
done_testing;
__END__
diff --git a/t/plugin/auth.t b/t/plugin/auth.t
index f5351de..bd9a3b1 100644
--- a/t/plugin/auth.t
+++ b/t/plugin/auth.t
@@ -196,8 +196,6 @@
->header_is('Location' => '/?q=Baum&ql=poliqarp');
-
-
done_testing;
__END__
diff --git a/t/server/mock.pl b/t/server/mock.pl
index 31bd494..21b938f 100644
--- a/t/server/mock.pl
+++ b/t/server/mock.pl
@@ -18,9 +18,11 @@
'access_token' => "4dcf8784ccfd26fac9bdb82778fe60e2",
'refresh_token' => "hlWci75xb8atDiq3924NUSvOdtAh7Nlf9z",
'access_token_2' => "abcde",
+ 'access_token_3' => 'jvgjbvjgzucgdwuiKHJK',
'refresh_token_2' => "fghijk",
'new_client_id' => 'fCBbQkA2NDA3MzM1Yw==',
'new_client_secret' => 'KUMaFxs6R1WGud4HM22w3HbmYKHMnNHIiLJ2ihaWtB4N5JxGzZgyqs5GTLutrORj',
+ 'auth_token_1' => 'mscajfdghnjdfshtkjcuynxahgz5il'
);
helper get_token => sub {
@@ -461,6 +463,21 @@
);
}
+ # Get auth_token_1
+ elsif ($grant_type eq 'authorization_code') {
+ if ($c->param('code') eq $tokens{auth_token_1}) {
+ return $c->render(
+ status => 200,
+ json => {
+ "access_token" => $tokens{access_token_3},
+ "expires_in" => 31536000,
+ "scope" => 'match_info search openid',
+ "token_type" => "Bearer"
+ }
+ );
+ };
+ }
+
# Unknown token grant
else {
return $c->render(
@@ -580,6 +597,23 @@
);
};
+post '/v1.0/oauth2/authorize' => sub {
+ my $c = shift;
+ my $type = $c->param('response_type');
+ my $client_id = $c->param('client_id');
+ my $redirect_uri = $c->param('redirect_uri');
+
+ if ($type eq 'code') {
+
+ return $c->redirect_to(
+ Mojo::URL->new($redirect_uri)->query({
+ code => $tokens{auth_token_1},
+ scope => 'match_info search openid'
+ })
+ );
+ }
+};
+
app->start;