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

(-)a/Koha/OAuth.pm (+87 lines)
Line 0 Link Here
1
package Koha::OAuth;
2
3
use Modern::Perl;
4
use Net::OAuth2::AuthorizationServer::ClientCredentialsGrant;
5
use Koha::OAuthAccessTokens;
6
use Koha::OAuthAccessToken;
7
8
sub grant {
9
    my $grant = Net::OAuth2::AuthorizationServer::ClientCredentialsGrant->new(
10
        verify_client_cb => \&_verify_client_cb,
11
        store_access_token_cb => \&_store_access_token_cb,
12
        verify_access_token_cb => \&_verify_access_token_cb
13
    );
14
15
    return $grant;
16
}
17
18
sub _verify_client_cb {
19
    my (%args) = @_;
20
21
    my ($client_id, $scopes_ref, $client_secret)
22
        = @args{ qw/ client_id scopes client_secret / };
23
24
    my $clients = C4::Context->config('api_client');
25
    $clients = [ $clients ] unless ref $clients eq 'ARRAY';
26
    my ($client) = grep { $_->{client_id} eq $client_id } @$clients;
27
    return (0, 'unauthorized_client') unless $client;
28
29
    return (0, 'access_denied') unless $client_secret eq $client->{client_secret};
30
31
    $client->{scope} //= [];
32
    $client->{scope} = [ $client->{scope} ] if ref $client->{scope} ne 'ARRAY';
33
    my $client_scopes = [];
34
    foreach my $scope ( @{ $scopes_ref // [] } ) {
35
        if (!grep { $_ eq $scope } @{ $client->{scope} }) {
36
            return (0, 'invalid_scope');
37
        }
38
        push @$client_scopes, $scope;
39
    }
40
41
    return (1, undef, $client_scopes);
42
}
43
44
sub _store_access_token_cb {
45
    my ( %args ) = @_;
46
47
    my ( $client_id, $access_token, $expires_in, $scopes_ref )
48
        = @args{ qw/ client_id access_token expires_in scopes / };
49
50
    my $at = Koha::OAuthAccessToken->new({
51
        access_token  => $access_token,
52
        scope         => join (' ', @$scopes_ref),
53
        expires       => time + $expires_in,
54
        client_id     => $client_id,
55
    });
56
    $at->store;
57
58
    return;
59
}
60
61
sub _verify_access_token_cb {
62
    my (%args) = @_;
63
64
    my ($access_token, $scopes_ref) = @args{qw(access_token scopes)};
65
66
    my $at = Koha::OAuthAccessTokens->find($access_token);
67
    if ($at) {
68
        if ( $at->expires <= time ) {
69
            # need to revoke the access token
70
            $at->delete;
71
72
            return (0, 'invalid_grant')
73
        } elsif ( $scopes_ref ) {
74
            foreach my $scope ( @{ $scopes_ref // [] } ) {
75
                unless ($at->has_scope($scope)) {
76
                    return (0, 'invalid_grant');
77
                }
78
            }
79
        }
80
81
        return $at->unblessed;
82
    }
83
84
    return (0, 'invalid_grant')
85
};
86
87
1;
(-)a/Koha/OAuthAccessToken.pm (+19 lines)
Line 0 Link Here
1
package Koha::OAuthAccessToken;
2
3
use Modern::Perl;
4
5
use base qw(Koha::Object);
6
7
sub has_scope {
8
    my ($self, $scope) = @_;
9
10
    my @scopes = split / /, $self->scope;
11
12
    return scalar grep { $_ eq $scope } @scopes;
13
}
14
15
sub _type {
16
    return 'OauthAccessToken';
17
}
18
19
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/Auth.pm (+21 lines)
Lines 26-31 use C4::Auth qw( check_cookie_auth get_session haspermission ); Link Here
26
use Koha::Account::Lines;
26
use Koha::Account::Lines;
27
use Koha::Checkouts;
27
use Koha::Checkouts;
28
use Koha::Holds;
28
use Koha::Holds;
29
use Koha::OAuth;
29
use Koha::Old::Checkouts;
30
use Koha::Old::Checkouts;
30
use Koha::Patrons;
31
use Koha::Patrons;
31
32
Lines 109-114 sub authenticate_api_request { Link Here
109
    my ( $c ) = @_;
110
    my ( $c ) = @_;
110
111
111
    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
112
    my $spec = $c->match->endpoint->pattern->defaults->{'openapi.op_spec'};
113
114
    my $authorization_header = $c->req->headers->authorization;
115
    if ($authorization_header) {
116
        my $grant = Koha::OAuth->grant;
117
        my ($type, $token) = split / /, $authorization_header;
118
        my ($is_valid, $error) = $grant->verify_access_token(
119
            access_token => $token,
120
            scopes => $spec->{'x-koha-scopes'} // [],
121
        );
122
123
        if (!$is_valid) {
124
            Koha::Exceptions::Authorization::Unauthorized->throw(
125
                error => $error,
126
                required_permissions => $spec->{'x-koha-scopes'},
127
            );
128
        }
129
130
        return 1;
131
    }
132
112
    my $authorization = $spec->{'x-koha-authorization'};
133
    my $authorization = $spec->{'x-koha-authorization'};
113
    my $cookie = $c->cookie('CGISESSID');
134
    my $cookie = $c->cookie('CGISESSID');
114
    my ($session, $user);
135
    my ($session, $user);
(-)a/Koha/REST/V1/OAuth.pm (+57 lines)
Line 0 Link Here
1
package Koha::REST::V1::OAuth;
2
3
use Modern::Perl;
4
5
use Mojo::Base 'Mojolicious::Controller';
6
use Koha::OAuth;
7
8
use C4::Context;
9
10
sub token {
11
    my $c = shift->openapi->valid_input or return;
12
13
    my $grant = Koha::OAuth->grant;
14
    my $client_id = $c->validation->param('client_id');
15
    my $client_secret = $c->validation->param('client_secret');
16
    my $scope = [ split / /, $c->validation->param('scope') ];
17
18
    # verify a client against known clients
19
    my ( $is_valid, $error, $scopes ) = $grant->verify_client(
20
      client_id     => $client_id,
21
      client_secret => $client_secret,
22
      scopes        => $scope,
23
    );
24
25
    unless ($is_valid) {
26
        return $c->render(status => 403, openapi => {error => $error});
27
    }
28
29
    # generate a token
30
    my $token = $grant->token(
31
      client_id       => $client_id,
32
      scopes          => $scopes,
33
    );
34
35
    # store access token
36
    my $expires_in = 3600;
37
    $grant->store_access_token(
38
        client_id         => $client_id,
39
        access_token      => $token,
40
        expires_in        => $expires_in,
41
        scopes => $scopes,
42
    );
43
44
    my $at = Koha::OAuthAccessTokens->search({
45
        access_token => $token,
46
    })->next;
47
48
    my $response = {
49
        access_token => $token,
50
        token_type => 'Bearer',
51
        expires_in => $expires_in,
52
    };
53
54
    return $c->render(status => 200, openapi => $response);
55
}
56
57
1;
(-)a/Koha/Schema/Result/OauthAccessToken.pm (+79 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 scope
39
40
  data_type: 'text'
41
  is_nullable: 1
42
43
=head2 expires
44
45
  data_type: 'integer'
46
  is_nullable: 0
47
48
=cut
49
50
__PACKAGE__->add_columns(
51
  "access_token",
52
  { data_type => "varchar", is_nullable => 0, size => 255 },
53
  "client_id",
54
  { data_type => "varchar", is_nullable => 0, size => 255 },
55
  "scope",
56
  { data_type => "text", is_nullable => 1 },
57
  "expires",
58
  { data_type => "integer", is_nullable => 0 },
59
);
60
61
=head1 PRIMARY KEY
62
63
=over 4
64
65
=item * L</access_token>
66
67
=back
68
69
=cut
70
71
__PACKAGE__->set_primary_key("access_token");
72
73
74
# Created by DBIx::Class::Schema::Loader v0.07046 @ 2018-03-14 12:13:59
75
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:VgSA5BIbeUR31WV1YwbfEQ
76
77
78
# You can replace this text with custom code or comments, and it will be preserved on regeneration
79
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 (+58 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
                    "name": "scope",
32
                    "in": "formData",
33
                    "description": "space-separated list of scopes",
34
                    "type": "string"
35
                }
36
            ],
37
            "responses": {
38
                "200": {
39
                    "description": "OK",
40
                    "schema": {
41
                        "type": "object",
42
                        "properties": {
43
                            "access_token": {
44
                                "type": "string"
45
                            }
46
                        }
47
                    }
48
                },
49
                "403": {
50
                    "description": "Access forbidden",
51
                    "schema": {
52
                        "$ref": "../definitions.json#/error"
53
                    }
54
                }
55
            }
56
        }
57
    }
58
}
(-)a/api/v1/swagger/paths/patrons.json (-2 / +8 lines)
Lines 46-52 Link Here
46
        "permissions": {
46
        "permissions": {
47
          "borrowers": "edit_borrowers"
47
          "borrowers": "edit_borrowers"
48
        }
48
        }
49
      }
49
      },
50
      "x-koha-scopes": [
51
        "patrons.read"
52
      ]
50
    }
53
    }
51
  },
54
  },
52
  "/patrons/{borrowernumber}": {
55
  "/patrons/{borrowernumber}": {
Lines 105-111 Link Here
105
        "permissions": {
108
        "permissions": {
106
          "borrowers": "edit_borrowers"
109
          "borrowers": "edit_borrowers"
107
        }
110
        }
108
      }
111
      },
112
      "x-koha-scopes": [
113
        "patrons.read"
114
      ]
109
    }
115
    }
110
  }
116
  }
111
}
117
}
(-)a/installer/data/mysql/atomicupdate/oauth_tokens.perl (-1 / +16 lines)
Line 0 Link Here
0
- 
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
            scope TEXT,
9
            expires INT NOT NULL,
10
            PRIMARY KEY (access_token)
11
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8
12
    });
13
14
    SetVersion( $DBversion );
15
    print "Upgrade to $DBversion done (Bug XXXXX - description)\n";
16
}

Return to bug 20402