File Coverage

blib/lib/Mojolicious/Plugin/OpenAPI/Security.pm
Criterion Covered Total %
statement 53 53 100.0
branch 28 30 93.3
condition 11 16 68.7
subroutine 5 5 100.0
pod 1 1 100.0
total 98 105 93.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::OpenAPI::Security;
2 48     48   368 use Mojo::Base -base;
  48         129  
  48         429  
3              
4             my %DEF_PATH
5             = ('openapiv2' => '/securityDefinitions', 'openapiv3' => '/components/securitySchemes');
6              
7             sub register {
8 59     59 1 807 my ($self, $app, $config) = @_;
9 59         211 my $openapi = $config->{openapi};
10 59 100       571 my $handlers = $config->{security} or return;
11              
12 3 50       11 return unless $openapi->validator->get($DEF_PATH{$openapi->validator->moniker});
13 3         406 return $openapi->route(
14             $openapi->route->under('/')->to(cb => $self->_build_action($openapi, $handlers)));
15             }
16              
17             sub _build_action {
18 3     3   632 my ($self, $openapi, $handlers) = @_;
19 3   50     21 my $global = $openapi->validator->get('/security') || [];
20 3         293 my $definitions = $openapi->validator->get($DEF_PATH{$openapi->validator->moniker});
21              
22             return sub {
23 28     28   350194 my $c = shift;
24 28 100 100     93 return 1 if $c->req->method eq 'OPTIONS' and $c->match->stack->[-1]{'openapi.default_options'};
25              
26 26   50     495 my $spec = $c->openapi->spec || {};
27 26 100       4139 my @security_or = @{$spec->{security} || $global};
  26         145  
28 26         71 my ($sync_mode, $n_checks, %res) = (1, 0);
29              
30             my $security_completed = sub {
31 24         60 my ($i, $status, @errors) = (0, 401);
32              
33             SECURITY_AND:
34 24         54 for my $security_and (@security_or) {
35 32         64 my @e;
36              
37 32         121 for my $name (sort keys %$security_and) {
38 44         106 my $error_path = sprintf '/security/%s/%s', $i, _pointer_escape($name);
39             push @e, ref $res{$name} ? $res{$name} : {message => $res{$name}, path => $error_path}
40 44 100       250 if defined $res{$name};
    100          
41             }
42              
43             # Authenticated
44             # Cannot call $c->continue() in case this callback was called
45             # synchronously, since it will result in an infinite loop.
46 32 100       95 unless (@e) {
47 13 100 66     31 return if eval { $sync_mode || $c->continue || 1 };
  13 100       100  
48 2         46 chomp $@;
49 2         9 $c->app->log->error($@);
50 2         44 @errors = ({message => 'Internal Server Error.', path => '/'});
51 2         5 $status = 500;
52 2         8 last SECURITY_AND;
53             }
54              
55             # Not authenticated
56 19         43 push @errors, @e;
57 19         40 $i++;
58             }
59 13 100 66     57 $status = $c->stash('status') || $status if $status < 500;
60 13         195 $c->render(openapi => {errors => \@errors}, status => $status);
61 13         10116 $n_checks = -1; # Make sure we don't render twice
62 26         128 };
63              
64 26         69 for my $security_and (@security_or) {
65 34         598 for my $name (sort keys %$security_and) {
66 46         570 my $security_cb = $handlers->{$name};
67              
68 46 100       163 if (!$security_cb) {
    100          
69 4 50       25 $res{$name} = {message => "No security callback for $name."} unless exists $res{$name};
70             }
71             elsif (!exists $res{$name}) {
72 40         98 $res{$name} = undef;
73 40         66 $n_checks++;
74              
75             # $security_cb is obviously called synchronously, but the callback
76             # might also be called synchronously. We need the $sync_mode guard
77             # to make sure that we do not call continue() if that is the case.
78             $c->$security_cb(
79             $definitions->{$name},
80             $security_and->{$name},
81             sub {
82 38   66     3170 $res{$name} //= $_[1];
83 38 100       181 $security_completed->() if --$n_checks == 0;
84             }
85 40         239 );
86             }
87             }
88             }
89              
90             # If $security_completed was called already, then $n_checks will zero and
91             # we return "1" which means we are in synchronous mode. When running async,
92             # we need to asign undef() to $sync_mode, since it is used inside
93             # $security_completed to call $c->continue()
94 24 100       203 return $sync_mode = $n_checks ? undef : 1;
95 3         334 };
96             }
97              
98 44     44   95 sub _pointer_escape { local $_ = shift; s/~/~0/g; s!/!~1!g; $_; }
  44         100  
  44         82  
  44         236  
