View | Details | Raw Unified | Return to bug 20402
Collapse All | Expand All

(-)a/C4/Installer/PerlDependencies.pm (+10 lines)
Lines 888-893 our $PERL_DEPS = { Link Here
888
        required   => 0,
888
        required   => 0,
889
        min_ver    => '0.52',
889
        min_ver    => '0.52',
890
    },
890
    },
891
    'Net::OAuth2::AuthorizationServer' => {
892
        usage    => 'REST API',
893
        required => '1',
894
        min_ver  => '0.16',
895
    },
896
    'Mojolicious::Plugin::OAuth2::Server' => {
897
        usage    => 'REST API',
898
        required => '1',
899
        min_ver  => '0.40',
900
    }
891
};
901
};
892
902
893
1;
903
1;
(-)a/Koha/OAuth.pm (+69 lines)
Line 0 Link Here
1
package Koha::OAuth;
2
3
use Modern::Perl;
4
use Koha::OAuthAccessTokens;
5
use Koha::OAuthAccessToken;
6
7
sub config {
8
    return {
9
        verify_client_cb => \&_verify_client_cb,
10
        store_access_token_cb => \&_store_access_token_cb,
11
        verify_access_token_cb => \&_verify_access_token_cb
12
    };
13
}
14
15
sub _verify_client_cb {
16
    my (%args) = @_;
17
18
    my ($client_id, $client_secret)
19
        = @args{ qw/ client_id client_secret / };
20
21
    return (0, 'unauthorized_client') unless $client_id;
22
23
    my $clients = C4::Context->config('api_client');
24
    $clients = [ $clients ] unless ref $clients eq 'ARRAY';
25
    my ($client) = grep { $_->{client_id} eq $client_id } @$clients;
26
    return (0, 'unauthorized_client') unless $client;
27
28
    return (0, 'access_denied') unless $client_secret eq $client->{client_secret};
29
30
    return (1, undef, []);
31
}
32
33
sub _store_access_token_cb {
34
    my ( %args ) = @_;
35
36
    my ( $client_id, $access_token, $expires_in )
37
        = @args{ qw/ client_id access_token expires_in / };
38
39
    my $at = Koha::OAuthAccessToken->new({
40
        access_token  => $access_token,
41
        expires       => time + $expires_in,
42
        client_id     => $client_id,
43
    });
44
    $at->store;
45
46
    return;
47
}
48
49
sub _verify_access_token_cb {
50
    my (%args) = @_;
51
52
    my $access_token = $args{access_token};
53
54
    my $at = Koha::OAuthAccessTokens->find($access_token);
55
    if ($at) {
56
        if ( $at->expires <= time ) {
57
            # need to revoke the access token
58
            $at->delete;
59
60
            return (0, 'invalid_grant')
61
        }
62
63
        return $at->unblessed;
64
    }
65
66
    return (0, 'invalid_grant')
67
};
68
69
1;
(-)a/Koha/OAuthAccessToken.pm (+11 lines)
Line 0 Link Here
1
package Koha::OAuthAccessToken;
2
3
use Modern::Perl;
4
5
use base qw(Koha::Object);
6
7
sub _type {
8
    return 'OauthAccessToken';
9
}
10
11
1;
(-)a/Koha/OAuthAccessTokens.pm (+15 lines)
Line 0 Link Here
1
package Koha::OAuthAccessTokens;
2
3
use Modern::Perl;
4
5
use base qw(Koha::Objects);
6
7
sub object_class {
8
    return 'Koha::OAuthAccessToken';
9
}
10
11
sub _type {
12
    return 'OauthAccessToken';
13
}
14
15
1;
(-)a/Koha/REST/V1.pm (+3 lines)
Lines 19-24 use Modern::Perl; Link Here
19
19
20
use Mojo::Base 'Mojolicious';
20
use Mojo::Base 'Mojolicious';
21
21
22
use Koha::OAuth;
23
22
use C4::Context;
24
use C4::Context;
23
25
24
=head1 NAME
26
=head1 NAME
Lines 51-56 sub startup { Link Here
51
        $self->secrets([$secret_passphrase]);
53
        $self->secrets([$secret_passphrase]);
52
    }
54
    }
53
55
56
    $self->plugin('OAuth2::Server' => Koha::OAuth::config);
