]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 1354589 - Implement OAuth2 on BMO
authordklawren <dklawren@users.noreply.github.com>
Tue, 20 Nov 2018 21:01:38 +0000 (16:01 -0500)
committerDylan William Hardison <dylan@hardison.net>
Tue, 20 Nov 2018 21:01:38 +0000 (16:01 -0500)
Bugzilla.pm
Bugzilla/Quantum.pm
Bugzilla/Quantum/OAuth2.pm [new file with mode: 0644]
Bugzilla/Test/Util.pm
t/mojo-oauth2.t [new file with mode: 0644]
template/en/default/account/auth/confirm_scopes.html.tmpl [new file with mode: 0644]
template/en/default/global/messages.html.tmpl

index b03d964628b66c11583b7809bc4c26f2d1a343ca..6b9755b28e649a7c9e0072721ae715798446dde9 100644 (file)
@@ -379,6 +379,14 @@ sub login {
         $class->user->update_last_seen_date();
     }
 
+    # If Mojo native app is requesting login, we need to possibly redirect
+    my $C = $Bugzilla::Quantum::CGI::C;
+    if ($C->session->{override_login_target}) {
+      my $mojo_url = Mojo::URL->new($C->session->{override_login_target});
+      $mojo_url->query($C->session->{cgi_params});
+      $C->redirect_to($mojo_url);
+    }
+
     return $class->user;
 }
 
index e33e01d611d170cd2958509c5fe3cefc42c0a479..3e765008d42eb26413ee50497ed42b1be73349a2 100644 (file)
@@ -21,6 +21,7 @@ use Bugzilla::Extension             ();
 use Bugzilla::Install::Requirements ();
 use Bugzilla::Logging;
 use Bugzilla::Quantum::CGI;
+use Bugzilla::Quantum::OAuth2 qw(oauth2);
 use Bugzilla::Quantum::SES;
 use Bugzilla::Quantum::Home;
 use Bugzilla::Quantum::Static;