99              
100             1;
101              
102             =encoding utf8
103              
104             =head1 NAME
105              
106             Mojolicious::Plugin::OpenAPI::Security - OpenAPI plugin for securing your API
107              
108             =head1 DESCRIPTION
109              
110             This plugin will allow you to use the security features provided by the OpenAPI
111             specification.
112              
113             Note that this is currently EXPERIMENTAL! Please let me know if you have any
114             feedback. See L for a
115             complete discussion.
116              
117             =head1 TUTORIAL
118              
119             =head2 Specification
120              
121             Here is an example specification that use
122             L
123             and L from
124             the OpenAPI spec:
125              
126             {
127             "swagger": "2.0",
128             "info": { "version": "0.8", "title": "Super secure" },
129             "schemes": [ "https" ],
130             "basePath": "/api",
131             "securityDefinitions": {
132             "dummy": {
133             "type": "apiKey",
134             "name": "Authorization",
135             "in": "header",
136             "description": "dummy"
137             }
138             },
139             "paths": {
140             "/protected": {
141             "post": {
142             "x-mojo-to": "super#secret_resource",
143             "security": [{"dummy": []}],
144             "parameters": [
145             { "in": "body", "name": "body", "schema": { "type": "object" } }
146             ],
147             "responses": {
148             "200": {"description": "Echo response", "schema": { "type": "object" }},
149             "401": {"description": "Sorry mate", "schema": { "type": "array" }}
150             }
151             }
152             }
153             }
154             }
155              
156             =head2 Application
157              
158             The specification above can be dispatched to handlers inside your
159             L application. The do so, add the "security" key when loading the
160             plugin, and reference the "securityDefinitions" name inside that to a callback.
161             In this example, we have the "dummy" security handler:
162              
163             package Myapp;
164             use Mojo::Base "Mojolicious";
165              
166             sub startup {
167             my $app = shift;
168              
169             $app->plugin(OpenAPI => {
170             url => "data:///security.json",
171             security => {
172             dummy => sub {
173             my ($c, $definition, $scopes, $cb) = @_;
174             return $c->$cb() if $c->req->headers->authorization;
175             return $c->$cb('Authorization header not present');
176             }
177             }
178             });
179             }
180              
181             1;
182              
183             C<$c> is a L object. C<$definition> is the security
184             definition from C. C<$scopes> is the Oauth scopes, which
185             in this case is just an empty array ref, but it will contain the value for
186             "security" under the given HTTP method.
187              
188             Call C<$cb> with C or no argument at all to indicate pass. Call C<$cb>
189             with a defined value (usually a string) to indicate that the check has failed.
190             When none of the sets of security restrictions are satisfied, the standard
191             OpenAPI structure is built using the values passed to the callbacks as the
192             messages and rendered to the client with a status of 401.
193              
194             Note that the callback must be called or the dispatch will hang.
195              
196             See also L for example
197             L application.
198              
199             =head2 Controller
200              
201             Your controllers and actions are unchanged. The difference in behavior is that
202             the action simply won't be called if you fail to pass the security tests.
203              
204             =head2 Exempted routes
205              
206             All of the routes created by the plugin are protected by the security
207             definitions with the following exemptions. The base route that renders the
208             spec/documentation is exempted. Additionally, when a route does not define its
209             own C handler a documentation endpoint is generated which is exempt as
210             well.
211              
212             =head1 METHODS
213              
214             =head2 register
215              
216             Called by L.
217              
218             =head1 SEE ALSO
219              
220             L.
221              
222             =cut