54
    $self->plugin(OpenAPI => {
57
    $self->plugin(OpenAPI => {
55
        url => $self->home->rel_file("api/v1/swagger/swagger.json"),
58
        url => $self->home->rel_file("api/v1/swagger/swagger.json"),
56
        route => $self->routes->under('/api/v1')->to('Auth#under'),
59
        route => $self->routes->under('/api/v1')->to('Auth#under'),
(-)a/Koha/REST/V1/Auth.pm (+27 lines)
Lines 22-31 use Modern::Perl; Link Here
22
use Mojo::Base 'Mojolicious::Controller';
22
use Mojo::Base 'Mojolicious::Controller';
23
23
24
use C4::Auth qw( check_cookie_auth get_session haspermission );
24
use C4::Auth qw( check_cookie_auth get_session haspermission );
25
use C4::Context;
25
26
26
use Koha::Account::Lines;
27
use Koha::Account::Lines;
27
use Koha::Checkouts;
28
use Koha::Checkouts;
28
use Koha::Holds;
29
use Koha::Holds;
30
use Koha::OAuth;
29
use Koha::Old::Checkouts;
31
use Koha::Old::Checkouts;
30
use Koha::Patrons;
32
use Koha::Patrons;
31
33
Lines 110-115 sub authenticate_api_request { Link Here
110
112
111
    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
113
    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
112
    my $authorization = $spec->{'x-koha-authorization'};
114
    my $authorization = $spec->{'x-koha-authorization'};
115
116
    if (my $oauth = $c->oauth) {
117
        my $clients = C4::Context->config('api_client');
118
        $clients = [ $clients ] unless ref $clients eq 'ARRAY';
119
        my ($client) = grep { $_->{client_id} eq $oauth->{client_id} } @$clients;
120
121
        my $patron = Koha::Patrons->find($client->{patron_id});
122
        my $permissions = $authorization->{'permissions'};
123
        # Check if the patron is authorized
124
        if ( haspermission($patron->userid, $permissions)
125
            or allow_owner($c, $authorization, $patron)
126
            or allow_guarantor($c, $authorization, $patron) ) {
127
128
            validate_query_parameters( $c, $spec );
129
130
            # Everything is ok
131
            return 1;
132
        }
133
134
        Koha::Exceptions::Authorization::Unauthorized->throw(
135
            error => "Authorization failure. Missing required permission(s).",
136
            required_permissions => $permissions,
137
        );
138
    }
139
113
    my $cookie = $c->cookie('CGISESSID');
140
    my $cookie = $c->cookie('CGISESSID');
114
    my ($session, $user);
141
    my ($session, $user);
115
    # Mojo doesn't use %ENV the way CGI apps do
142
    # Mojo doesn't use %ENV the way CGI apps do
(-)a/Koha/REST/V1/OAuth.pm (+62 lines)
Line 0 Link Here
1
package Koha::REST::V1::OAuth;
2
3
use Modern::Perl;
4
5
use Mojo::Base 'Mojolicious::Controller';
6
7
use Net::OAuth2::AuthorizationServer;
8
use Koha::OAuth;
9
10
use C4::Context;
11
12
sub token {
13
    my $c = shift->openapi->valid_input or return;
14
15
    my $grant_type = $c->validation->param('grant_type');
16
    unless ($grant_type eq 'client_credentials') {
17
        return $c->render(status => 400, openapi => {error => 'Unimplemented grant type'});
18
    }
19
20
    my $client_id = $c->validation->param('client_id');
21
    my $client_secret = $c->validation->param('client_secret');
22
23
    my $cb = "${grant_type}_grant";
24
    my $server = Net::OAuth2::AuthorizationServer->new;
25
    my $grant = $server->$cb(Koha::OAuth::config);
26
27
    # verify a client against known clients
28
    my ( $is_valid, $error ) = $grant->verify_client(
29
        client_id     => $client_id,
30
        client_secret => $client_secret,
31
    );
32
33
    unless ($is_valid) {
34
        return $c->render(status => 403, openapi => {error => $error});
35
    }
36
37
    # generate a token
38
    my $token = $grant->token(
39
        client_id => $client_id,
40
        type      => 'access',
41
    );
42
43
    # store access token
44
    my $expires_in = 3600;
45
    $grant->store_access_token(
46
        client_id    => $client_id,
47
        access_token => $token,
48
        expires_in   => $expires_in,
49
    );
50
51
    my $at = Koha::OAuthAccessTokens->search({ access_token => $token })->next;
52
53
    my $response = {
54
        access_token => $token,
55
        token_type => 'Bearer',
56
        expires_in => $expires_in,
57
    };
58
59
    return $c->render(status => 200, openapi => $response);
60
}
61
62
1;
(-)a/Koha/Schema/Result/OauthAccessToken.pm (+72 lines)
Line 0 Link Here
1
use utf8;
2
package Koha::Schema::Result::OauthAccessToken;
3
4
# Created by DBIx::Class::Schema::Loader
5
# DO NOT MODIFY THE FIRST PART OF THIS FILE
6
7
=head1 NAME
8
9
Koha::Schema::Result::OauthAccessToken
10
11
=cut
12
13
use strict;
14
use warnings;
15
16
use base 'DBIx::Class::Core';
17
18
=head1 TABLE: C<oauth_access_tokens>
19
20
=cut
21
22
__PACKAGE__->table("oauth_access_tokens");
23
24
=head1 ACCESSORS
25
26
=head2 access_token
27
28
  data_type: 'varchar'
29
  is_nullable: 0
30
  size: 255
31
32
=head2 client_id
33
34
  data_type: 'varchar'
35
  is_nullable: 0
36
  size: 255
37
38
=head2 expires
39
40
  data_type: 'integer'
41
  is_nullable: 0
42
43
=cut
44
45
__PACKAGE__->add_columns(
46
  "access_token",
47
  { data_type => "varchar", is_nullable => 0, size => 255 },
48
  "client_id",
49
  { data_type => "varchar", is_nullable => 0, size => 255 },
50
  "expires",
51
  { data_type => "integer", is_nullable => 0 },
52
);
53
54
=head1 PRIMARY KEY
55
56
=over 4
57
58
=item * L</access_token>
59
60
=back
61
62
=cut
63
64
__PACKAGE__->set_primary_key("access_token");
65
66
67
# Created by DBIx::Class::Schema::Loader v0.07046 @ 2018-04-11 17:44:30
68
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:u2e++Jrwln4Qhi3UPx2CQA
69
70
71
# You can replace this text with custom code or comments, and it will be preserved on regeneration
72
1;
(-)a/api/v1/swagger/paths.json (+3 lines)
Lines 1-4 Link Here
1
{
1
{
2
  "/oauth/token": {
3
    "$ref": "paths/oauth.json#/~1oauth~1token"
4
  },
2
  "/acquisitions/vendors": {
5
  "/acquisitions/vendors": {
3
    "$ref": "paths/acquisitions_vendors.json#/~1acquisitions~1vendors"
6
    "$ref": "paths/acquisitions_vendors.json#/~1acquisitions~1vendors"
4
  },
7
  },
(-)a/api/v1/swagger/paths/oauth.json (+64 lines)
Line 0 Link Here
1
{
2
    "/oauth/token": {
3
        "post": {
4
            "x-mojo-to": "OAuth#token",
5
            "operationId": "tokenOAuth",
6
            "tags": ["oauth"],
7
            "produces": [
8
                "application/json"
9
            ],
10
            "parameters": [
11
                {
12
                    "name": "grant_type",
13
                    "in": "formData",
14
                    "description": "grant type (client_credentials)",
15
                    "required": true,
16
                    "type": "string"
17
                },
18
                {
19
                    "name": "client_id",
20
                    "in": "formData",
21
                    "description": "client id",
22
                    "type": "string"
23
                },
24
                {
25
                    "name": "client_secret",
26
                    "in": "formData",
27
                    "description": "client secret",
28
                    "type": "string"
29
                }
30
            ],
31
            "responses": {
32
                "200": {
33
                    "description": "OK",
34
                    "schema": {
35
                        "type": "object",
36
                        "properties": {
37
                            "access_token": {
38
                                "type": "string"
39
                            },
40
                            "token_type": {
41
                                "type": "string"
42
                            },
43
                            "expires_in": {
44
                                "type": "integer"
45
                            }
46
                        }
47
                    }
48
                },
49
                "400": {
50
                    "description": "Bad Request",
51
                    "schema": {
52
                        "$ref": "../definitions.json#/error"
53
                    }
54
                },
55
                "403": {
56
                    "description": "Access forbidden",
57
                    "schema": {
58
                        "$ref": "../definitions.json#/error"
59
                    }
60
                }
61
            }
62
        }
63
    }
64
}
(-)a/etc/koha-conf.xml (+15 lines)
Lines 127-132 __PAZPAR2_TOGGLE_XML_POST__ Link Here
127
 <!-- Secret passphrase used by Mojolicious for signed cookies -->
127
 <!-- Secret passphrase used by Mojolicious for signed cookies -->
128
 <api_secret_passphrase>CHANGEME</api_secret_passphrase>
128
 <api_secret_passphrase>CHANGEME</api_secret_passphrase>
129
129
130
 <!-- Uncomment and modify the following to enable OAuth2 authentication for the
131
      REST API -->
132
 <!--
133
 <api_client>
134
    <client_id>client1</client_id>
135
    <client_secret>secret1</client_secret>
136
    <patron_id>1</patron_id>
137
 </api_client>
138
 <api_client>
139
    <client_id>client2</client_id>
140
    <client_secret>secret2</client_secret>
141
    <patron_id>2</patron_id>
142
 </api_client>
143
 -->
144
130
 <!-- true type font mapping accoding to type from $font_types in C4/Creators/Lib.pm -->
145
 <!-- true type font mapping accoding to type from $font_types in C4/Creators/Lib.pm -->
131
 <ttf>
146
 <ttf>
132
    <font type="TR" >__FONT_DIR__/DejaVuSerif.ttf</font>
147
    <font type="TR" >__FONT_DIR__/DejaVuSerif.ttf</font>
(-)a/installer/data/mysql/atomicupdate/oauth_tokens.perl (+15 lines)
Line 0 Link Here
1
$DBversion = 'XXX';
2
if (CheckVersion($DBversion)) {
3
    $dbh->do(q{DROP TABLE IF EXISTS oauth_access_tokens});
4
    $dbh->do(q{
5
        CREATE TABLE oauth_access_tokens (
6
            access_token VARCHAR(255) NOT NULL,
7
            client_id VARCHAR(255) NOT NULL,
8
            expires INT NOT NULL,
9
            PRIMARY KEY (access_token)
10
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8
11
    });
12
13
    SetVersion( $DBversion );
14
    print "Upgrade to $DBversion done (Bug XXXXX - description)\n";
15
}
(-)a/misc/cronjobs/cleanup_database.pl (+11 lines)
Lines 81-86 Usage: $0 [-h|--help] [--sessions] [--sessdays DAYS] [-v|--verbose] [--zebraqueu Link Here
81
   --temp-uploads     Delete temporary uploads.
81
   --temp-uploads     Delete temporary uploads.
82
   --temp-uploads-days DAYS Override the corresponding preference value.
82
   --temp-uploads-days DAYS Override the corresponding preference value.
83
   --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
83
   --uploads-missing FLAG Delete upload records for missing files when FLAG is true, count them otherwise
84
   --oauth-tokens     Delete expired OAuth2 tokens
84
USAGE
85
USAGE
85
    exit $_[0];
86
    exit $_[0];
86
}
87
}
Lines 106-111 my $special_holidays_days; Link Here
106
my $temp_uploads;
107
my $temp_uploads;
107
my $temp_uploads_days;
108
my $temp_uploads_days;
108
my $uploads_missing;
109
my $uploads_missing;
110
my $oauth_tokens;
109
111
110
GetOptions(
112
GetOptions(
111
    'h|help'            => \$help,
113
    'h|help'            => \$help,
Lines 129-134 GetOptions( Link Here
129
    'temp-uploads'      => \$temp_uploads,
131
    'temp-uploads'      => \$temp_uploads,
130
    'temp-uploads-days:i' => \$temp_uploads_days,
132
    'temp-uploads-days:i' => \$temp_uploads_days,
131
    'uploads-missing:i' => \$uploads_missing,
133
    'uploads-missing:i' => \$uploads_missing,
134
    'oauth-tokens'      => \$oauth_tokens,
132
) || usage(1);
135
) || usage(1);
133
136
134
# Use default values
137
# Use default values
Lines 162-167 unless ( $sessions Link Here
162
    || $special_holidays_days
165
    || $special_holidays_days
163
    || $temp_uploads
166
    || $temp_uploads
164
    || defined $uploads_missing
167
    || defined $uploads_missing
168
    || $oauth_tokens
165
) {
169
) {
166
    print "You did not specify any cleanup work for the script to do.\n\n";
170
    print "You did not specify any cleanup work for the script to do.\n\n";
167
    usage(1);
171
    usage(1);
Lines 333-338 if( defined $uploads_missing ) { Link Here
333
    }
337
    }
334
}
338
}
335
339
340
if ($oauth_tokens) {
341
    require Koha::OAuthAccessTokens;
342
343
    my $count = int Koha::OAuthAccessTokens->search({ expires => { '<=', time } })->delete;
344
    say "Removed $count expired OAuth2 tokens";
345
}
346
336
exit(0);
347
exit(0);
337
348
338
sub RemoveOldSessions {
349
sub RemoveOldSessions {
(-)a/t/db_dependent/api/v1/oauth.t (-1 / +101 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/env perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it under the
6
# terms of the GNU General Public License as published by the Free Software
7
# Foundation; either version 3 of the License, or (at your option) any later
8
# version.
9
#
10
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
11
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12
# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License along
15
# with Koha; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
18
use Modern::Perl;
19
20
use Test::More tests => 1;
21
use Test::Mojo;
22
23
use Koha::Database;
24
25
use t::lib::Mocks;
26
use t::lib::TestBuilder;
27
28
my $t = Test::Mojo->new('Koha::REST::V1');
29
my $schema  = Koha::Database->new->schema;
30
my $builder = t::lib::TestBuilder->new();
31
32
subtest '/oauth/token tests' => sub {
33
    plan tests => 19;
34
35
    $schema->storage->txn_begin;
36
37
    my $patron = $builder->build({
38
        source => 'Borrower',
39
        value  => {
40
            surname => 'Test OAuth',
41
            flags => 0,
42
        },
43
    });
44
45
    # Missing parameter grant_type
46
    $t->post_ok('/api/v1/oauth/token')
47
        ->status_is(400);
48
49
    # Wrong grant type
50
    $t->post_ok('/api/v1/oauth/token', form => { grant_type => 'password' })
51
        ->status_is(400)
52
        ->json_is({error => 'Unimplemented grant type'});
53
54
    # No client_id/client_secret
55
    $t->post_ok('/api/v1/oauth/token', form => { grant_type => 'client_credentials' })
56
        ->status_is(403)
57
        ->json_is({error => 'unauthorized_client'});
58
59
    my ($client_id, $client_secret) = ('client1', 'secr3t');
60
    t::lib::Mocks::mock_config('api_client', {
61
        'client_id' => $client_id,
62
        'client_secret' => $client_secret,
63
        patron_id => $patron->{borrowernumber},
64
    });
65
66
    my $formData = {
67
        grant_type => 'client_credentials',
68
        client_id => $client_id,
69
        client_secret => $client_secret,
70
    };
71
    $t->post_ok('/api/v1/oauth/token', form => $formData)
72
        ->status_is(200)
73
        ->json_is('/expires_in' => 3600)
74
        ->json_is('/token_type' => 'Bearer')
75
        ->json_has('/access_token');
76
77
    my $access_token = $t->tx->res->json->{access_token};
78
79
    # Without access token, it returns 401
80
    $t->get_ok('/api/v1/patrons')->status_is(401);
81
82
    # With access token, but without permissions, it returns 403
83
    my $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
84
    $tx->req->headers->authorization("Bearer $access_token");
85
    $t->request_ok($tx)->status_is(403);
86
87
    # With access token and permissions, it returns 200
88
    $builder->build({
89
        source => 'UserPermission',
90
        value  => {
91
            borrowernumber => $patron->{borrowernumber},
92
            module_bit => 4, # borrowers
93
            code => 'edit_borrowers',
94
        },
95
    });
96
    $tx = $t->ua->build_tx(GET => '/api/v1/patrons');
97
    $tx->req->headers->authorization("Bearer $access_token");
98
    $t->request_ok($tx)->status_is(200);
99
100
    $schema->storage->txn_rollback;
101
};

Return to bug 20402