@@ -, +, @@ authentication in API - X-Koha-Username: userid of borrower - X-Koha-Timestamp: timestamp of the request - X-Koha-Signature: signature of the request - HTTP method (uppercase) - URL path and query string - username - timestamp of the request --- Koha/ApiTimestamp.pm | 46 +++++++++++++++++++ Koha/ApiTimestamps.pm | 58 ++++++++++++++++++++++++ Koha/REST/V1.pm | 63 ++++++++++++++++++++++++-- Koha/Schema/Result/ApiTimestamp.pm | 81 ++++++++++++++++++++++++++++++++++ installer/data/mysql/kohastructure.sql | 15 +++++++ installer/data/mysql/updatedatabase.pl | 21 +++++++++ 6 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 Koha/ApiTimestamp.pm create mode 100644 Koha/ApiTimestamps.pm create mode 100644 Koha/Schema/Result/ApiTimestamp.pm --- a/Koha/ApiTimestamp.pm +++ a/Koha/ApiTimestamp.pm @@ -0,0 +1,46 @@ +package Koha::ApiTimestamp; + +# Copyright BibLibre 2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use base qw(Koha::Object); + +=head1 NAME + +Koha::ApiTimestamp - Koha API Timestamp Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub type { + return 'ApiTimestamp'; +} + +1; --- a/Koha/ApiTimestamps.pm +++ a/Koha/ApiTimestamps.pm @@ -0,0 +1,58 @@ +package Koha::ApiTimestamps; + +# Copyright BibLibre 2015 +# +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation; either version 3 of the License, or (at your option) any later +# version. +# +# Koha 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 Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Carp; + +use Koha::Database; + +use Koha::Borrower; + +use base qw(Koha::Objects); + +=head1 NAME + +Koha::ApiTimestamps - Koha API Timestamps Object class + +=head1 API + +=head2 Class Methods + +=cut + +=head3 type + +=cut + +sub type { + return 'ApiTimestamp'; +} + +sub object_class { + return 'Koha::ApiTimestamp'; +} + +=head1 AUTHOR + +Kyle M Hall + +=cut + +1; --- a/Koha/REST/V1.pm +++ a/Koha/REST/V1.pm @@ -4,6 +4,16 @@ use Modern::Perl; use Mojo::Base 'Mojolicious'; use Mojo::Log; +use Digest::SHA qw(hmac_sha256_hex); + +use Koha::Borrower; +use Koha::Borrowers; +use Koha::ApiKey; +use Koha::ApiKeys; +use Koha::ApiTimestamp; +use Koha::ApiTimestamps; + + =head startup Starts the Mojolicious application using whatever server is configured. @@ -71,9 +81,56 @@ sub startup { my $route = $self->routes->under->to( cb => sub { my $c = shift; - my $user = $c->param('user'); - # Do the authentication stuff here... - $c->stash('user', $user); + my $req_username = $c->req->headers->header('X-Koha-Username'); + my $req_timestamp = $c->req->headers->header('X-Koha-Timestamp'); + my $req_signature = $c->req->headers->header('X-Koha-Signature'); + my $req_method = $c->req->method; + my $req_url = '/' . $c->req->url->path_query; + + # "Anonymous" mode + return 1 + unless ( defined($req_username) + or defined($req_timestamp) + or defined($req_signature) ); + + my $borrower = Koha::Borrowers->find({userid => $req_username}); + + my @apikeys = Koha::ApiKeys->search({ + borrowernumber => $borrower->borrowernumber, + active => 1, + }); + + my $message = "$req_method $req_url $req_username $req_timestamp"; + my $signature = ''; + foreach my $apikey (@apikeys) { + $signature = hmac_sha256_hex($message, $apikey->api_key); + + last if $signature eq $req_signature; + } + + unless ($signature eq $req_signature) { + $c->res->code(403); + $c->render(json => { error => "Authentication failed" }); + return; + } + + my $api_timestamp = Koha::ApiTimestamps->find($borrower->borrowernumber); + my $timestamp = $api_timestamp ? $api_timestamp->timestamp : 0; + unless ($timestamp < $req_timestamp) { + $c->res->code(403); + $c->render(json => { error => "Bad timestamp" }); + return; + } + + unless ($api_timestamp) { + $api_timestamp = new Koha::ApiTimestamp; + $api_timestamp->borrowernumber($borrower->borrowernumber); + } + $api_timestamp->timestamp($req_timestamp); + $api_timestamp->store; + + # Authentication succeeded, store authenticated user in stash + $c->stash('user', $borrower); return 1; } ); --- a/Koha/Schema/Result/ApiTimestamp.pm +++ a/Koha/Schema/Result/ApiTimestamp.pm @@ -0,0 +1,81 @@ +use utf8; +package Koha::Schema::Result::ApiTimestamp; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Koha::Schema::Result::ApiTimestamp + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("api_timestamps"); + +=head1 ACCESSORS + +=head2 borrowernumber + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 timestamp + + data_type: 'bigint' + is_nullable: 1 + +=cut + +__PACKAGE__->add_columns( + "borrowernumber", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "timestamp", + { data_type => "bigint", is_nullable => 1 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("borrowernumber"); + +=head1 RELATIONS + +=head2 borrowernumber + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "borrowernumber", + "Koha::Schema::Result::Borrower", + { borrowernumber => "borrowernumber" }, + { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07025 @ 2015-03-24 08:27:29 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FfKz14uY/UgVOWcF4T5J8A + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; --- a/installer/data/mysql/kohastructure.sql +++ a/installer/data/mysql/kohastructure.sql @@ -32,6 +32,21 @@ CREATE TABLE api_keys ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -- +-- Table structure for table api_timestamps +-- + +DROP TABLE IF EXISTS api_timestamps; +CREATE TABLE api_timestamps ( + borrowernumber int(11) NOT NULL, -- foreign key to the borrowers table + timestamp bigint, -- timestamp of the last request from this user + PRIMARY KEY (borrowernumber), + CONSTRAINT api_timestamps_fk_borrowernumber + FOREIGN KEY (borrowernumber) + REFERENCES borrowers (borrowernumber) + ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- -- Table structure for table `auth_header` -- --- a/installer/data/mysql/updatedatabase.pl +++ a/installer/data/mysql/updatedatabase.pl @@ -10663,6 +10663,27 @@ if(CheckVersion($DBversion)) { SetVersion($DBversion); } +$DBversion = "XXX"; +if(CheckVersion($DBversion)) { + $dbh->do(q{ + DROP TABLE IF EXISTS api_timestamps; + }); + $dbh->do(q{ + CREATE TABLE api_timestamps ( + borrowernumber int(11) NOT NULL, + timestamp bigint, + PRIMARY KEY (borrowernumber), + CONSTRAINT api_timestamps_fk_borrowernumber + FOREIGN KEY (borrowernumber) + REFERENCES borrowers (borrowernumber) + ON DELETE CASCADE ON UPDATE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + }); + + print "Upgrade to $DBversion done (Bug 13920: Add API timestamps table)\n"; + SetVersion($DBversion); +} + # DEVELOPER PROCESS, search for anything to execute in the db_update directory # SEE bug 13068 --