From e9f9a96c4e45b0f8748b616114b49a569d6a2cdd Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Thu, 4 Aug 2016 13:33:14 +0300 Subject: [PATCH] Bug 17004: Add API route to authenticate patron (CGISESSID) including logout POST /auth/session (login) DELETE /auth/session (logout) Required POST data: - "password" - either "userid" or "cardnumber". To test: 1. Make sure you are logged out from Koha. 2. Make a POST request to http://yourlibrary/api/v1/auth/session with form data "userid" => and "password" => . 3. If your userid and password is correct, you should be returned with most basic patron data and your CGISESSID. 4. Also attempt with invalid login to get an error. 5. Send a DELETE request to /auth/session 6. Observe that you are no longer logged-in in Koha. 7. Run tests at b/t/db_dependent/api/v1/auth.t You may find this cURL useful: curl -X DELETE http://lib/api/v1/auth/session --cookie 'CGISESSID=88e735aaf7c6cf194a775425cbd00570' (replace CGISESSID=... with your CGISESSID) ---- I've rebased both attachments to master (restructuralized swagger definitions). And added proposed permissions as proposed by Benjamin Rokseth. Btw, very nice work Lari! Signed-off-by: Benjamin Rokseth --- Koha/REST/V1/Auth.pm | 101 +++++++++++++++++++++ api/v1/swagger/definitions.json | 3 + api/v1/swagger/definitions/session.json | 32 +++++++ api/v1/swagger/definitions/sessionid.json | 10 +++ api/v1/swagger/parameters.json | 12 +++ api/v1/swagger/parameters/auth.json | 31 +++++++ api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/auth.json | 58 +++++++++++++ t/db_dependent/api/v1/auth.t | 140 ++++++++++++++++++++++++++++++ 9 files changed, 390 insertions(+) create mode 100644 Koha/REST/V1/Auth.pm create mode 100644 api/v1/swagger/definitions/session.json create mode 100644 api/v1/swagger/definitions/sessionid.json create mode 100644 api/v1/swagger/parameters/auth.json create mode 100644 api/v1/swagger/paths/auth.json create mode 100644 t/db_dependent/api/v1/auth.t diff --git a/Koha/REST/V1/Auth.pm b/Koha/REST/V1/Auth.pm new file mode 100644 index 0000000..58c8b80 --- /dev/null +++ b/Koha/REST/V1/Auth.pm @@ -0,0 +1,101 @@ +package Koha::REST::V1::Auth; + +# Copyright 2016 Koha-Suomi +# +# 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 Mojo::Base 'Mojolicious::Controller'; + +use Koha::Patrons; +use C4::Auth; + +use CGI; + +sub login { + my ($c, $args, $cb) = @_; + + my $userid = $args->{userid} || $args->{cardnumber}; + my $password = $args->{password}; + my $patron; + + return $c->$cb({ error => "Either userid or cardnumber is required " + ."- neither given." }, 403) unless ($userid); + + my $cgi = CGI->new; + $cgi->param(userid => $userid); + $cgi->param(password => $password); + my ($status, $cookie, $sessionid) = C4::Auth::check_api_auth($cgi); + + return $c->$cb({ error => "Login failed." }, 401) if $status eq "failed"; + return $c->$cb({ error => "Session expired." }, 401) if $status eq "expired"; + return $c->$cb({ error => "Database is under maintenance." }, 401) if $status eq "maintenance"; + return $c->$cb({ error => "Login failed." }, 401) unless $status eq "ok"; + + $patron = Koha::Patrons->find({ userid => $userid }) unless $patron; + $patron = Koha::Patrons->find({ cardnumber => $userid }) unless $patron; + + my $session = _swaggerize_session($sessionid, $patron); + + $c->cookie(CGISESSID => $sessionid, { path => "/" }); + + return $c->$cb($session, 201); +} + +sub logout { + my ($c, $args, $cb) = @_; + + my $sessionid = $args->{session}->{sessionid}; + $sessionid = $c->cookie('CGISESSID') unless $sessionid; + + my ($status, $sid) = C4::Auth::check_cookie_auth($sessionid); + unless ($status eq "ok") { + return $c->$cb({ error => "Invalid session id."}, 401); + } + + $c->cookie(CGISESSID => $sessionid, { path => "/", expires => 1 }); + + my $session = C4::Auth::get_session($sessionid); + $session->delete; + $session->flush; + return $c->$cb({}, 200); +} + +sub _swaggerize_session { + my ($sessionid, $patron) = @_; + + return unless ref($patron) eq 'Koha::Patron'; + + my $rawPermissions = C4::Auth::haspermission($patron->userid); # defaults to all permissions + my @permissions; + + # delete all empty permissions + while ( my ($key, $val) = each %{$rawPermissions} ) { + push @permissions, $key if $val; + } + + return { + borrowernumber => $patron->borrowernumber, + firstname => $patron->firstname, + surname => $patron->surname, + email => $patron->email, + sessionid => $sessionid, + permissions => \@permissions, + }; +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index e4d7427..a17c793 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -1,4 +1,7 @@ { + "session": { + "$ref": "definitions/session.json" + }, "patron": { "$ref": "definitions/patron.json" }, diff --git a/api/v1/swagger/definitions/session.json b/api/v1/swagger/definitions/session.json new file mode 100644 index 0000000..253dc23 --- /dev/null +++ b/api/v1/swagger/definitions/session.json @@ -0,0 +1,32 @@ +{ + "type": "object", + "properties": { + "borrowernumber": { + "type": "string", + "description": "internally assigned user identifier" + }, + "email": { + "type": ["string", "null"], + "description": "primary email address for patron's primary address" + }, + "surname": { + "type": "string", + "description": "patron's last name" + }, + "firstname": { + "type": ["string", "null"], + "description": "patron's first name" + }, + "sessionid": { + "description": "Koha Session identifier", + "type": "string" + }, + "permissions": { + "description": "patron's permissions", + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/api/v1/swagger/definitions/sessionid.json b/api/v1/swagger/definitions/sessionid.json new file mode 100644 index 0000000..6e013aa --- /dev/null +++ b/api/v1/swagger/definitions/sessionid.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "sessionid": { + "description": "The Koha sessionid", + "type": "string" + } + }, + "required": ["sessionid"] +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index b20e19f..87380a8 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -7,5 +7,17 @@ }, "holdIdPathParam": { "$ref": "parameters/hold.json#/holdIdPathParam" + }, + "cardnumberPostParam": { + "$ref": "parameters/auth.json#/cardnumberPostParam" + }, + "passwordPostParam": { + "$ref": "parameters/auth.json#/passwordPostParam" + }, + "useridPostParam": { + "$ref": "parameters/auth.json#/useridPostParam" + }, + "sessionidBodyParam": { + "$ref": "parameters/auth.json#/sessionidBodyParam" } } diff --git a/api/v1/swagger/parameters/auth.json b/api/v1/swagger/parameters/auth.json new file mode 100644 index 0000000..05fdbc1 --- /dev/null +++ b/api/v1/swagger/parameters/auth.json @@ -0,0 +1,31 @@ +{ + "cardnumberPostParam": { + "name": "cardnumber", + "in": "formData", + "description": "Borrower's card's barcode/identifier", + "required": false, + "type": "string" + }, + "passwordPostParam": { + "name": "password", + "in": "formData", + "required": true, + "type": "string" + }, + "useridPostParam": { + "name": "userid", + "in": "formData", + "description": "The userid of the Borrower, unique value", + "required": false, + "type": "string" + }, + "sessionidBodyParam": { + "name": "session", + "description": "The CGISESSID Cookie used to authenticate a session", + "in": "body", + "required": false, + "schema" : { + "$ref": "../definitions/sessionid.json" + } + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index f00b1b4..2e8fd38 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -1,4 +1,7 @@ { + "/auth/session": { + "$ref": "paths/auth.json#/~1auth~1session" + }, "/holds": { "$ref": "paths/holds.json#/~1holds" }, diff --git a/api/v1/swagger/paths/auth.json b/api/v1/swagger/paths/auth.json new file mode 100644 index 0000000..bf190a9 --- /dev/null +++ b/api/v1/swagger/paths/auth.json @@ -0,0 +1,58 @@ +{ + "/auth/session": { + "post": { + "operationId": "loginAuth", + "tags": ["auth"], + "summary": "Login to Koha and get a session cookie", + "description": "Makes a 'normal' username + password login to Koha, and returns the sessionid you need put to the CGISESSID-cookie. Koha uses this cookie to track a session.\nBe aware that the authenticated session most probably is IP-locked so authenticating from one IP and passing the session to another wont work.", + "parameters": [ + { "$ref": "../parameters.json#/cardnumberPostParam" }, + { "$ref": "../parameters.json#/useridPostParam" }, + { "$ref": "../parameters.json#/passwordPostParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "201": { + "description": "A borrower with SSO-relevant fields", + "schema": { + "$ref": "../definitions.json#/session" + } + }, + "401": { + "description": "Bad username/cardnumber and/or password", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + }, + "delete": { + "operationId": "logoutAuth", + "tags": ["auth"], + "summary": "Logout from Koha.", + "description": "Logouts user from Koha by marking session as expired. sessionid is optional, if not given, logs out currently logged in user", + "parameters": [ + { "$ref": "../parameters.json#/sessionidBodyParam" } + ], + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Successfully logged out", + "schema": { + "type": "object" + } + }, + "401": { + "description": "Bad session id", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + } + } +} diff --git a/t/db_dependent/api/v1/auth.t b/t/db_dependent/api/v1/auth.t new file mode 100644 index 0000000..56d1a87 --- /dev/null +++ b/t/db_dependent/api/v1/auth.t @@ -0,0 +1,140 @@ +#!/usr/bin/env perl + +# Copyright 2016 Koha-Suomi +# +# 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 Test::More tests => 38; +use Test::Mojo; + +use t::lib::TestBuilder; + +use C4::Auth; +use C4::Context; +use Koha::AuthUtils; + +my $builder = t::lib::TestBuilder->new(); + +my $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; + +$ENV{REMOTE_ADDR} = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; +my $password = "2anxious? if someone finds out"; + +my $borrower = $builder->build({ + source => 'Borrower', + value => { + branchcode => $branchcode, + categorycode => $categorycode, + password => Koha::AuthUtils::hash_password($password), + } +}); + +my $auth_by_userid = { + userid => $borrower->{userid}, + password => $password, +}; +my $auth_by_cardnumber = { + cardnumber => $borrower->{cardnumber}, + password => $password, +}; +my $invalid_login = { + userid => $borrower->{userid}, + password => "please let me in", +}; +my $invalid_login2 = { + cardnumber => $borrower->{cardnumber}, + password => "my password is password, don't tell anyone", +}; + +my $tx = $t->ua->build_tx(POST => '/api/v1/auth/session' => form => $auth_by_userid); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(201) + ->json_is('/firstname', $borrower->{firstname}) + ->json_is('/surname', $borrower->{surname}) + ->json_is('/borrowernumber', $borrower->{borrowernumber}) + ->json_is('/email', $borrower->{email}) + ->json_has('/sessionid'); + +$tx = $t->ua->build_tx(POST => '/api/v1/auth/session' => form => $auth_by_cardnumber); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(201) + ->json_is('/firstname', $borrower->{firstname}) + ->json_is('/surname', $borrower->{surname}) + ->json_is('/borrowernumber', $borrower->{borrowernumber}) + ->json_is('/email', $borrower->{email}) + ->json_has('/sessionid'); +my $sessionid = $tx->res->json->{sessionid}; + +$tx = $t->ua->build_tx(POST => '/api/v1/auth/session' => form => $invalid_login); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(401) + ->json_is('/error', "Login failed."); + +$tx = $t->ua->build_tx(POST => '/api/v1/auth/session' => form => $invalid_login2); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(401) + ->json_is('/error', "Login failed."); + +$tx = $t->ua->build_tx(DELETE => '/api/v1/auth/session' => json => { sessionid => $sessionid."123" }); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(401) + ->json_is('/error', "Invalid session id."); + +my ($sess_status, $sid) = C4::Auth::check_cookie_auth($sessionid); +is($sess_status, "ok", "Session is valid before logging out."); +$tx = $t->ua->build_tx(DELETE => '/api/v1/auth/session' => json => { sessionid => $sessionid }); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +($sess_status, $sid) = C4::Auth::check_cookie_auth($sessionid); +isnt($sess_status, "ok", "Session is not valid after logging out."); + +$tx = $t->ua->build_tx(POST => '/api/v1/auth/session' => form => $auth_by_cardnumber); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(201) + ->json_is('/firstname', $borrower->{firstname}) + ->json_is('/surname', $borrower->{surname}) + ->json_is('/borrowernumber', $borrower->{borrowernumber}) + ->json_is('/email', $borrower->{email}) + ->json_has('/sessionid'); +$sessionid = $tx->res->json->{sessionid}; + +($sess_status, $sid) = C4::Auth::check_cookie_auth($sessionid); +is($sess_status, "ok", "Session is valid before logging out."); +$tx = $t->ua->build_tx(DELETE => '/api/v1/auth/session'); +$tx->req->env({REMOTE_ADDR => '127.0.0.1'}); +$t->request_ok($tx) + ->status_is(200); + +($sess_status, $sid) = C4::Auth::check_cookie_auth($sessionid); +isnt($sess_status, "ok", "Session is not valid after logging out."); + +$dbh->rollback; -- 2.1.4