@@ -46,6 +47,9 @@ sub startup {
   $self->plugin('ForwardedFor') if Bugzilla->has_feature('better_xff');
   $self->plugin('Bugzilla::Quantum::Plugin::Helpers');
 
+  # OAuth2 Support
+  oauth2($self);
+
   # hypnotoad is weird and doesn't look for MOJO_LISTEN itself.
   $self->config(
     hypnotoad => {
diff --git a/Bugzilla/Quantum/OAuth2.pm b/Bugzilla/Quantum/OAuth2.pm
new file mode 100644 (file)
index 0000000..87d1aaf
--- /dev/null
@@ -0,0 +1,424 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Quantum::OAuth2;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla;
+use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Logging;
+use Bugzilla::Util;
+use Bugzilla::Token;
+
+use DateTime;
+
+use Mojo::Util qw(secure_compare);
+
+use base qw(Exporter);
+our @EXPORT_OK = qw(oauth2);
+
+sub oauth2 {
+  my ($self) = @_;
+
+  $self->plugin(
+    'OAuth2::Server' => {
+      login_resource_owner      => \&_resource_owner_logged_in,
+      confirm_by_resource_owner => \&_resource_owner_confirm_scopes,
+      verify_client             => \&_verify_client,
+      store_auth_code           => \&_store_auth_code,
+      verify_auth_code          => \&_verify_auth_code,
+      store_access_token        => \&_store_access_token,
+      verify_access_token       => \&_verify_access_token,
+    }
+  );
+
+  $self->helper(
+    'bugzilla.oauth' => sub {
+        my ($c, @scopes) = @_;
+
+        my $oauth = $c->oauth(@scopes);
+
+        if ($oauth && $oauth->{user_id}) {
+          my $user = Bugzilla::User->check({id => $oauth->{user_id}, cache => 1});
+          Bugzilla->set_user($user);
+          return $user;
+        }
+
+        return undef;
+    }
+  );
+
+  return 1;
+}
+
+sub _resource_owner_logged_in {
+  my (%args) = @_;
+  my $c = $args{mojo_controller};
+
+  $c->session->{override_login_target} = $c->url_for('current');
+  $c->session->{cgi_params} = $c->req->params->to_hash;
+
+  $c->bugzilla->login(LOGIN_REQUIRED) || return;
+
+  delete $c->session->{override_login_target};
+  delete $c->session->{cgi_params};
+
+  return 1;
+}
+
+sub _resource_owner_confirm_scopes {
+  my (%args) = @_;
+  my ($c, $client_id, $scopes_ref)
+    = @args{qw/ mojo_controller client_id scopes /};
+
+  my $is_allowed = $c->param("oauth_confirm_${client_id}");
+
+  # if user hasn't yet allowed the client access, or if they denied
+  # access last time, we check [again] with the user for access
+  if (!defined $is_allowed) {
+    my $client
+      = Bugzilla->dbh->selectrow_hashref(
+      'SELECT * FROM oauth2_client WHERE id = ?',
+      undef, $client_id);
+    my $vars = {
+      client => $client,
+      scopes => $scopes_ref,
+      token  => issue_session_token('oauth_confirm_scopes')
+    };
+    $c->stash(%$vars);
+    $c->render(
+      template => 'account/auth/confirm_scopes',
+      handler  => 'bugzilla'
+    );
+    return undef;
+  }
+
+  my $token = $c->param('token');
+  check_token_data($token, 'oauth_confirm_scopes');
+  delete_token($token);
+
+  return $is_allowed;
+}
+
+sub _verify_client {
+  my (%args) = @_;
+  my ($c, $client_id, $scopes_ref)
+    = @args{qw/ mojo_controller client_id scopes /};
+  my $dbh = Bugzilla->dbh;
+
+  if (!@{$scopes_ref}) {
+    INFO('Client did not provide scopes');
+    return (0, 'invalid_scope');
+  }
+
+  if (
+    my $client_data = $dbh->selectrow_hashref(
+      'SELECT * FROM oauth2_client WHERE id = ?',
+      undef, $client_id
+    )
+    )
+  {
+    if (!$client_data->{active}) {
+      INFO("Client ($client_id) is not active");
+      return (0, 'unauthorized_client');
+    }
+
+    foreach my $rqd_scope (@{$scopes_ref}) {
+      my $scope_allowed = $dbh->selectrow_array(
+        'SELECT allowed FROM oauth2_client_scope
+                JOIN oauth2_scope ON oauth2_scope.id = oauth2_client_scope.scope_id
+          WHERE client_id = ? AND oauth2_scope.description = ?', undef,
+        $client_id, $rqd_scope
+      );
+      if (defined $scope_allowed) {
+        if (!$scope_allowed) {
+          INFO("Client disallowed scope ($rqd_scope)");
+          return (0, 'access_denied');
+        }
+      }
+      else {
+        INFO("Client lacks scope ($rqd_scope)");
+        return (0, 'invalid_scope');
+      }
+    }
+
+    return (1);
+  }
+
+  INFO("Client ($client_id) does not exist");
+  return (0, 'unauthorized_client');
+}
+
+sub _store_auth_code {
+  my (%args) = @_;
+  my ($c, $auth_code, $client_id, $expires_in, $uri, $scopes_ref)
+    = @args{
+    qw/ mojo_controller auth_code client_id expires_in redirect_uri scopes /};
+  my $dbh = Bugzilla->dbh;
+
+  my $user_id = Bugzilla->user->id;
+
+  $dbh->do(
+    'INSERT INTO oauth2_auth_code VALUES (?, ?, ?, ?, ?, 0)',
+    undef,
+    $auth_code,
+    $client_id,
+    Bugzilla->user->id,
+    DateTime->from_epoch(epoch => time + $expires_in),
+    $uri
+  );
+
+  foreach my $rqd_scope (@{$scopes_ref}) {
+    my $scope_id
+      = $dbh->selectrow_array(
+      'SELECT id FROM oauth2_scope WHERE description = ?',
+      undef, $rqd_scope);
+    if ($scope_id) {
+      $dbh->do('INSERT INTO oauth2_auth_code_scope VALUES (?, ?, 1)',
+        undef, $auth_code, $scope_id);
+    }
+    else {
+      ERROR("Unknown scope ($rqd_scope) in _store_auth_code");
+    }
+  }
+
+  return;
+}
+
+sub _verify_auth_code {
+  my (%args) = @_;
+  my ($c, $client_id, $client_secret, $auth_code, $uri)
+    = @args{
+    qw/ mojo_controller client_id client_secret auth_code redirect_uri /};
+  my $dbh = Bugzilla->dbh;
+
+  my $client_data
+    = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE id = ?',
+    undef, $client_id);
+  $client_data || return (0, 'unauthorized_client');
+
+  my $auth_code_data = $dbh->selectrow_hashref(
+    'SELECT expires, verified, redirect_uri, user_id FROM oauth2_auth_code WHERE client_id = ? AND auth_code = ?',
+    undef, $client_id, $auth_code
+  );
+
+  if (!$auth_code_data
+    or $auth_code_data->{verified}
+    or ($uri ne $auth_code_data->{redirect_uri})
+    or (datetime_from($auth_code_data->{expires})->epoch <= time)
+    or !secure_compare($client_secret, $client_data->{secret}))
+  {
+    INFO('Auth code does not exist') if !$auth_code;
+    INFO('Client secret does not match')
+      if !secure_compare($client_secret, $client_data->{secret});
+
+    if ($auth_code) {
+      INFO('Client secret does not match')
+        if ($uri && $auth_code_data->{redirect_uri} ne $uri);
+      INFO('Auth code expired') if ($auth_code_data->{expires} <= time);
+
+      if ($auth_code_data->{verified}) {
+
+        # the auth code has been used before - we must revoke the auth code
+        # and any associated access tokens (same client_id and user_id)
+        INFO( 'Auth code already used to get access token, '
+            . 'revoking all associated access tokens');
+        $dbh->do('DELETE FROM oauth2_auth_code WHERE auth_code = ?',
+          undef, $auth_code);
+        $dbh->do(
+          'DELETE FROM oauth2_access_token WHERE client_id = ? AND user_id = ?',
+          undef, $client_id, $auth_code_data->{user_id}
+        );
+      }
+    }
+
+    return (0, 'invalid_grant');
+  }
+
+  $dbh->do('UPDATE oauth2_auth_code SET verified = 1 WHERE auth_code = ?',
+    undef, $auth_code);
+
+  # scopes are those that were requested in the authorization request, not
+  # those stored in the client (i.e. what the auth request restriced scopes
+  # to and not everything the client is capable of)
+  my $scope_descriptions = $dbh->selectcol_arrayref(
+    'SELECT oauth2_scope.description FROM oauth2_scope
+            JOIN oauth2_auth_code_scope ON oauth2_scope.id = oauth2_auth_code_scope.scope_id
+      WHERE oauth2_auth_code_scope.auth_code = ?', undef, $auth_code
+  );
+
+  my %scope = map { $_ => 1 } @{$scope_descriptions};
+
+  return ($client_id, undef, {%scope}, $auth_code_data->{user_id});
+}
+
+sub _store_access_token {
+  my (%args) = @_;
+  my ($c, $client, $auth_code, $access_token, $refresh_token, $expires_in,
+    $scopes, $old_refresh_token)
+    = @args{
+    qw/ mojo_controller client_id auth_code access_token refresh_token expires_in scopes old_refresh_token /
+    };
+  my $dbh = Bugzilla->dbh;
+  my ($user_id);
+
+  if (!defined $auth_code && $old_refresh_token) {
+    # must have generated an access token via a refresh token so revoke the
+    # old access token and refresh token (also copy required data if missing)
+    my $prev_refresh_token
+      = $dbh->selectrow_hashref(
+      'SELECT * FROM oauth2_refresh_token WHERE refresh_token = ?',
+      undef, $old_refresh_token);
+    my $prev_access_token
+      = $dbh->selectrow_hashref(
+      'SELECT * FROM oauth2_access_token WHERE access_token = ?',
+      undef, $prev_refresh_token->{access_token});
+
+    # access tokens can be revoked, whilst refresh tokens can remain so we
+    # need to get the data from the refresh token as the access token may
+    # no longer exist at the point that the refresh token is used
+    my $scope_descriptions = $dbh->selectall_array(
+      'SELECT oauth2_scope.description FROM oauth2_scope
+              JOIN oauth2_access_token_scope ON scope.id = oauth2_access_token_scope.scope_id
+        WHERE access_token = ?', undef, $old_refresh_token
+    );
+    $scopes //= map { $_ => 1 } @{ $scope_descriptions };
+
+    $user_id = $prev_refresh_token->{user_id};
+  }
+  else {
+    $user_id
+      = $dbh->selectrow_array(
+      'SELECT user_id FROM oauth2_auth_code WHERE auth_code = ?',
+      undef, $auth_code);
+  }
+
+  if (ref $client) {
+    $scopes //= $client->{scope};
+    $user_id //= $client->{user_id};
+    $client = $client->{client_id};
+  }
+
+  foreach my $token_type (qw/ access refresh /) {
+    my $table = "oauth2_${token_type}_token";
+
+    # if the client has en existing access/refresh token we need to revoke it
+    $dbh->do("DELETE FROM $table WHERE client_id = ? AND user_id = ?",
+      undef, $client, $user_id);
+  }
+
+  $dbh->do(
+    'INSERT INTO oauth2_access_token VALUES (?, ?, ?, ?, ?)', undef,
+    $access_token,                                            $refresh_token,
+    $client,                                                  $user_id,
+    DateTime->from_epoch(epoch => time + $expires_in)
+  );
+
+  $dbh->do('INSERT INTO oauth2_refresh_token VALUES (?, ?, ?, ?)',
+    undef, $refresh_token, $access_token, $client, $user_id);
+
+  foreach my $rqd_scope (keys %{$scopes}) {
+    my $scope_id
+      = $dbh->selectrow_array(
+      'SELECT id FROM oauth2_scope WHERE description = ?',
+      undef, $rqd_scope);
+    if ($scope_id) {
+      foreach my $related (qw/ access_token refresh_token /) {
+        my $table = "oauth2_${related}_scope";
+        $dbh->do(
+          "INSERT INTO $table VALUES (?, ?, ?)",
+          undef,
+          $related eq 'access_token' ? $access_token : $refresh_token,
+          $scope_id,
+          $scopes->{$rqd_scope}
+        );
+      }
+    }
+    else {
+      ERROR("Unknown scope ($rqd_scope) in _store_access_token");
+    }
+  }
+
+  return;
+}
+
+sub _verify_access_token {
+  my (%args) = @_;
+  my ($c, $access_token, $scopes_ref)
+    = @args{qw/ mojo_controller access_token scope /};
+  my $dbh = Bugzilla->dbh;
+
+  if (
+    my $refresh_token_data = $dbh->selectrow_hashref(
+      'SELECT * FROM oauth2_refresh_token WHERE access_token = ?', undef,
+      $access_token
+    )
+    )
+  {
+    foreach my $scope (@{$scopes_ref // []}) {
+      my $scope_allowed = $dbh->selectrow_array(
+        'SELECT allowed FROM oauth2_refresh_token_scope
+                JOIN oauth2_scope ON oauth2_scope.id = oauth2_refresh_token_scope.scope_id
+          WHERE refresh_token = ? AND oauth2_scope.description = ?', undef,
+        $access_token, $scope
+      );
+
+      if (!defined $scope_allowed || !$scope_allowed) {
+        INFO("Refresh token doesn't have scope ($scope)");
+        return (0, 'invalid_grant');
+      }
+    }
+
+    return {
+      client_id => $refresh_token_data->{client_id},
+      user_id   => $refresh_token_data->{user_id},
+    };
+  }
+  elsif (
+    my $access_token_data = $dbh->selectrow_hashref(
+      'SELECT expires, client_id, user_id FROM oauth2_access_token WHERE access_token = ?',
+      undef,
+      $access_token
+    )
+    )
+  {
+    if (datetime_from($access_token_data->{expires})->epoch <= time) {
+      INFO('Access token has expired');
+      $dbh->do('DELETE FROM oauth2_access_token WHERE access_token = ?',
+        undef, $access_token);
+      return (0, 'invalid_grant');
+    }
+
+    foreach my $scope (@{$scopes_ref // []}) {
+      my $scope_allowed = $dbh->selectrow_array(
+        'SELECT allowed FROM oauth2_access_token_scope
+                JOIN oauth2_scope ON oauth2_access_token_scope.scope_id = oauth2_scope.id
+          WHERE scope.description = ? AND access_token = ?', undef, $scope,
+        $access_token
+      );
+      if (!defined $scope_allowed || !$scope_allowed) {
+        INFO("Access token doesn't have scope ($scope)");
+        return (0, 'invalid_grant');
+      }
+    }
+
+    return {
+      client_id => $access_token_data->{client_id},
+      user_id   => $access_token_data->{user_id},
+    };
+  }
+  else {
+    INFO('Access token does not exist');
+    return (0, 'invalid_grant');
+  }
+}
+
+1;
index 9fbc151f74a5f46d892fd196ea13f7da2ff275ad..b8485f29e4ce63e153c8ec223da18ac4ac77d7ff 100644 (file)
@@ -12,10 +12,12 @@ use strict;
 use warnings;
 
 use base qw(Exporter);
-our @EXPORT = qw(create_user issue_api_key mock_useragent_tx);
+our @EXPORT
+  = qw(create_user create_oauth_client issue_api_key mock_useragent_tx);
 
 use Bugzilla::User;
 use Bugzilla::User::APIKey;
+use Bugzilla::Util qw(generate_random_password);
 use Mojo::Message::Response;
 use Test2::Tools::Mock qw(mock);
 
@@ -32,6 +34,33 @@ sub create_user {
     });
 }
 
+sub create_oauth_client {
+  my ($description, $scopes) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  my $id     = generate_random_password(20);
+  my $secret = generate_random_password(40);
+
+  $dbh->do(
+    'INSERT INTO oauth2_client (id, description, secret) VALUES (?, ?, ?)',
+    undef, $id, $description, $secret);
+
+  foreach my $scope (@{$scopes}) {
+    my $scope_id
+      = $dbh->selectrow_array('SELECT id FROM oauth2_scope WHERE description = ?',
+      undef, $scope);
+    if (!$scope_id) {
+      die "Scope $scope not found";
+    }
+    $dbh->do(
+      'INSERT INTO oauth2_client_scope (client_id, scope_id, allowed) VALUES (?, ?, 1)',
+      undef, $id, $scope_id
+    );
+  }
+
+  return $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE id = ?', undef, $id);
+}
+
 sub issue_api_key {
     my ($login, $given_api_key) = @_;
     my $user = Bugzilla::User->check({ name => $login });
diff --git a/t/mojo-oauth2.t b/t/mojo-oauth2.t
new file mode 100644 (file)
index 0000000..904ee81
--- /dev/null
@@ -0,0 +1,177 @@
+#!/usr/bin/perl
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+use strict;
+use warnings;
+use 5.10.1;
+use lib qw( . lib local/lib/perl5 );
+
+BEGIN {
+  $ENV{LOG4PERL_CONFIG_FILE}     = 'log4perl-t.conf';
+  $ENV{BUGZILLA_DISABLE_HOSTAGE} = 1;
+}
+
+use Bugzilla::Test::MockDB;
+use Bugzilla::Test::MockParams (password_complexity => 'no_constraints');
+use Bugzilla::Test::Util qw(create_user create_oauth_client);
+
+use Test2::V0;
+use Test::Mojo;
+
+my $oauth_login    = 'oauth@mozilla.bugs';
+my $oauth_password = 'password123456789!';
+my $referer        = Bugzilla->localconfig->{urlbase};
+my $stash          = {};
+
+# Create user to use as OAuth2 resource owner
+create_user($oauth_login, $oauth_password);
+
+# Create a new OAuth2 client used for testing
+my $oauth_client = create_oauth_client('Shiny New OAuth Client', ['user:read']);
+ok $oauth_client->{id}, 'New client id (' . $oauth_client->{id} . ')';
+ok $oauth_client->{secret}, 'New client secret (' . $oauth_client->{secret} . ')';
+
+my $t = Test::Mojo->new('Bugzilla::Quantum');
+
+# Allow 1 redirect max
+$t->ua->max_redirects(1);
+
+# Custom routes and hooks required to support running the tests
+_setup_routes($t->app->routes);
+$t->app->hook(after_dispatch => sub { $stash = shift->stash });
+
+# User should be logged out so /oauth/authorize should redirect to a login screen
+$t->get_ok(
+  '/oauth/authorize' => {Referer => $referer} => form => {
+    client_id     => $oauth_client->{id},
+    response_type => 'code',
+    state         => 'state',
+    scope         => 'user:read',
+    redirect_uri  => '/oauth/redirect'
+  }
+)->status_is(200)
+  ->element_exists('div.login-form input[name=Bugzilla_login_token]')
+  ->text_is('html head title' => 'Log in to Bugzilla');
+
+# Login the user in using the resource owner username and password
+# Once logged in, we should automatically be redirected to the confirm
+# scopes page.
+$t->post_ok(
+  '/login' => {Referer => $referer} => form => {
+    Bugzilla_login         => $oauth_login,
+    Bugzilla_password      => $oauth_password,
+    Bugzilla_restrictlogin => 1,
+    GoAheadAndLogIn        => 1,
+    client_id              => $oauth_client->{id},
+    response_type          => 'code',
+    state                  => 'state',
+    scope                  => 'user:read',
+    redirect_uri           => '/oauth/redirect'
+  }
+)->status_is(200)->text_is('title' => 'Confirm OAuth2 Scopes');
+
+# Get the csrf token to allow submitting the scope confirmation form
+my $csrf_token = $t->tx->res->dom->at('input[name=token]')->val;
+ok $csrf_token, "Get csrf token ($csrf_token)";
+
+# Redirect and get the auth code needed for obtaining an access token
+# Once we accept the scopes requested, we should get redirected to the
+# URI specified in the redirect_uri value. In this case a simple text page.
+$t->get_ok(
+  '/oauth/authorize' => {Referer => $referer} => form => {
+    "oauth_confirm_" . $oauth_client->{id} => 1,
+    token                                  => $csrf_token,
+    client_id                              => $oauth_client->{id},
+    response_type                          => 'code',
+    state                                  => 'state',
+    scope                                  => 'user:read',
+    redirect_uri                           => '/oauth/redirect'
+  }
+)->status_is(200)->content_is('Redirect Success!');
+
+# The redirect page (normally an external site associated with the
+# OAuth2 client) should verify the state token and also get a temporary
+# auth code that will be used to request an access token.
+my $state = $stash->{state};
+ok $state eq 'state', "State was returned correctly";
+my $auth_code = $stash->{auth_code};
+ok $auth_code, "Get auth code ($auth_code)";
+
+# Contact the OAuth2 server using the auth code to obtain an access token
+# This happens as a backend POST the the server and is not visible to the
+# end user.
+$t->post_ok(
+  '/oauth/access_token' => {Referer => $referer} => form => {
+    client_id     => $oauth_client->{id},
+    client_secret => $oauth_client->{secret},
+    code          => $auth_code,
+    grant_type    => 'authorization_code',
+    redirect_uri  => '/oauth/redirect',
+  }
+)->status_is(200)->json_has('access_token', 'Has access token')
+  ->json_has('refresh_token', 'Has refresh token')
+  ->json_has('token_type',    'Has token type');
+
+my $access_data = $t->tx->res->json;
+
+# Using the access token (bearer) we are able to authenticate for an API call.
+
+# 1. Access API unauthenticated and should generate a login_required error
+$t->get_ok('/oauth/whoami')->status_is(401)
+  ->json_is('/error' => 'login_required');
+
+# 2. Passing a Bearer header containing the access token, the server should
+# allow us to get data about our user
+$t->get_ok('/oauth/whoami' =>
+    {Authorization => 'Bearer ' . $access_data->{access_token}})
+  ->status_is(200)->json_is('/name' => $oauth_login);
+
+done_testing;
+
+sub _setup_routes {
+  my $r = shift;
+
+  # Add /oauth/redirect route for checking final redirection
+  $r->get(
+    '/oauth/redirect' => sub {
+      my $c = shift;
+      $c->stash(state => $c->param('state'), auth_code => $c->param('code'));
+      $c->render(status => 200, text => 'Redirect Success!');
+      return;
+    }
+  );
+
+  # API call for testing oauth authentication
+  $r->get(
+    '/oauth/whoami' => sub {
+      my $c = shift;
+
+      my $user = $c->bugzilla->oauth('user:read');
+
+      if ($user && $user->id) {
+        $c->render(
+          status => 200,
+          json   => {
+            id       => $user->id,
+            name     => $user->login,
+            realname => $user->name
+          }
+        );
+      }
+      else {
+        $c->render(
+          status => 401,
+          json   => {
+            error => 'login_required',
+            error_description =>
+              'You must log in before using this part of Bugzilla.'
+          }
+        );
+      }
+    }
+  );
+}
diff --git a/template/en/default/account/auth/confirm_scopes.html.tmpl b/template/en/default/account/auth/confirm_scopes.html.tmpl
new file mode 100644 (file)
index 0000000..76b51e1
--- /dev/null
@@ -0,0 +1,44 @@
+[%# This Source Code Form is subject to the terms of the Mozilla Public
+  # License, v. 2.0. If a copy of the MPL was not distributed with this
+  # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+  #
+  # This Source Code Form is "Incompatible With Secondary Licenses", as
+  # defined by the Mozilla Public License, v. 2.0.
+  #%]
+
+[% PROCESS global/header.html.tmpl
+   title = "Confirm OAuth2 Scopes" %]
+
+<h1>[% title FILTER html %] </h1>
+<p>
+  A third-party website <em>[% client.description FILTER html %]</em> would like to have
+  the following access to your [% terms.Bugzilla %] account.
+</p>
+
+<p>
+Scopes:
+<ul>
+  [% FOREACH scope = scopes %]
+    <li>
+      [% scope FILTER html %]
+    </li>
+  [% END %]
+</ul>
+</p>
+
+<p>Do you want this website to have the above access to your [% terms.Bugzilla %]
+  account?</p>
+
+<div>
+  <form action="/oauth/authorize" method="get">
+    <input type="hidden" name="oauth_confirm_[% client.id FILTER html %]" value="1">
+    <input type="hidden" name="token" value="[% token FILTER html %]">
+    <input type="submit" name="submit" value="Accept">
+    [% FOREACH field = c.req.params.names %]
+      <input type="hidden" name="[% field FILTER html %]"
+             value="[% c.param(field) FILTER html_linebreak %]">
+    [% END %]
+  </form>
+</div>
+
+[% PROCESS global/footer.html.tmpl %]
index 0a7b4ea296e0dbdeec63860cd71617a5baebffe6..8a9a368bdb381cc2d781d4747a90c7e8823fbed4 100644 (file)
   [% ELSIF message_tag == "install_workflow_init" %]
     Setting up the default status workflow...
 
+  [% ELSIF message_tag == 'oauth_client_created' %]
+    The OAuth2 client <em>[% client.description FILTER html %]</em> has been created.
+
+  [% ELSIF message_tag == 'oauth_client_updated' %]
+    The OAuth2 client <em>[% client.description FILTER html %]</em> has been updated.
+
+  [% ELSIF message_tag == "oauth_client_deleted" %]
+    The OAuth2 client <em>[% client.description FILTER html %]</em> has been deleted.
+
   [% ELSIF message_tag == "product_created" %]
     [% title = "Product Created" %]
     The product <em>[% product.name FILTER html %]</em> has been created. You will need to