use Bugzilla::Util;
use Bugzilla::Token;
use DateTime;
+use List::MoreUtils qw(any);
+use Mojo::URL;
use Mojo::Util qw(secure_compare);
+use Try::Tiny;
+
+use constant TOKEN_TYPE_AUTH => 0;
+use constant TOKEN_TYPE_ACCESS => 1;
+use constant TOKEN_TYPE_REFRESH => 2;
sub register {
my ($self, $app, $conf) = @_;
$conf->{verify_auth_code} = \&_verify_auth_code;
$conf->{store_access_token} = \&_store_access_token;
$conf->{verify_access_token} = \&_verify_access_token;
+ $conf->{jwt_secret} = Bugzilla->localconfig->{jwt_secret};
+ $conf->{jwt_claims} = sub {
+ my $args = shift;
+ if (!$args->{user_id}) {
+ return (user_id => Bugzilla->user->id);
+ }
+ };
$app->helper(
'bugzilla.oauth' => sub {
if ($oauth && $oauth->{user_id}) {
my $user = Bugzilla::User->check({id => $oauth->{user_id}, cache => 1});
+ return undef if !$user->is_enabled;
Bugzilla->set_user($user);
return $user;
}
$c->session->{override_login_target} = $c->url_for('current');
$c->session->{cgi_params} = $c->req->params->to_hash;
- $c->bugzilla->login(LOGIN_REQUIRED) || return;
+ $c->bugzilla->login(LOGIN_REQUIRED) || return undef;
delete $c->session->{override_login_target};
delete $c->session->{cgi_params};
# 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 = ?',
+ = Bugzilla->dbh->selectrow_hashref(
+ 'SELECT * FROM oauth2_client WHERE client_id = ?',
undef, $client_id);
my $vars = {
client => $client,
scopes => $scopes_ref,
token => scalar issue_session_token('oauth_confirm_scopes')
};
- $c->stash(%$vars);
+ $c->stash(%{$vars});
$c->render(template => 'account/auth/confirm_scopes', handler => 'bugzilla');
return undef;
}
sub _verify_client {
my (%args) = @_;
- my ($c, $client_id, $scopes_ref)
- = @args{qw/ mojo_controller client_id scopes /};
+ my ($c, $client_id, $scopes_ref, $redirect_uri)
+ = @args{qw/ mojo_controller client_id scopes redirect_uri /};
my $dbh = Bugzilla->dbh;
if (!@{$scopes_ref}) {
return (0, 'invalid_scope');
}
+ if (!$ENV{MOJO_TEST} && Mojo::URL->new($redirect_uri)->scheme ne 'https') {
+ INFO("invalid_redirect_uri: $redirect_uri");
+ return (0, 'invalid_redirect_uri');
+ }
+
if (
my $client_data = $dbh->selectrow_hashref(
- 'SELECT * FROM oauth2_client WHERE id = ?',
+ 'SELECT * FROM oauth2_client WHERE client_id = ?',
undef, $client_id
)
)
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 ($scopes_ref) {
+ my $client_scopes = $dbh->selectcol_arrayref(
+ 'SELECT oauth2_scope.description FROM oauth2_scope
+ JOIN oauth2_client_scope ON oauth2_scope.id = oauth2_client_scope.scope_id
+ WHERE oauth2_client_scope.client_id = ?', undef, $client_data->{id}
);
- 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');
+
+ foreach my $scope (@{$scopes_ref // []}) {
+ return (0, 'invalid_grant') if !_has_scope($scope, $client_scopes);
}
}
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)
my $dbh = Bugzilla->dbh;
my $client_data
- = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE id = ?',
+ = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE client_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
- );
+ my ($res, $jwt_claims) = _get_jwt_claims($auth_code, 'auth');
+ return (0, 'invalid_jwt') unless $res;
- 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)
+ my $jwt_data = $dbh->selectrow_hashref('SELECT * FROM oauth2_jwt WHERE jti = ?',
+ undef, $jwt_claims->{jti});
+
+ if (!$jwt_data
+ or ($jwt_data->{type} ne TOKEN_TYPE_AUTH)
+ or ($jwt_claims->{user_id} != $jwt_data->{user_id})
+ or ($uri ne $jwt_claims->{aud})
+ or ($jwt_claims->{exp} <= 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});
- }
+ if ($jwt_data) {
+ INFO('Client redirect_uri does not match')
+ if ($uri && $jwt_claims->{aud} ne $uri);
+ INFO('Auth code expired') if ($jwt_claims->{exp} <= time);
+ $dbh->do('DELETE FROM oauth2_jwt WHERE client_id = ? AND user_id = ? AND type = ?',
+ undef, $client_data->{id}, $jwt_claims->{user_id}, TOKEN_TYPE_AUTH);
}
return (0, 'invalid_grant');
}
- $dbh->do('UPDATE oauth2_auth_code SET verified = 1 WHERE auth_code = ?',
- undef, $auth_code);
+ $dbh->do('DELETE FROM oauth2_jwt WHERE id = ?',
+ undef, $jwt_data->{id});
- # 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
- );
+ return ($client_id, undef, $jwt_claims->{scopes}, $jwt_claims->{user_id});
+}
+
+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 $client_data
+ = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE client_id = ?',
+ undef, $client_id);
+
+ my ($res, $jwt_claims) = _get_jwt_claims($auth_code, 'auth');
+ return (0, 'invalid_jwt') unless $res;
- my %scope = map { $_ => 1 } @{$scope_descriptions};
+ $dbh->do(
+ 'INSERT INTO oauth2_jwt (jti, client_id, user_id, type, expires) VALUES (?, ?, ?, ?, ?)',
+ undef,
+ $jwt_claims->{jti},
+ $client_data->{id},
+ $jwt_claims->{user_id},
+ TOKEN_TYPE_AUTH,
+ DateTime->from_epoch(epoch => time + $expires_in),
+ );
- return ($client_id, undef, {%scope}, $auth_code_data->{user_id});
+ return undef;
}
sub _store_access_token {
my (%args) = @_;
- my ($c, $client, $auth_code, $access_token, $refresh_token, $expires_in,
+ my ($c, $client_id, $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) {
+ my $client_data
+ = $dbh->selectrow_hashref('SELECT * FROM oauth2_client WHERE client_id = ?',
+ undef, $client_id);
+ 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};
+ my ($res, $jwt_claims) = _get_jwt_claims($old_refresh_token, 'refresh');
+ return (0, 'invalid_jwt') unless $res;
+ my $jwt_data = $dbh->selectrow_hashref('SELECT * FROM oauth2_jwt WHERE jti = ?', undef, $jwt_claims->{jti});
+ return (0, 'invalid_grant') if !$jwt_data;
+ $user_id = $jwt_claims->{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};
+ my ($res, $jwt_claims) = _get_jwt_claims($auth_code, 'auth');
+ return (0, 'invalid_jwt') unless $res;
+ $user_id = $jwt_claims->{user_id};
}
- foreach my $token_type (qw/ access refresh /) {
- my $table = "oauth2_${token_type}_token";
+ my ($res, $jwt_claims) = _get_jwt_claims($access_token, 'access');
+ return (0, 'invalid_jwt') unless $res;
- # 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);
- }
+ # If the client has en existing access/refesh tokens, we need to revoke them
+ INFO('Revoking old access tokens (refresh)');
+ $dbh->do('DELETE FROM oauth2_jwt WHERE client_id = ? AND user_id = ?',
+ undef, $client_data->{id}, $jwt_claims->{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)
+ 'INSERT INTO oauth2_jwt (jti, client_id, user_id, type, expires) VALUES (?, ?, ?, ?, ?)',
+ undef,
+ $jwt_claims->{jti},
+ $client_data->{id},
+ $user_id,
+ TOKEN_TYPE_ACCESS,
+ 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");
- }
- }
+ ($res, $jwt_claims) = _get_jwt_claims($refresh_token, 'refresh');
+ return (0, 'invalid_jwt') unless $res;
- return;
+ $dbh->do(
+ 'INSERT INTO oauth2_jwt (jti, client_id, user_id, type) VALUES (?, ?, ?, ?)',
+ undef,
+ $jwt_claims->{jti},
+ $client_data->{id},
+ $user_id,
+ TOKEN_TYPE_REFRESH
+ );
+
+ return undef;
}
sub _verify_access_token {
my (%args) = @_;
- my ($c, $access_token, $scopes_ref)
- = @args{qw/ mojo_controller access_token scope /};
+ my ($c, $access_token, $scopes_ref, $is_refresh_token)
+ = @args{qw/ mojo_controller access_token scopes is_refresh_token /};
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
- );
+ my ($res, $jwt_claims) = _get_jwt_claims($access_token);
+ return (0, 'invalid_jwt') unless $res;
- if (!defined $scope_allowed || !$scope_allowed) {
- INFO("Refresh token doesn't have scope ($scope)");
- return (0, 'invalid_grant');
+ my $jwt_data = $dbh->selectrow_hashref('SELECT * FROM oauth2_jwt WHERE jti = ?', undef, $jwt_claims->{jti});
+
+ if ($jwt_data && $is_refresh_token) {
+ if ($scopes_ref) {
+ foreach my $scope (@{$scopes_ref // []}) {
+ return (0, 'invalid_grant') if !_has_scope($scope, $jwt_claims->{scopes});
}
}
- return {
- client_id => $refresh_token_data->{client_id},
- user_id => $refresh_token_data->{user_id},
- };
+ return ($jwt_claims, undef, $jwt_claims->{scopes}, $jwt_claims->{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) {
+
+ if ($jwt_data) {
+ if ($jwt_claims->{exp} <= time) {
INFO('Access token has expired');
- $dbh->do('DELETE FROM oauth2_access_token WHERE access_token = ?',
- undef, $access_token);
+ $dbh->do('DELETE FROM oauth2_jwt WHERE id = ?',
+ undef, $jwt_data->{id});
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');
+ elsif ($scopes_ref) {
+ foreach my $scope (@{$scopes_ref // []}) {
+ if (!_has_scope($scope, $jwt_claims->{scopes})) {
+ INFO("Scope $scope not found");
+ return (0, 'invalid_grant');
+ }
}
}
- return {
- client_id => $access_token_data->{client_id},
- user_id => $access_token_data->{user_id},
- };
+ return ($jwt_claims, undef, $jwt_claims->{scopes}, $jwt_claims->{user_id});
}
else {
INFO('Access token does not exist');
}
}
+sub _get_jwt_claims {
+ my ($jwt, $check_type) = @_;
+ my ($claims, $jwt_error);
+
+ try {
+ $claims = Bugzilla->jwt->decode($jwt);
+ }
+ catch {
+ INFO("Error decoding JWT: $_");
+ $jwt_error = 1;
+ };
+
+ return (0) if $jwt_error;
+
+ if (defined $check_type && $check_type ne $claims->{type}) {
+ INFO("JWT not correct type: got: " . $claims->{type} . " expected: $check_type");
+ return (0);
+ }
+
+ return (1, $claims);
+}
+
+sub _has_scope {
+ my ($scope, $available_scopes) = @_;
+ return any {$scope} @{$available_scopes // []};
+}
+
1;
oauth2_client => {
FIELDS => [
- id => {TYPE => 'varchar(255)', NOTNULL => 1, PRIMARYKEY => 1},
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ client_id => {TYPE => 'varchar(255)', NOTNULL => 1},
description => {TYPE => 'varchar(255)', NOTNULL => 1},
secret => {TYPE => 'varchar(255)', NOTNULL => 1},
active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'},
- last_modified => {TYPE => 'DATETIME'}
- ]
+ last_modified => {TYPE => 'DATETIME'},
+ ],
},
oauth2_scope => {
FIELDS => [
- id => {TYPE => 'INT3', NOTNULL => 1, PRIMARYKEY => 1},
- description => {TYPE => 'varchar(255)', NOTNULL => 1}
- ]
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ description => {TYPE => 'varchar(255)', NOTNULL => 1},
+ ],
},
oauth2_client_scope => {
FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
client_id => {
- TYPE => 'varchar(255)',
+ TYPE => 'INT4',
NOTNULL => 1,
REFERENCES => {
TABLE => 'oauth2_client',
COLUMN => 'id',
UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
+ DELETE => 'CASCADE',
}
},
scope_id => {
- TYPE => 'INT3',
+ TYPE => 'INT4',
NOTNULL => 1,
REFERENCES => {
TABLE => 'oauth2_scope',
COLUMN => 'id',
UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
+ DELETE => 'CASCADE',
+ },
},
- allowed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}
],
INDEXES => [
oauth2_client_scope_idx =>
{FIELDS => ['client_id', 'scope_id'], TYPE => 'UNIQUE'},
- ]
- },
-
- oauth2_auth_code => {
- FIELDS => [
- auth_code => {TYPE => 'varchar(255)', NOTNULL => 1, PRIMARYKEY => 1},
- client_id => {
- TYPE => 'varchar(255)',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_client',
- COLUMN => 'id',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- user_id => {
- TYPE => 'INT3',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'profiles',
- COLUMN => 'userid',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- expires => {TYPE => 'DATETIME', NOTNULL => 1},
- redirect_uri => {TYPE => 'TINYTEXT', NOTNULL => 1},
- verified => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
- ]
- },
-
- oauth2_auth_code_scope => {
- FIELDS => [
- auth_code => {
- TYPE => 'varchar(255)',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_auth_code',
- COLUMN => 'auth_code',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- scope_id => {
- TYPE => 'INT3',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_scope',
- COLUMN => 'id',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- allowed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
],
- INDEXES => [
- oauth2_auth_code_scope_idx =>
- {FIELDS => ['auth_code', 'scope_id'], TYPE => 'UNIQUE'},
- ]
},
- oauth2_access_token => {
+ oauth2_jwt => {
FIELDS => [
- access_token => {TYPE => 'varchar(255)', NOTNULL => 1, PRIMARYKEY => 1},
- refresh_token => {TYPE => 'varchar(255)'},
- client_id => {
- TYPE => 'varchar(255)',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_client',
- COLUMN => 'id',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- user_id => {
- TYPE => 'INT3',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'profiles',
- COLUMN => 'userid',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- expires => {TYPE => 'DATETIME', NOTNULL => 1},
- ]
- },
-
- oauth2_access_token_scope => {
- FIELDS => [
- access_token => {
- TYPE => 'varchar(255)',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_access_token',
- COLUMN => 'access_token',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- scope_id => {
- TYPE => 'INT3',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_scope',
- COLUMN => 'id',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- allowed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
- ],
- INDEXES => [
- oauth2_access_token_scope_idx =>
- {FIELDS => ['access_token', 'scope_id'], TYPE => 'UNIQUE'}
- ]
- },
-
- oauth2_refresh_token => {
- FIELDS => [
- refresh_token => {TYPE => 'varchar(255)', NOTNULL => 1, PRIMARYKEY => 1},
- access_token => {
- TYPE => 'varchar(255)',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_access_token',
- COLUMN => 'access_token',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ jti => {TYPE => 'varchar(255)', NOTNULL => 1},
+ type => {TYPE => 'INT2', NOTNULL => 1},
client_id => {
- TYPE => 'varchar(255)',
+ TYPE => 'INT4',
NOTNULL => 1,
REFERENCES => {
TABLE => 'oauth2_client',
COLUMN => 'id',
UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
+ DELETE => 'CASCADE',
}
},
user_id => {
TABLE => 'profiles',
COLUMN => 'userid',
UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- }
- ]
- },
-
- oauth2_refresh_token_scope => {
- FIELDS => [
- refresh_token => {
- TYPE => 'varchar(255)',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_refresh_token',
- COLUMN => 'refresh_token',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
+ DELETE => 'CASCADE',
+ },
},
- scope_id => {
- TYPE => 'INT3',
- NOTNULL => 1,
- REFERENCES => {
- TABLE => 'oauth2_scope',
- COLUMN => 'id',
- UPDATE => 'CASCADE',
- DELETE => 'CASCADE'
- }
- },
- allowed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+ expires => {TYPE => 'DATETIME'},
],
INDEXES => [
- oauth2_refresh_token_scope_idx =>
- {FIELDS => ['refresh_token', 'scope_id'], TYPE => 'UNIQUE'}
- ]
+ oauth2_jwt_jti_type_idx => {FIELDS => [qw(jti type)], TYPE => 'UNIQUE'},
+ ],
}
};
BEGIN {
$ENV{LOG4PERL_CONFIG_FILE} = 'log4perl-t.conf';
$ENV{BUGZILLA_DISABLE_HOSTAGE} = 1;
+ $ENV{MOJO_TEST} = 1;
}
use Bugzilla::Test::MockDB;
my $stash = {};
# Create user to use as OAuth2 resource owner
-create_user($oauth_login, $oauth_password);
+my $oauth_user = 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->{client_id},
+ 'New client id (' . $oauth_client->{client_id} . ')';
ok $oauth_client->{secret},
'New client secret (' . $oauth_client->{secret} . ')';
# 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},
+ client_id => $oauth_client->{client_id},
response_type => 'code',
state => 'state',
scope => 'user:read',
Bugzilla_password => $oauth_password,
Bugzilla_restrictlogin => 1,
GoAheadAndLogIn => 1,
- client_id => $oauth_client->{id},
+ client_id => $oauth_client->{client_id},
response_type => 'code',
state => 'state',
scope => 'user:read',
# 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'
+ "oauth_confirm_" . $oauth_client->{client_id} => 1,
+ token => $csrf_token,
+ client_id => $oauth_client->{client_id},
+ response_type => 'code',
+ state => 'state',
+ scope => 'user:read',
+ redirect_uri => '/oauth/redirect'
}
)->status_is(200)->content_is('Redirect Success!');
# end user.
$t->post_ok(
'/oauth/access_token' => {Referer => $referer} => form => {
- client_id => $oauth_client->{id},
+ client_id => $oauth_client->{client_id},
client_secret => $oauth_client->{secret},
code => $auth_code,
grant_type => 'authorization_code',
{Authorization => 'Bearer ' . $access_data->{access_token}})->status_is(200)
->json_is('/login' => $oauth_login);
+# Should be able to use the refresh token to get a new access token
+$t->post_ok(
+ '/oauth/access_token' => {Referer => $referer} => form => {
+ client_id => $oauth_client->{client_id},
+ client_secret => $oauth_client->{secret},
+ refresh_token => $access_data->{refresh_token},
+ grant_type => 'refresh_token',
+ 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');
+
+$access_data = $t->tx->res->json;
+
+$t->get_ok('/api/user/profile' =>
+ {Authorization => 'Bearer ' . $access_data->{access_token}})->status_is(200)
+ ->json_is('/login' => $oauth_login);
+
+# API should fail if user is disabled
+$oauth_user->set_disabledtext('DISABLED');
+$oauth_user->update();
+$t->get_ok('/api/user/profile' =>
+ {Authorization => 'Bearer ' . $access_data->{access_token}})
+ ->status_is(401);
+
+# Should get an error if we try to re-use the same auth code again
+$t->post_ok(
+ '/oauth/access_token' => {Referer => $referer} => form => {
+ client_id => $oauth_client->{client_id},
+ client_secret => $oauth_client->{secret},
+ code => $auth_code,
+ grant_type => 'authorization_code',
+ redirect_uri => '/oauth/redirect',
+ }
+)->status_is(400)->json_is('/error' => 'invalid_grant');
+
done_testing;
sub _setup_routes {