[Rt-commit] rt branch, 5.0-trunk, updated. rt-5.0.0alpha1-438-ga90d185b56

? sunnavy sunnavy at bestpractical.com
Wed May 13 17:40:54 EDT 2020


The branch, 5.0-trunk has been updated
       via  a90d185b5680e9331cdcdd6119e359170c4152c2 (commit)
       via  ecbcb721de0abadd47d00a17f694d2dc80386d0f (commit)
       via  7ed36c94facb5eea166edf6646b7058771bbe01a (commit)
       via  60f3fec50c49064793e1348c0a6159044b430e23 (commit)
       via  e1e2e13068a5068ff7da4a12a68585cf23d11a7c (commit)
       via  33e7e52f5350ec25a610be4135f89a923c91f7ee (commit)
       via  4cfda3bfb4e90da11425d247b173aa0526c6857d (commit)
      from  b1146b795fad7595587a87b6b1448cd9824bf149 (commit)

Summary of changes:
 docs/authentication.pod                            |  19 +-
 docs/web_deployment.pod                            |  10 +
 etc/acl.Pg                                         |   2 +
 etc/schema.Oracle                                  |  15 +
 etc/schema.Pg                                      |  15 +
 etc/schema.SQLite                                  |  13 +
 etc/schema.mysql                                   |  14 +
 etc/upgrade/{4.1.1 => 4.5.7}/acl.Pg                |   6 +-
 etc/upgrade/4.5.7/schema.Oracle                    |  14 +
 etc/upgrade/4.5.7/schema.Pg                        |  15 +
 etc/upgrade/4.5.7/schema.SQLite                    |  13 +
 etc/upgrade/4.5.7/schema.mysql                     |  14 +
 lib/RT.pm                                          |   2 +
 lib/RT/AuthToken.pm                                | 372 +++++++++++++++++++++
 lib/RT/{Configurations.pm => AuthTokens.pm}        |  29 +-
 lib/RT/Authen/Token.pm                             | 127 +++++++
 lib/RT/Interface/Web.pm                            | 111 ++++++
 lib/RT/Interface/Web/MenuBuilder.pm                |  29 ++
 .../UserRights.html => Users/AuthTokens.html}      |  30 +-
 .../SearchSelection => Elements/AuthToken/Create}  |  95 ++----
 .../Elements/EditLinks => Elements/AuthToken/Edit} |  80 ++---
 .../Elements/Wrapper => Elements/AuthToken/Help}   |  17 +-
 .../SelectObjects => Elements/AuthToken/List}      |  50 +--
 .../Global/Topics.html => Prefs/AuthTokens.html}   |  17 +-
 share/static/css/elevator-light/login.css          |   2 +-
 share/static/css/elevator-light/misc.css           |   1 -
 share/static/css/elevator-light/nav.css            |   2 +-
 27 files changed, 934 insertions(+), 180 deletions(-)
 copy etc/upgrade/{4.1.1 => 4.5.7}/acl.Pg (92%)
 create mode 100644 etc/upgrade/4.5.7/schema.Oracle
 create mode 100644 etc/upgrade/4.5.7/schema.Pg
 create mode 100644 etc/upgrade/4.5.7/schema.SQLite
 create mode 100644 etc/upgrade/4.5.7/schema.mysql
 create mode 100644 lib/RT/AuthToken.pm
 copy lib/RT/{Configurations.pm => AuthTokens.pm} (83%)
 create mode 100644 lib/RT/Authen/Token.pm
 copy share/html/Admin/{Global/UserRights.html => Users/AuthTokens.html} (80%)
 copy share/html/{Widgets/SearchSelection => Elements/AuthToken/Create} (55%)
 copy share/html/{Admin/Elements/EditLinks => Elements/AuthToken/Edit} (59%)
 copy share/html/{Install/Elements/Wrapper => Elements/AuthToken/Help} (85%)
 copy share/html/{Admin/Tools/Shredder/Elements/SelectObjects => Elements/AuthToken/List} (66%)
 copy share/html/{Admin/Global/Topics.html => Prefs/AuthTokens.html} (87%)

- Log -----------------------------------------------------------------
commit 4cfda3bfb4e90da11425d247b173aa0526c6857d
Author: Craig <craig at bestpractical.com>
Date:   Mon May 4 17:57:42 2020 -0400

    Core RT::Authen::Token
    
    The html/js/css/gif are not imported, as we are going to re-implement
    it.

