| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
#!/usr/bin/env perl |
|
2
|
|
|
|
|
|
|
package Plack::Middleware::Validate_Google_IAP_JWT; |
|
3
|
2
|
|
|
2
|
|
704324
|
use strict; |
|
|
2
|
|
|
|
|
5
|
|
|
|
2
|
|
|
|
|
88
|
|
|
4
|
2
|
|
|
2
|
|
13
|
use warnings; |
|
|
2
|
|
|
|
|
4
|
|
|
|
2
|
|
|
|
|
311
|
|
|
5
|
|
|
|
|
|
|
|
|
6
|
|
|
|
|
|
|
our $VERSION = "0.04"; |
|
7
|
|
|
|
|
|
|
|
|
8
|
2
|
|
|
|
|
43
|
use MOP4Import::Base::CLI_JSON -as_base |
|
9
|
|
|
|
|
|
|
, [fields => |
|
10
|
|
|
|
|
|
|
, [key_url => default => "https://www.gstatic.com/iap/verify/public_key-jwk"] |
|
11
|
|
|
|
|
|
|
, [want_iss => default => "https://cloud.google.com/iap"], |
|
12
|
|
|
|
|
|
|
, [want_hd => doc => "expected hosted domain"], |
|
13
|
|
|
|
|
|
|
, [guest_subpath => doc => "Allow guest access(skip JWT check) for this subpath"] |
|
14
|
|
|
|
|
|
|
, qw( |
|
15
|
|
|
|
|
|
|
app |
|
16
|
|
|
|
|
|
|
_iap_public_key |
|
17
|
|
|
|
|
|
|
_expires_at |
|
18
|
|
|
|
|
|
|
) |
|
19
|
|
|
|
|
|
|
] |
|
20
|
2
|
|
|
2
|
|
1566
|
; |
|
|
2
|
|
|
|
|
208123
|
|
|
21
|
|
|
|
|
|
|
|
|
22
|
2
|
|
|
2
|
|
9864
|
use parent qw(Plack::Middleware); |
|
|
2
|
|
|
|
|
9
|
|
|
|
2
|
|
|
|
|
19
|
|
|
23
|
|
|
|
|
|
|
|
|
24
|
2
|
|
|
2
|
|
12366
|
use File::Basename; |
|
|
2
|
|
|
|
|
3
|
|
|
|
2
|
|
|
|
|
163
|
|
|
25
|
2
|
|
|
2
|
|
1432
|
use Time::Piece; |
|
|
2
|
|
|
|
|
27696
|
|
|
|
2
|
|
|
|
|
15
|
|
|
26
|
|
|
|
|
|
|
|
|
27
|
2
|
|
|
2
|
|
975
|
use URI; |
|
|
2
|
|
|
|
|
6403
|
|
|
|
2
|
|
|
|
|
89
|
|
|
28
|
2
|
|
|
2
|
|
1608
|
use HTTP::Tiny; |
|
|
2
|
|
|
|
|
98992
|
|
|
|
2
|
|
|
|
|
127
|
|
|
29
|
|
|
|
|
|
|
|
|
30
|
2
|
|
|
2
|
|
1890
|
use Crypt::JWT (); |
|
|
2
|
|
|
|
|
131488
|
|
|
|
2
|
|
|
|
|
99
|
|
|
31
|
|
|
|
|
|
|
|
|
32
|
2
|
|
|
|
|
38
|
use MOP4Import::PSGIEnv qw( |
|
33
|
|
|
|
|
|
|
HTTP_X_GOOG_IAP_JWT_ASSERTION |
|
34
|
|
|
|
|
|
|
psgix.goog_iap_jwt |
|
35
|
|
|
|
|
|
|
psgix.goog_iap_jwt_aud |
|
36
|
|
|
|
|
|
|
psgix.goog_iap_jwt_email |
|
37
|
|
|
|
|
|
|
psgix.goog_iap_jwt_sub |
|
38
|
|
|
|
|
|
|
psgix.goog_iap_jwt_account |
|
39
|
2
|
|
|
2
|
|
1007
|
); |
|
|
2
|
|
|
|
|
24582
|
|
|
40
|
|
|
|
|
|
|
|
|
41
|
|
|
|
|
|
|
use MOP4Import::Types |
|
42
|
2
|
|
|
|
|
29
|
JWT => [[fields => qw( |
|
43
|
|
|
|
|
|
|
aud email sub |
|
44
|
|
|
|
|
|
|
)]], |
|
45
|
|
|
|
|
|
|
Response => [[fields => qw( |
|
46
|
|
|
|
|
|
|
success |
|
47
|
|
|
|
|
|
|
url |
|
48
|
|
|
|
|
|
|
status |
|
49
|
|
|
|
|
|
|
reason |
|
50
|
|
|
|
|
|
|
content |
|
51
|
|
|
|
|
|
|
headers |
|
52
|
|
|
|
|
|
|
protocol |
|
53
|
|
|
|
|
|
|
redirects |
|
54
|
|
|
|
|
|
|
)]], |
|
55
|
|
|
|
|
|
|
ResHeaders => [[fields => qw( |
|
56
|
|
|
|
|
|
|
accept-ranges |
|
57
|
|
|
|
|
|
|
cache-control |
|
58
|
|
|
|
|
|
|
content-length |
|
59
|
|
|
|
|
|
|
content-security-policy |
|
60
|
|
|
|
|
|
|
content-type |
|
61
|
|
|
|
|
|
|
cross-origin-opener-policy |
|
62
|
|
|
|
|
|
|
cross-origin-resource-policy |
|
63
|
|
|
|
|
|
|
date |
|
64
|
|
|
|
|
|
|
expires |
|
65
|
|
|
|
|
|
|
last-modified |
|
66
|
|
|
|
|
|
|
report-to |
|
67
|
|
|
|
|
|
|
server |
|
68
|
|
|
|
|
|
|
vary |
|
69
|
|
|
|
|
|
|
x-content-type-options |
|
70
|
|
|
|
|
|
|
x-xss-protection |
|
71
|
|
|
|
|
|
|
)]] |
|
72
|
2
|
|
|
2
|
|
17246
|
; |
|
|
2
|
|
|
|
|
5
|
|
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
sub call { |
|
75
|
3
|
|
|
3
|
1
|
37262
|
(my MY $self, my Env $env) = @_; |
|
76
|
|
|
|
|
|
|
|
|
77
|
3
|
|
|
|
|
20
|
my $app = $self->app; |
|
78
|
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
# Allow fake app |
|
80
|
3
|
50
|
|
|
|
33
|
if (ref $app eq 'ARRAY') { |
|
81
|
0
|
|
|
|
|
0
|
my $value = $app; |
|
82
|
|
|
|
|
|
|
$app = sub { |
|
83
|
0
|
|
|
0
|
|
0
|
return $value; |
|
84
|
0
|
|
|
|
|
0
|
}; |
|
85
|
|
|
|
|
|
|
} |
|
86
|
|
|
|
|
|
|
|
|
87
|
3
|
100
|
66
|
|
|
27
|
if ($self->{guest_subpath} |
|
88
|
|
|
|
|
|
|
and substr($env->{PATH_INFO}, 0, length($self->{guest_subpath})) |
|
89
|
|
|
|
|
|
|
eq $self->{guest_subpath}) { |
|
90
|
1
|
|
|
|
|
6
|
return $app->($env); |
|
91
|
|
|
|
|
|
|
} |
|
92
|
|
|
|
|
|
|
|
|
93
|
2
|
100
|
|
|
|
12
|
unless ($env->{HTTP_X_GOOG_IAP_JWT_ASSERTION}) { |
|
94
|
1
|
|
|
|
|
12
|
return [403, [], ["Forbidden (no JWT assertion)\n"]]; |
|
95
|
|
|
|
|
|
|
} |
|
96
|
|
|
|
|
|
|
|
|
97
|
1
|
|
|
|
|
6
|
(my JWT $jwt, my $err) = $self->decode_jwt_env_or_error($env); |
|
98
|
1
|
50
|
|
|
|
3
|
if ($err) { |
|
99
|
1
|
|
|
|
|
5
|
my ($code, $diag) = $self->parse_jwt_error($err); |
|
100
|
1
|
|
|
|
|
15
|
return [$code, [], [$diag]]; |
|
101
|
|
|
|
|
|
|
} |
|
102
|
|
|
|
|
|
|
|
|
103
|
0
|
|
|
|
|
0
|
$env->{'psgix.goog_iap_jwt'} = $jwt; |
|
104
|
0
|
|
|
|
|
0
|
$env->{'psgix.goog_iap_jwt_aud'} = $jwt->{aud}; |
|
105
|
0
|
|
|
|
|
0
|
$env->{'psgix.goog_iap_jwt_email'} = $jwt->{email}; |
|
106
|
0
|
|
|
|
|
0
|
$env->{'psgix.goog_iap_jwt_sub'} = $jwt->{sub}; |
|
107
|
0
|
0
|
|
|
|
0
|
if ($self->{want_hd}) { |
|
108
|
0
|
|
|
|
|
0
|
(my $account = $jwt->{email}) =~ s,@\Q$self->{want_hd}\E\z,,; |
|
109
|
0
|
|
|
|
|
0
|
$env->{'psgix.goog_iap_jwt_account'} = $account; |
|
110
|
|
|
|
|
|
|
} |
|
111
|
|
|
|
|
|
|
|
|
112
|
0
|
|
|
|
|
0
|
$app->($env) |
|
113
|
|
|
|
|
|
|
} |
|
114
|
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
sub decode_jwt_env_or_error { |
|
116
|
1
|
|
|
1
|
0
|
3
|
(my MY $self, my Env $env) = @_; |
|
117
|
1
|
|
|
|
|
23
|
local $@; |
|
118
|
1
|
|
|
|
|
3
|
my $res = eval {$self->decode_jwt_env($env)}; |
|
|
1
|
|
|
|
|
5
|
|
|
119
|
1
|
50
|
|
|
|
415
|
if ($@) { |
|
120
|
1
|
|
|
|
|
5
|
(undef, $@) |
|
121
|
|
|
|
|
|
|
} else { |
|
122
|
0
|
|
|
|
|
0
|
$res; |
|
123
|
|
|
|
|
|
|
} |
|
124
|
|
|
|
|
|
|
} |
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
sub decode_jwt_env { |
|
127
|
1
|
|
|
1
|
0
|
4
|
(my MY $self, my Env $env) = @_; |
|
128
|
|
|
|
|
|
|
Crypt::JWT::decode_jwt( |
|
129
|
|
|
|
|
|
|
token => $env->{HTTP_X_GOOG_IAP_JWT_ASSERTION}, |
|
130
|
|
|
|
|
|
|
kid_keys => $self->iap_public_key, |
|
131
|
|
|
|
|
|
|
verify_exp => 1, verify_iat => 1, |
|
132
|
|
|
|
|
|
|
verify_iss => $self->{want_iss}, |
|
133
|
1
|
50
|
|
|
|
6
|
($self->{want_hd} ? (verify_hd => $self->{want_hd}) : ()), |
|
134
|
|
|
|
|
|
|
) |
|
135
|
|
|
|
|
|
|
} |
|
136
|
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
sub parse_jwt_error { |
|
138
|
1
|
|
|
1
|
0
|
3
|
(my MY $self, my $errmsg) = @_; |
|
139
|
1
|
50
|
|
|
|
4
|
if ($errmsg =~ /^(JWT: \S+ claim check failed.*?) at/) { |
|
140
|
0
|
|
|
|
|
0
|
(403, $1); |
|
141
|
|
|
|
|
|
|
} else { |
|
142
|
1
|
|
|
|
|
3
|
(400, $errmsg); |
|
143
|
|
|
|
|
|
|
} |
|
144
|
|
|
|
|
|
|
} |
|
145
|
|
|
|
|
|
|
|
|
146
|
|
|
|
|
|
|
sub iap_public_key { |
|
147
|
1
|
|
|
1
|
0
|
3
|
(my MY $self) = @_; |
|
148
|
1
|
50
|
33
|
|
|
7
|
if ($self->{_iap_public_key} and (time + 10) < $self->{_expires_at}) { |
|
149
|
|
|
|
|
|
|
return $self->{_iap_public_key} |
|
150
|
0
|
|
|
|
|
0
|
} |
|
151
|
1
|
|
|
|
|
6
|
my ($ok, $err) = $self->fetch_iap_public_key_with_expires; |
|
152
|
1
|
50
|
|
|
|
5
|
if ($err) { |
|
153
|
0
|
|
|
|
|
0
|
Carp::croak "Can't fetch iap public_key: $err"; |
|
154
|
|
|
|
|
|
|
} |
|
155
|
|
|
|
|
|
|
|
|
156
|
1
|
|
|
|
|
4
|
($self->{_iap_public_key}, $self->{_expires_at}) = @$ok; |
|
157
|
|
|
|
|
|
|
|
|
158
|
1
|
|
|
|
|
11
|
return $self->{_iap_public_key}; |
|
159
|
|
|
|
|
|
|
} |
|
160
|
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
sub fetch_iap_public_key { |
|
162
|
0
|
|
|
0
|
1
|
0
|
(my MY $self) = @_; |
|
163
|
0
|
|
|
|
|
0
|
my ($ok, $err) = $self->fetch_iap_public_key_with_expires; |
|
164
|
0
|
0
|
|
|
|
0
|
if ($err) { |
|
165
|
0
|
|
|
|
|
0
|
return (undef, $err) |
|
166
|
|
|
|
|
|
|
} else { |
|
167
|
0
|
|
|
|
|
0
|
$ok->[0] |
|
168
|
|
|
|
|
|
|
} |
|
169
|
|
|
|
|
|
|
} |
|
170
|
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
sub fetch_iap_public_key_with_expires { |
|
172
|
1
|
|
|
1
|
0
|
3
|
(my MY $self) = @_; |
|
173
|
1
|
|
|
|
|
12
|
my Response $response = HTTP::Tiny->new->request(GET => $self->{key_url}); |
|
174
|
1
|
50
|
|
|
|
338376
|
if ($response->{success}) { |
|
175
|
1
|
|
|
|
|
1405
|
my $jwt = $self->cli_decode_json($response->{content}); |
|
176
|
1
|
|
|
|
|
506
|
my ResHeaders $headers = $response->{headers}; |
|
177
|
1
|
50
|
|
|
|
6
|
my $expires = $headers->{expires} ? $self->parse_http_date($headers->{expires}) : undef; |
|
178
|
1
|
|
|
|
|
212
|
[$jwt, $expires]; |
|
179
|
|
|
|
|
|
|
} else { |
|
180
|
|
|
|
|
|
|
(undef, $response->{reason}) |
|
181
|
0
|
|
|
|
|
0
|
} |
|
182
|
|
|
|
|
|
|
} |
|
183
|
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
sub parse_http_date { |
|
185
|
1
|
|
|
1
|
0
|
13
|
(my MY $self, my $date) = @_; |
|
186
|
1
|
|
|
|
|
11
|
Time::Piece->strptime($date, "%a, %d %b %Y %H:%M:%S %Z")->epoch |
|
187
|
|
|
|
|
|
|
} |
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
MY->run(\@ARGV) unless caller; |
|
190
|
|
|
|
|
|
|
1; |
|
191
|
|
|
|
|
|
|
__END__ |