[Rt-commit] rt branch, 5.0/core-authen-token, created. rt-5.0.0alpha1-417-ge06249475c

? sunnavy sunnavy at bestpractical.com
Mon May 11 16:14:44 EDT 2020


The branch, 5.0/core-authen-token has been created
        at  e06249475ca0682d92b950ceac95481cf7ffba4c (commit)

- Log -----------------------------------------------------------------
commit 6417195ce92fdeb14c4f953c7436f9b116d2995a
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 21f54c46c1..060cc1bb05 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -506,6 +506,7 @@ sub InitClasses {
     require RT::CustomFieldValues::Canonicalizer;
     require RT::Configuration;
     require RT::Configurations;
+    require RT::Authen::Token;
 
     _BuildTableAttributes();
 
@@ -771,6 +772,7 @@ our %CORED_PLUGINS = (
     'RT::Extension::PriorityAsString' => '5.0',
     'RT::Extension::AssetSQL' => '5.0',
     'RT::Extension::LifecycleUI' => '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 9da5b21eb2389aced08b0ddabe39941ba9d29522
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 8b412ee946e2c5097bf17c508caaaaa40d15c47c
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 e1dbab219c6b4161c324dd2814e146c6850d3a48
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 e06249475ca0682d92b950ceac95481cf7ffba4c
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..a605b5c70c 100644
--- a/docs/authentication.pod
+++ b/docs/authentication.pod
@@ -86,6 +86,11 @@ An example of using LDAP authentication and HTTP Basic auth:
     </Location>
 
 
+If you are using token authentication then add the following directive to
+your RT Apache configuration to allow RT to access the Authorization header.
+
+    SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+
 =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..c698c99666 100644
--- a/docs/web_deployment.pod
+++ b/docs/web_deployment.pod
@@ -32,6 +32,11 @@ spontaneously logged in as other users in the system.
 See also L<authentication/Apache configuration>, in case you intend to
 use Apache to provide authentication.
 
+If you are using token authentication then add the following directive to
+your RT Apache configuration to allow RT to access the Authorization header.
+
+    SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
+
 =head3 mod_fcgid
 
 B<WARNING>: Before mod_fcgid 2.3.6, the maximum request size was 1GB.

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


More information about the rt-commit mailing list