diff --git a/etc/acl.Pg b/etc/acl.Pg
index 41a44b16c1..dc3ca03f37 100644
--- a/etc/acl.Pg
+++ b/etc/acl.Pg
@@ -68,6 +68,8 @@ sub acl {
         ObjectCustomRoles
         configurations_id_seq
         Configurations
+        authtokens_id_seq
+        AuthTokens
     );
 
     my $db_user = RT->Config->Get('DatabaseUser');
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 2b366299cf..5117ad2ef8 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -554,3 +554,18 @@ CREATE TABLE Configurations (
 
 CREATE INDEX Configurations1 ON Configurations (LOWER(Name), Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
+
+CREATE SEQUENCE AuthTokens_seq;
+CREATE TABLE AuthTokens (
+    id              NUMBER(11,0)    CONSTRAINT AuthTokens_key PRIMARY KEY,
+    Owner           NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Token           VARCHAR2(256),
+    Description     varchar2(255)   DEFAULT '',
+    LastUsed        DATE,
+    Creator         NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Created         DATE,
+    LastUpdatedBy   NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    LastUpdated     DATE
+);
+
+CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/etc/schema.Pg b/etc/schema.Pg
index c51be5dff3..5f6c3d85fb 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -796,3 +796,18 @@ CREATE TABLE Configurations (
 CREATE INDEX Configurations1 ON Configurations (LOWER(Name), Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
 
+CREATE SEQUENCE authtokens_id_seq;
+CREATE TABLE AuthTokens (
+    id                integer                  DEFAULT nextval('authtokens_id_seq'),
+    Owner             integer         NOT NULL DEFAULT 0,
+    Token             varchar(256)    NULL,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          timestamp                DEFAULT NULL,
+    Creator           integer         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     integer         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index c51070aa87..bc8b456ecd 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -585,3 +585,16 @@ CREATE TABLE Configurations (
 CREATE INDEX Configurations1 ON Configurations (Name, Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
 
+CREATE TABLE AuthTokens (
+    id                INTEGER PRIMARY KEY,
+    Owner             int(11)         NOT NULL DEFAULT 0,
+    Token             varchar(256)    collate NOCASE NULL  ,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          timestamp                DEFAULT NULL,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL
+);
+
+CREATE INDEX AuthTokensOwner on AuthTokens (Owner);
diff --git a/etc/schema.mysql b/etc/schema.mysql
index ca90073340..69cb029093 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -576,3 +576,17 @@ CREATE TABLE Configurations (
 CREATE INDEX Configurations1 ON Configurations (Name, Disabled);
 CREATE INDEX Configurations2 ON Configurations (Disabled);
 
+CREATE TABLE AuthTokens (
+    id                int(11)         NOT NULL AUTO_INCREMENT,
+    Owner             int(11)         NOT NULL DEFAULT 0,
+    Token             varchar(256)    NULL,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          datetime                 DEFAULT NULL,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           datetime                 DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       datetime                 DEFAULT NULL,
+    PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/etc/upgrade/4.5.7/acl.Pg b/etc/upgrade/4.5.7/acl.Pg
new file mode 100644
index 0000000000..61345ade01
--- /dev/null
+++ b/etc/upgrade/4.5.7/acl.Pg
@@ -0,0 +1,29 @@
+sub acl {
+    my $dbh = shift;
+
+    my @acls;
+    my @tables = qw (
+        authtokens_id_seq
+        AuthTokens
+    );
+
+    my $db_user = RT->Config->Get('DatabaseUser');
+
+    my $sequence_right
+        = ( $dbh->{pg_server_version} >= 80200 )
+        ? "USAGE, SELECT, UPDATE"
+        : "SELECT, UPDATE";
+
+    foreach my $table (@tables) {
+        # Tables are upper-case, sequences are lowercase in @tables
+        if ( $table =~ /^[a-z]/ ) {
+            push @acls, "GRANT $sequence_right ON $table TO \"$db_user\";"
+        }
+        else {
+            push @acls, "GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO \"$db_user\";"
+        }
+    }
+    return (@acls);
+}
+
+1;
diff --git a/etc/upgrade/4.5.7/schema.Oracle b/etc/upgrade/4.5.7/schema.Oracle
new file mode 100644
index 0000000000..dafe9c04b1
--- /dev/null
+++ b/etc/upgrade/4.5.7/schema.Oracle
@@ -0,0 +1,14 @@
+CREATE SEQUENCE AuthTokens_seq;
+CREATE TABLE AuthTokens (
+    id              NUMBER(11,0)    CONSTRAINT AuthTokens_key PRIMARY KEY,
+    Owner           NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Token           VARCHAR2(256),
+    Description     varchar2(255)   DEFAULT '',
+    LastUsed        DATE,
+    Creator         NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Created         DATE,
+    LastUpdatedBy   NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    LastUpdated     DATE
+);
+
+CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/etc/upgrade/4.5.7/schema.Pg b/etc/upgrade/4.5.7/schema.Pg
new file mode 100644
index 0000000000..29a5372a89
--- /dev/null
+++ b/etc/upgrade/4.5.7/schema.Pg
@@ -0,0 +1,15 @@
+CREATE SEQUENCE authtokens_id_seq;
+CREATE TABLE AuthTokens (
+    id                integer                  DEFAULT nextval('authtokens_id_seq'),
+    Owner             integer         NOT NULL DEFAULT 0,
+    Token             varchar(256)    NULL,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          timestamp                DEFAULT NULL,
+    Creator           integer         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     integer         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/etc/upgrade/4.5.7/schema.SQLite b/etc/upgrade/4.5.7/schema.SQLite
new file mode 100644
index 0000000000..d761c4aafb
--- /dev/null
+++ b/etc/upgrade/4.5.7/schema.SQLite
@@ -0,0 +1,13 @@
+CREATE TABLE AuthTokens (
+    id                INTEGER PRIMARY KEY,
+    Owner             int(11)         NOT NULL DEFAULT 0,
+    Token             varchar(256)    collate NOCASE NULL  ,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          timestamp                DEFAULT NULL,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL
+);
+
+CREATE INDEX AuthTokensOwner on AuthTokens (Owner);
diff --git a/etc/upgrade/4.5.7/schema.mysql b/etc/upgrade/4.5.7/schema.mysql
new file mode 100644
index 0000000000..0bf9a499ae
--- /dev/null
+++ b/etc/upgrade/4.5.7/schema.mysql
@@ -0,0 +1,14 @@
+CREATE TABLE AuthTokens (
+    id                int(11)         NOT NULL AUTO_INCREMENT,
+    Owner             int(11)         NOT NULL DEFAULT 0,
+    Token             varchar(256)    NULL,
+    Description       varchar(255)    NOT NULL DEFAULT '',
+    LastUsed          datetime                 DEFAULT NULL,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           datetime                 DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       datetime                 DEFAULT NULL,
+    PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/lib/RT.pm b/lib/RT.pm
index e280b2074d..0aa78b08e5 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -507,6 +507,7 @@ sub InitClasses {
     require RT::Configuration;
     require RT::Configurations;
     require RT::REST2;
+    require RT::Authen::Token;
 
     _BuildTableAttributes();
 
@@ -773,6 +774,7 @@ our %CORED_PLUGINS = (
     'RT::Extension::AssetSQL' => '5.0',
     'RT::Extension::LifecycleUI' => '5.0',
     'RT::Extension::REST2' => '5.0',
+    'RT::Authen::Token' => '5.0',
 );
 
 sub InitPlugins {
diff --git a/lib/RT/AuthToken.pm b/lib/RT/AuthToken.pm
new file mode 100644
index 0000000000..d4472970bc
--- /dev/null
+++ b/lib/RT/AuthToken.pm
@@ -0,0 +1,372 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+use 5.10.1;
+
+package RT::AuthToken;
+use base 'RT::Record';
+
+require RT::User;
+require RT::Util;
+use Digest::SHA 'sha512_hex';
+
+=head1 NAME
+
+RT::AuthToken - Represents an authentication token for a user
+
+=cut
+
+=head1 METHODS
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database.  Available
+keys are:
+
+=over 4
+
+=item Owner
+
+The user ID for whom this token will authenticate. If it's not the AuthToken
+object's CurrentUser, then the AdminUsers permission is required.
+
+=item Description
+
+A human-readable description of what this token will be used for.
+
+=back
+
+Returns a tuple of (status, msg) on failure and (id, msg, authstring) on
+success. Note that this is the only time the authstring will be directly
+readable (as it is stored in the database hashed like a password, so use
+this opportunity to capture it.
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = (
+        Owner       => undef,
+        Description => '',
+        @_,
+    );
+
+    return (0, $self->loc("Permission Denied"))
+        unless $self->CurrentUserHasRight('ManageAuthTokens');
+
+    return (0, $self->loc("Owner required"))
+        unless $args{Owner};
+
+    return (0, $self->loc("Permission Denied"))
+        unless $args{Owner} == $self->CurrentUser->Id
+            || $self->CurrentUserHasRight('AdminUsers');
+
+    my $token = $self->_GenerateToken;
+
+    my ( $id, $msg ) = $self->SUPER::Create(
+        Token => $self->_CryptToken($token),
+        map { $_ => $args{$_} } grep {exists $args{$_}}
+            qw(Owner Description),
+    );
+    unless ($id) {
+        return (0, $self->loc("Authentication token create failed: [_1]", $msg));
+    }
+
+    my $authstring = $self->_BuildAuthString($self->Owner, $token);
+
+    return ($id, $self->loc('Authentication token created'), $authstring);
+}
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the AuthToken
+
+=cut
+
+sub CurrentUserCanSee {
+    my $self = shift;
+
+    return 0 unless $self->CurrentUserHasRight('ManageAuthTokens');
+
+    return 0 unless $self->__Value('Owner') == $self->CurrentUser->Id
+                 ||  $self->CurrentUserHasRight('AdminUsers');
+
+    return 1;
+}
+
+=head2 SetOwner
+
+Not permitted
+
+=cut
+
+sub SetOwner {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied"));
+}
+
+=head2 SetToken
+
+Not permitted
+
+=cut
+
+sub SetToken {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied"));
+}
+
+=head2 Delete
+
+Checks ACL
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied")) unless $self->CurrentUserCanSee;
+    my ($ok, $msg) = $self->SUPER::Delete(@_);
+    return ($ok, $self->loc("Authentication token revoked.")) if $ok;
+    return ($ok, $msg);
+}
+
+=head2 UpdateLastUsed
+
+Sets the "last used" time, without touching "last updated"
+
+=cut
+
+sub UpdateLastUsed {
+    my $self = shift;
+
+    my $now = RT::Date->new( $self->CurrentUser );
+    $now->SetToNow;
+
+    return $self->__Set(
+        Field => 'LastUsed',
+        Value => $now->ISO,
+    );
+}
+
+=head2 ParseAuthString AUTHSTRING
+
+Class method that takes as input an authstring and provides a tuple
+of (user id, token) on success, or the empty list on failure.
+
+=cut
+
+sub ParseAuthString {
+    my $class = shift;
+    my $input = shift;
+
+    my ($version) = $input =~ s/^([0-9]+)-//
+        or return;
+
+    if ($version == 1) {
+        my ($user_id, $token) = $input =~ /^([0-9]+)-([0-9a-f]{32})$/i
+            or return;
+        return ($user_id, $token);
+    }
+
+    return;
+}
+
+=head2 IsToken
+
+Analogous to L<RT::User/IsPassword>, without all of the legacy password
+forms.
+
+=cut
+
+sub IsToken {
+    my $self = shift;
+    my $value = shift;
+
+    my $stored = $self->__Value('Token');
+
+    # If it's a new-style (>= RT 4.0) password, it starts with a '!'
+    my (undef, $method, @rest) = split /!/, $stored;
+    if ($method eq "bcrypt") {
+        if (RT::Util->can('constant_time_eq')) {
+            return 0 unless RT::Util::constant_time_eq(
+                $self->_CryptToken_bcrypt($value, @rest),
+                $stored,
+            );
+        } else {
+            return 0 unless $self->_CryptToken_bcrypt($value, @rest) eq $stored;
+        }
+        # Upgrade to a larger number of rounds if necessary
+        return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
+    }
+    else {
+        $RT::Logger->warn("Unknown hash method $method");
+        return 0;
+    }
+
+    # We got here by validating successfully, but with a legacy
+    # password form.  Update to the most recent form.
+    $self->_Set(Field => 'Token', Value => $self->_CryptToken($value));
+    return 1;
+}
+
+=head2 LastUsedObj
+
+L</LastUsed> as an L<RT::Date> object.
+
+=cut
+
+sub LastUsedObj {
+    my $self = shift;
+    my $date = RT::Date->new($self->CurrentUser);
+    $date->Set(Format => 'sql', Value => $self->LastUsed);
+    return $date;
+}
+
+=head1 PRIVATE METHODS
+
+Documented for internal use only, do not call these from outside
+RT::AuthToken itself.
+
+=head2 _Set
+
+Checks if the current user can I<ManageAuthTokens> before calling
+C<SUPER::_Set>.
+
+=cut
+
+sub _Set {
+    my $self = shift;
+    my %args = (
+        Field => undef,
+        Value => undef,
+        @_
+    );
+
+    return (0, $self->loc("Permission Denied"))
+        unless $self->CurrentUserCanSee;
+
+    return $self->SUPER::_Set(@_);
+}
+
+=head2 _Value
+
+Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>.
+
+=cut
+
+sub _Value {
+    my $self = shift;
+    return unless $self->CurrentUserCanSee;
+    return $self->SUPER::_Value(@_);
+}
+
+=head2 _GenerateToken
+
+Generates an unpredictable auth token
+
+=cut
+
+sub _GenerateToken {
+    my $class = shift;
+    require Time::HiRes;
+
+    my $input = join '',
+                    Time::HiRes::time(), # subsecond-precision time
+                    {},                  # unpredictable memory address
+                    rand();              # RNG
+
+    my $digest = sha512_hex($input);
+
+    return substr($digest, 0, 32);
+}
+
+=head2 _BuildAuthString
+
+Takes a user id and token and provides an authstring for use in place of
+a (username, password) combo.
+
+=cut
+
+sub _BuildAuthString {
+    my $self    = shift;
+    my $version = 1;
+    my $userid  = shift;
+    my $token   = shift;
+
+    return $version . '-' . $userid . '-' . $token;
+}
+
+sub _CryptToken_bcrypt {
+    my $self = shift;
+    return $self->CurrentUser->UserObj->_GeneratePassword_bcrypt(@_);
+}
+
+sub _CryptToken {
+    my $self = shift;
+    return $self->_CryptToken_bcrypt(@_);
+}
+
+sub Table { "AuthTokens" }
+
+sub _CoreAccessible {
+    {
+        id            => { read => 1, type => 'int(11)',        default => '' },
+        Owner         => { read => 1, type => 'int(11)',        default => '0' },
+        Token         => { read => 1, sql_type => 12, length => 256, is_blob => 0, is_numeric => 0, type => 'varchar(256)', default => ''},
+        Description   => { read => 1, type => 'varchar(255)',   default => '',  write => 1 },
+        LastUsed      => { read => 1, type => 'datetime',       default => '',  write => 1 },
+        Creator       => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
+        Created       => { read => 1, type => 'datetime',       default => '',  auto => 1 },
+        LastUpdatedBy => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
+        LastUpdated   => { read => 1, type => 'datetime',       default => '',  auto => 1 },
+    }
+}
+
+1;
diff --git a/lib/RT/AuthTokens.pm b/lib/RT/AuthTokens.pm
new file mode 100644
index 0000000000..6f5351ee10
--- /dev/null
+++ b/lib/RT/AuthTokens.pm
@@ -0,0 +1,99 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::AuthTokens;
+use base 'RT::SearchBuilder';
+
+=head1 NAME
+
+RT::AuthTokens - a collection of L<RT::AuthToken> objects
+
+=cut
+
+=head2 LimitOwner
+
+Limit Owner
+
+=cut
+
+sub LimitOwner {
+    my $self = shift;
+    my %args = (
+        FIELD    => 'Owner',
+        OPERATOR => '=',
+        @_
+    );
+
+    $self->SUPER::Limit(%args);
+}
+
+sub NewItem {
+    my $self = shift;
+    return RT::AuthToken->new( $self->CurrentUser );
+}
+
+=head2 _Init
+
+Sets default ordering by id ascending.
+
+=cut
+
+sub _Init {
+    my $self = shift;
+
+    $self->OrderBy( FIELD => 'id', ORDER => 'ASC' );
+    return $self->SUPER::_Init( @_ );
+}
+
+sub Table { "AuthTokens" }
+
+1;
+
diff --git a/lib/RT/Authen/Token.pm b/lib/RT/Authen/Token.pm
new file mode 100644
index 0000000000..b10ebb9764
--- /dev/null
+++ b/lib/RT/Authen/Token.pm
@@ -0,0 +1,127 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Authen::Token;
+
+use strict;
+use warnings;
+
+use RT::System;
+
+'RT::System'->AddRight(Staff => ManageAuthTokens => 'Manage authentication tokens'); # loc
+
+use RT::AuthToken;
+use RT::AuthTokens;
+
+sub UserForAuthString {
+    my $self = shift;
+    my $authstring = shift;
+    my $user = shift;
+
+    my ($user_id, $cleartext_token) = RT::AuthToken->ParseAuthString($authstring);
+    return unless $user_id;
+
+    my $user_obj = RT::CurrentUser->new;
+    $user_obj->Load($user_id);
+    return if !$user_obj->Id || $user_obj->Disabled;
+
+    if (length $user) {
+        my $check_user = RT::CurrentUser->new;
+        $check_user->Load($user);
+        return unless $check_user->Id && $user_obj->Id == $check_user->Id;
+    }
+
+    my $tokens = RT::AuthTokens->new(RT->SystemUser);
+    $tokens->LimitOwner(VALUE => $user_id);
+    while (my $token = $tokens->Next) {
+        if ($token->IsToken($cleartext_token)) {
+            $token->UpdateLastUsed;
+            return ($user_obj, $token);
+        }
+    }
+
+    return;
+}
+
+=head1 NAME
+
+RT-Authen-Token - token-based authentication
+
+=head1 DESCRIPTION
+
+Allow for users to generate and login with authentication tokens. Users
+with the C<ManageAuthTokens> permission will see a new "Auth Tokens"
+menu item under "Logged in as ____" -> Settings. On that page they will
+be able to generate new tokens and modify or revoke existing tokens.
+
+Once you have an authentication token, you may use it in place of a
+password to log into RT. (Additionally, L<REST2> allows for using auth
+tokens with the C<Authorization: token> HTTP header.) One common use
+case is to use an authentication token as an application-specific
+password, so that you may revoke that application's access without
+disturbing other applications. You also need not change your password,
+since the application never received it.
+
+If you have the C<AdminUsers> permission, along with
+C<ManageAuthTokens>, you may generate, modify, and revoke tokens for
+other users as well by visiting Admin -> Users -> Select -> (user) ->
+Auth Tokens.
+
+Authentication tokens are stored securely (hashed and salted) in the
+database just like passwords, and so cannot be recovered after they are
+generated.
+
+=head2 Update your Apache configuration
+
+If you are running RT under Apache, add the following directive to your RT
+Apache configuration to allow RT to access the Authorization header.
+
+    SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+=cut
+
+1;
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index d8fe7af58d..ae4c4b4178 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -336,6 +336,8 @@ sub HandleRequest {
     $HTML::Mason::Commands::m->comp( '/Elements/DoAuth', %$ARGS )
         if @{ RT->Config->Get( 'ExternalAuthPriority' ) || [] };
 
+    AttemptTokenAuthentication($ARGS) unless _UserLoggedIn();
+
     # Process per-page authentication callbacks
     $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' );
 
@@ -867,6 +869,39 @@ sub AttemptPasswordAuthentication {
     }
 }
 
+sub AttemptTokenAuthentication {
+    my $ARGS = shift;
+    my ($pass, $user) = ('', '');
+    if ((RequestENV('HTTP_AUTHORIZATION')||'') =~ /^token (.*)$/i) {
+        $pass ||= $1;
+        my ($user_obj, $token) = RT::Authen::Token->UserForAuthString($pass, $user);
+        if ( $user_obj ) {
+            # log in
+            my $remote_addr = RequestENV('REMOTE_ADDR');
+            $RT::Logger->info("Successful login for @{[$user_obj->Name]} from $remote_addr using authentication token #@{[$token->Id]} (\"@{[$token->Description]}\")");
+
+            # It's important to nab the next page from the session before we blow
+            # the session away
+            my $next = RT::Interface::Web::RemoveNextPage($ARGS->{'next'});
+            $next = $next->{'url'} if ref $next;
+
+            RT::Interface::Web::InstantiateNewSession();
+            $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+
+            # Really the only time we don't want to redirect here is if we were
+            # passed user and pass as query params in the URL.
+            if ($next) {
+                RT::Interface::Web::Redirect($next);
+            }
+            elsif ($ARGS->{'next'}) {
+                # Invalid hash, but still wants to go somewhere, take them to /
+                RT::Interface::Web::Redirect(RT->Config->Get('WebURL'));
+            }
+        }
+    }
+}
+
+
 =head2 LoadSessionFromCookie
 
 Load or setup a session cookie for the current user.
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 7d0f04911c..67f44d16a6 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -296,6 +296,9 @@ sub BuildMainNav {
         my $settings = $about_me->child( settings => title => loc('Settings'), path => '/Prefs/Other.html' );
         $settings->child( options        => title => loc('Preferences'),        path => '/Prefs/Other.html' );
         $settings->child( about_me       => title => loc('About me'),       path => '/Prefs/AboutMe.html' );
+        if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
+            $settings->child( auth_tokens => title => loc('Auth Tokens'), path => '/Prefs/AuthTokens.html' );
+        }
         $settings->child( search_options => title => loc('Search options'), path => '/Prefs/SearchOptions.html' );
         $settings->child( myrt           => title => loc('RT at a glance'), path => '/Prefs/MyRT.html' );
         $settings->child( dashboards_in_menu =>
@@ -1351,6 +1354,10 @@ sub _BuildAdminMenu {
                     $page->child( keys    => title => loc('Private keys'),   path => "/Admin/Users/Keys.html?id=" . $id );
                 }
                 $page->child( 'summary'   => title => loc('User Summary'),   path => "/User/Summary.html?id=" . $id );
+
+                if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
+                    $page->child( auth_tokens => title => loc('Auth Tokens'), path => '/Admin/Users/AuthTokens.html?id=' . $id );
+                }
             }
         }
 

commit 33e7e52f5350ec25a610be4135f89a923c91f7ee
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue May 12 03:52:43 2020 +0800

    Upgrade charset of AuthTokens table to utf8mb4 for MySQL/MariaDB

diff --git a/etc/schema.mysql b/etc/schema.mysql
index 69cb029093..6c368b7909 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -587,6 +587,6 @@ CREATE TABLE AuthTokens (
     LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
     LastUpdated       datetime                 DEFAULT NULL,
     PRIMARY KEY (id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB CHARACTER SET utf8mb4;
 
 CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);
diff --git a/etc/upgrade/4.5.7/schema.mysql b/etc/upgrade/4.5.7/schema.mysql
index 0bf9a499ae..aab6f75c5c 100644
--- a/etc/upgrade/4.5.7/schema.mysql
+++ b/etc/upgrade/4.5.7/schema.mysql
@@ -9,6 +9,6 @@ CREATE TABLE AuthTokens (
     LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
     LastUpdated       datetime                 DEFAULT NULL,
     PRIMARY KEY (id)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB CHARACTER SET utf8mb4;
 
 CREATE INDEX AuthTokensOwner ON AuthTokens (Owner);

commit e1e2e13068a5068ff7da4a12a68585cf23d11a7c
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Tue May 12 03:24:43 2020 +0800

    Reduce our z-index to coordinate with ones in bootstrap
    
    Bootstrap has correctly ordered z-index settings, e.g. .modal is 1050,
    .tooltip is 1070. Previously we overrode .modal's z-index with 10000,
    which caused tooltips to not show up in modals.
    
    This commit resets .modal's z-index and reduces our nav's
    correspondingly so .modal could cover nav.

diff --git a/share/static/css/elevator-light/login.css b/share/static/css/elevator-light/login.css
index 6a43419b5b..f7255ebef8 100644
--- a/share/static/css/elevator-light/login.css
+++ b/share/static/css/elevator-light/login.css
@@ -13,7 +13,7 @@
 
 #quick-personal {
     position: absolute;
-    z-index: 9999;
+    z-index: 1000;
     left: 0;
     /* This avoids a very weird bug in Chrome where opening a select causes a
      * hover event at (0,0), which will be over top of the menu sometimes */
diff --git a/share/static/css/elevator-light/misc.css b/share/static/css/elevator-light/misc.css
index 60a0837e98..f37e36834f 100644
--- a/share/static/css/elevator-light/misc.css
+++ b/share/static/css/elevator-light/misc.css
@@ -105,7 +105,6 @@ textarea.messagebox, #cke_Content, #cke_UpdateContent {
 
 .modal {
   background: rgb(0, 0, 0, .70); 
-  z-index: 10000; 
 }
 
 /* manipulate the svg image for selected bookmarks */
diff --git a/share/static/css/elevator-light/nav.css b/share/static/css/elevator-light/nav.css
index d6450cb24c..d945d18e11 100644
--- a/share/static/css/elevator-light/nav.css
+++ b/share/static/css/elevator-light/nav.css
@@ -78,7 +78,7 @@ ul.sf-menu li {
     position: absolute;
     top: 1px;
     left: 0;
-    z-index: 9999;
+    z-index: 1000;
     text-color: #000;
 }
 

commit 60f3fec50c49064793e1348c0a6159044b430e23
Author: Craig <craig at bestpractical.com>
Date:   Thu May 7 10:43:41 2020 -0400

    Re-implement Authen-Token web UI to work like other RT pages

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index ae4c4b4178..ef5b059bec 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -4983,6 +4983,78 @@ sub ProcessCustomDateRanges {
     return @results;
 }
 
+=head2 ProcessAuthToken ARGSRef => ARGSREF
+
+Returns an array of results messages.
+
+=cut
+
+sub ProcessAuthToken {
+    my %args = (
+        ARGSRef => undef,
+        @_
+    );
+    my $args_ref = $args{ARGSRef};
+
+    my @results;
+    my $token = RT::AuthToken->new( $session{CurrentUser} );
+
+    if ( $args_ref->{Create} ) {
+
+        # Don't require password for systems with some form of federated auth
+        my %res = $session{'CurrentUser'}->CurrentUserRequireToSetPassword();
+
+        if ( !length( $args_ref->{Description} ) ) {
+            push @results, loc("Description cannot be blank.");
+        }
+        elsif ( $res{'CanSet'} && !length( $args_ref->{Password} ) ) {
+            push @results, loc("Please enter your current password.");
+        }
+        elsif ( $res{'CanSet'} && !$session{CurrentUser}->IsPassword( $args_ref->{Password} ) ) {
+            push @results, loc("Please enter your current password correctly.");
+        }
+        else {
+            my ( $ok, $msg, $auth_string ) = $token->Create(
+                Owner       => $args_ref->{Owner},
+                Description => $args_ref->{Description},
+            );
+            if ($ok) {
+            }
+            push @results, $msg;
+            push @results,
+                loc(
+                '"[_1]" is your new authentication token. Treat it carefully like a password. Please save it now because you cannot access it again.',
+                $auth_string
+                );
+        }
+    }
+    elsif ( $args_ref->{Update} || $args_ref->{Revoke} ) {
+
+        $token->Load( $args_ref->{Token} );
+        if ( $token->Id ) {
+            if ( $args_ref->{Update} ) {
+                if ( length( $args_ref->{Description} ) ) {
+                    if ( $args_ref->{Description} ne $token->Description ) {
+                        my ( $ok, $msg ) = $token->SetDescription( $args_ref->{Description} );
+                        push @results, $msg;
+                    }
+                }
+                else {
+                    push @results, loc("Description cannot be blank.");
+                }
+            }
+            elsif ( $args_ref->{Revoke} ) {
+                my ( $ok, $msg ) = $token->Delete;
+                push @results, $msg;
+            }
+        }
+        else {
+            push @results, loc("Could not find token: [_1]", $args_ref->{Token});
+        }
+    }
+    return @results;
+}
+
 package RT::Interface::Web;
 RT::Base->_ImportOverlays();
 
diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 67f44d16a6..1e42097388 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -329,6 +329,12 @@ sub BuildMainNav {
             $page->child(
                 custom_date_ranges => title => loc('Custom Date Ranges'),
                 path               => "/Prefs/CustomDateRanges.html"
+            )
+        }
+
+        if ( $request_path =~ m{^/Prefs/AuthTokens\.html} ) {
+            $page->child( create_auth_token => title => loc('Create'),
+                raw_html => q[<a class="btn menu-item" href="#create-auth-token" data-toggle="modal" rel="modal:open">].loc("Create")."</a>"
             );
         }
     }
@@ -1356,7 +1362,23 @@ sub _BuildAdminMenu {
                 $page->child( 'summary'   => title => loc('User Summary'),   path => "/User/Summary.html?id=" . $id );
 
                 if ( $current_user->HasRight( Right => 'ManageAuthTokens', Object => RT->System ) ) {
-                    $page->child( auth_tokens => title => loc('Auth Tokens'), path => '/Admin/Users/AuthTokens.html?id=' . $id );
+                    my $auth_tokens = $page->child(
+                        auth_tokens => title => loc('Auth Tokens'),
+                        path        => '/Admin/Users/AuthTokens.html?id=' . $id
+                    );
+
+                    if ( $request_path =~ m{^/Admin/Users/AuthTokens\.html} ) {
+                        $auth_tokens->child(
+                            select_auth_token => title => loc('Select'),
+                            path              => '/Admin/Users/AuthTokens.html?id=' . $id,
+                        );
+                        $auth_tokens->child(
+                            create_auth_token => title => loc('Create'),
+                            raw_html =>
+                                q[<a class="btn menu-item" href="#create-auth-token" data-toggle="modal" rel="modal:open">]
+                                . loc("Create") . "</a>"
+                        );
+                    }
                 }
             }
         }
diff --git a/share/html/Admin/Users/AuthTokens.html b/share/html/Admin/Users/AuthTokens.html
new file mode 100644
index 0000000000..db9adbd30b
--- /dev/null
+++ b/share/html/Admin/Users/AuthTokens.html
@@ -0,0 +1,71 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Admin/Elements/Header, Title => loc("[_1]'s authentication tokens",$UserObj->Name)  &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<& /Elements/AuthToken/List, %ARGS, Owner => $id &>
+
+<%ARGS>
+$id => undef
+</%ARGS>
+<%INIT>
+my $UserObj = RT::User->new( $session{'CurrentUser'} );
+$UserObj->Load( $id );
+unless ( $UserObj->id ) {
+    Abort( loc("Couldn't load user #[_1]", $id) );
+}
+$id = $ARGS{'id'} = $UserObj->id;
+
+my @results = ProcessAuthToken(ARGSRef => \%ARGS);
+MaybeRedirectForResults(
+    Actions   => \@results,
+    Arguments => { id => $id },
+);
+
+</%INIT>
diff --git a/share/html/Elements/AuthToken/Create b/share/html/Elements/AuthToken/Create
new file mode 100644
index 0000000000..d94b60aadf
--- /dev/null
+++ b/share/html/Elements/AuthToken/Create
@@ -0,0 +1,99 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="modal" id="create-auth-token">
+  <div class="modal-dialog modal-dialog-centered" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title"><&|/l&>Create auth token</&></h5>
+        <a id="auth-token-close-modal" href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">×</span>
+        </a>
+      </div>
+      <div class="modal-body">
+        <form method="POST">
+          <input type="hidden" name="Owner" value="<% $Owner %>">
+%         if ( $res{'CanSet'} ){
+          <div class="form-row">
+            <div class="label col-4">
+              <&|/l, $session{'CurrentUser'}->Name()&>[_1]'s current password</&>:
+            </div>
+            <div class="value col-8">
+              <input class="form-control" type="password" name="Password" size="16" autocomplete="off" /></td>
+            </div>
+          </div>
+%         }
+          <div class="form-row">
+            <div class="label col-4">
+              <&|/l&>Description</&>:
+              <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% loc("What's this token for?") %>"></span>
+            </div>
+            <div class="value col-8">
+              <input class="form-control" type="text" name="Description" value="<% $Description %>" size="16" />
+            </div>
+          </div>
+
+          <div class="form-row">
+            <div class="col-12">
+              <& /Elements/Submit, Label => loc("Create"), Name => 'Create' &>
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<%INIT>
+# Don't require password for systems with some form of federated auth
+my %res = $session{'CurrentUser'}->CurrentUserRequireToSetPassword();
+</%INIT>
+
+<%ARGS>
+$Owner
+$Description => ''
+</%ARGS>
diff --git a/share/html/Elements/AuthToken/Edit b/share/html/Elements/AuthToken/Edit
new file mode 100644
index 0000000000..870c562c82
--- /dev/null
+++ b/share/html/Elements/AuthToken/Edit
@@ -0,0 +1,85 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="modal" id="edit-auth-token-<% $Token->id %>">
+  <div class="modal-dialog modal-dialog-centered" role="document">
+    <div class="modal-content">
+      <div class="modal-header">
+        <h5 class="modal-title"><&|/l&>Update auth token</&></h5>
+        <a id="auth-token-close-modal" href="javascript:void(0)" class="close" data-dismiss="modal" aria-label="Close">
+          <span aria-hidden="true">×</span>
+        </a>
+      </div>
+      <div class="modal-body">
+        <form method="POST">
+          <input type="hidden" name="Token" value="<% $Token->Id %>">
+          <div class="form-row">
+            <div class="label col-4">
+              <&|/l&>Description</&>:
+              <span class="far fa-question-circle icon-helper" data-toggle="tooltip" data-placement="top" data-original-title="<% loc("What's this token for?") %>"></span>
+
+            </div>
+            <div class="value col-8">
+              <input class="form-control" type="text" name="Description" value="<% $Token->Description %>" size="16" />
+            </div>
+          </div>
+
+          <div class="form-row justify-content-end">
+            <div class="col-auto">
+              <input type="submit" class="button btn btn-primary" name="Revoke" value="<% loc('Revoke') %>" />
+              <input type="submit" class="button btn btn-primary" name="Update" value="<% loc('Update') %>" />
+            </div>
+          </div>
+        </form>
+      </div>
+    </div>
+  </div>
+</div>
+
+<%ARGS>
+$Token
+</%ARGS>
diff --git a/share/html/Elements/AuthToken/Help b/share/html/Elements/AuthToken/Help
new file mode 100644
index 0000000000..282489ff54
--- /dev/null
+++ b/share/html/Elements/AuthToken/Help
@@ -0,0 +1,52 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<div class="form-row">
+  <div class="col-12">
+    <p><&|/l&>Authentication tokens allow other applications to use your user account without having to share your password, while allowing you to revoke access on an application-specific basis. Changing your password <em>does not</em> invalidate your auth tokens; you must revoke them here.</&></p>
+  </div>
+</div>
diff --git a/share/html/Elements/AuthToken/List b/share/html/Elements/AuthToken/List
new file mode 100644
index 0000000000..59d1196897
--- /dev/null
+++ b/share/html/Elements/AuthToken/List
@@ -0,0 +1,84 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/AuthToken/Help &>
+<& /Elements/AuthToken/Create, Owner => $Owner &>
+
+<div class="authtoken-list mx-auto max-width-sm" data-owner="<% $Owner %>">
+% if ($tokens->Count == 0) {
+  <p class="mt-3 mb-1 ml-3"><&|/l&>No authentication tokens.</&></p>
+% } else {
+  <ul class="list-group">
+% while (my $token = $tokens->Next) {
+    <& Edit, Token => $token &>
+    <li class="list-group-item" id="token-<% $token->Id %>">
+      <div class="d-inline-block mt-1">
+        <span class="description font-weight-bold"><% $token->Description %></span>
+        <span class="last-used font-italic ml-2">
+%       my $used = $token->LastUsedObj;
+%       if ( $used->IsSet ) {
+          <&|/l, $used->AgeAsString &>used [_1]</&>
+%       } else {
+          <&|/l&>never used</&>
+%       }
+        </span>
+      </div>
+      <a class="button btn btn-sm btn-primary float-right" href="#edit-auth-token-<% $token->id %>" data-toggle="modal" rel="modal:open"><% loc('Edit') %></a>
+    </li>
+% }
+  </ul>
+% }
+</div>
+
+<%INIT>
+my $tokens = RT::AuthTokens->new($session{CurrentUser});
+$tokens->LimitOwner(VALUE => $Owner);
+</%INIT>
+
+<%ARGS>
+$Owner
+</%ARGS>
diff --git a/share/html/Prefs/AuthTokens.html b/share/html/Prefs/AuthTokens.html
new file mode 100644
index 0000000000..23b3a2be1c
--- /dev/null
+++ b/share/html/Prefs/AuthTokens.html
@@ -0,0 +1,58 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<& /Elements/Header, Title => loc('My authentication tokens') &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<& /Elements/AuthToken/List, %ARGS, Owner => $session{'CurrentUser'}->Id &>
+
+<%INIT>
+my @results = ProcessAuthToken(ARGSRef => \%ARGS);
+
+MaybeRedirectForResults( Actions => \@results );
+</%INIT>

commit 7ed36c94facb5eea166edf6646b7058771bbe01a
Author: Craig <craig at bestpractical.com>
Date:   Thu May 7 14:18:35 2020 -0400

    Add documentation for using token auth

diff --git a/docs/authentication.pod b/docs/authentication.pod
index 882805ed21..eba5b36be1 100644
--- a/docs/authentication.pod
+++ b/docs/authentication.pod
@@ -15,6 +15,24 @@ may be all you need.  The administration pages under Admin → Users
 provide new user creation as well as password setting and control of RT's
 privileged flag for existing users.
 
+=head1 Token Authentication
+
+Authentication tokens are typically used for accessing RT's REST APIs,
+often L<RT::REST2>. To set up token access, first select an RT user
+account you will use when accessing APIs and give that user account
+appropriate rights to operate on tickets based on what you plan to do
+(read ticket information, create tickets, update tickets, etc.).
+
+You can then give that user the right ManageAuthTokens which will
+add a new option in the menu Logged in as > Settings > AuthTokens.
+
+When setting up token authentication, add the following directive to
+your RT Apache configuration to allow RT to access the Authorization header.
+
+    SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+
+You can find more information about tokens in L<RT::Authen::Token>.
+
 =head1 External Authentication
 
 There are two primary types of external authentication: in one you type your
@@ -85,7 +103,6 @@ An example of using LDAP authentication and HTTP Basic auth:
         Require local
     </Location>
 
-
 =head3 RT Configuration Options
 
 All of the following options control the behavior of RT's built-in external
diff --git a/docs/web_deployment.pod b/docs/web_deployment.pod
index 779401f985..3ba4f83e71 100644
--- a/docs/web_deployment.pod
+++ b/docs/web_deployment.pod
@@ -103,6 +103,16 @@ C<SetHandler modperl>, as the example below uses.
         </Perl>
     </VirtualHost>
 
+=head3 Token Authentication
+
+If you plan to set up token-based access, possibly to use L<RT::REST2>,
+add the following directive to your RT Apache configuration to allow
+RT to access the Authorization header.
+
+    SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+
+More information is available in L<RT::Authen::Token>.
+
 =head2 nginx
 
 C<nginx> requires that you start RT's fastcgi process externally, for

commit ecbcb721de0abadd47d00a17f694d2dc80386d0f
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Wed May 13 16:51:56 2020 -0400

    Show an error message on create failure
    
    Previously the $ok check was an empty block, so failures would
    show the failure message and then "new token" message.
    
    Show appropriate messages on success, and a generic error message
    on failure. Error details are logged.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index ef5b059bec..35aa50d5d0 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -5019,13 +5019,17 @@ sub ProcessAuthToken {
                 Description => $args_ref->{Description},
             );
             if ($ok) {
+                push @results, $msg;
+                push @results,
+                    loc(
+                        '"[_1]" is your new authentication token. Treat it carefully like a password. Please save it now because you cannot access it again.',
+                        $auth_string
+                    );
+            }
+            else {
+                push @results, loc('Unable to create a new authentication token. Contact your RT administrator.');
+                RT->Logger->error('Unable to create authentication token: ' . $msg);
             }
-            push @results, $msg;
-            push @results,
-                loc(
-                '"[_1]" is your new authentication token. Treat it carefully like a password. Please save it now because you cannot access it again.',
-                $auth_string
-                );
         }
     }
     elsif ( $args_ref->{Update} || $args_ref->{Revoke} ) {

commit a90d185b5680e9331cdcdd6119e359170c4152c2
Merge: b1146b795f ecbcb721de
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu May 14 05:39:47 2020 +0800

    Merge branch '5.0/core-authen-token' into 5.0-trunk


-----------------------------------------------------------------------


More information about the rt-commit mailing list