File Coverage

blib/lib/Mojar/Auth/Jwt.pm
Criterion Covered Total %
statement 56 73 76.7
branch 10 20 50.0
condition 2 6 33.3
subroutine 14 17 82.3
pod 9 9 100.0
total 91 125 72.8


line stmt bran cond sub pod time code
1             package Mojar::Auth::Jwt;
2 4     4   23916 use Mojo::Base -base;
  4         11  
  4         43  
3              
4             our $VERSION = 0.032;
5              
6 4     4   903 use Carp 'croak';
  4         11  
  4         259  
7 4     4   2031 use Crypt::OpenSSL::RSA ();
  4         14305  
  4         121  
8 4     4   2003 use MIME::Base64 ();
  4         2477  
  4         123  
9 4     4   1964 use Mojar::ClassShare 'have';
  4         2228  
  4         43  
10 4     4   2072 use Mojo::JSON 'encode_json', 'decode_json';
  4         400475  
  4         4610  
11              
12             # Attributes
13              
14             # JWT Header
15             has typ => 'JWT';
16             has alg => 'RS256';
17              
18             # JWT Claim Set
19             has 'iss';
20             has scope => sub { q{https://www.googleapis.com/auth/analytics.readonly} };
21             has aud => q{https://accounts.google.com/o/oauth2/token};
22             has iat => sub { time };
23             has duration => 60*60; # 1 hour
24             has exp => sub { time + $_[0]->duration };
25              
26             # JWT Signature
27             has 'private_key';
28              
29             # Mogrified chunks
30              
31             sub header {
32 7     7 1 6389 my $self = shift;
33              
34 7 100       23 if (@_ == 0) {
35 5         25 my @h = map +( ($_, $self->$_) ), qw(typ alg);
36 5         71 return $self->{header} = $self->mogrify( { @h } );
37             }
38             else {
39 2         14 %$self = ( %$self, @_ );
40             }
41 2         13 return $self;
42             }
43              
44             sub body {
45 4     4 1 5471 my $self = shift;
46              
47 4 50       14 if (@_ == 0) {
48 4         9 foreach (qw(iss scope)) {
49 7 100       33 croak "Missing required field ($_)" unless defined $self->$_;
50             }
51 3 50       24 $self->{scope} = join ' ', @{$self->{scope}} if ref $self->{scope};
  0         0  
52 3         12 my @c = map +( ($_, $self->$_) ), qw(iss scope aud exp iat);
53 3         61 return $self->{body} = $self->mogrify( { @c } );
54             }
55             else {
56 0         0 %$self = ( %$self, @_ );
57             }
58 0         0 return $self;
59             }
60              
61             sub signature {
62 1     1 1 2837 my $self = shift;
63              
64 1 50       5 if (@_ == 0) {
65 1 50       4 croak 'Unrecognised algorithm (not RS256)' unless $self->alg eq 'RS256';
66 1         10 my $input = $self->header .q{.}. $self->body;
67              
68 1         29 return $self->{signature} = MIME::Base64::encode_base64url(
69             $self->cipher->sign($input)
70             );
71             }
72             else {
73 0         0 %$self = ( %$self, @_ );
74             }
75 0         0 return $self;
76             }
77              
78             has cipher => sub {
79             my $self = shift;
80             foreach ('private_key') {
81             croak qq{Missing required field ($_)} unless defined $self->$_;
82             }
83              
84             my $cipher = Crypt::OpenSSL::RSA->new_private_key($self->private_key);
85             $cipher->use_pkcs1_padding;
86             $cipher->use_sha256_hash; # Requires openssl v0.9.8+
87             return $cipher;
88             };
89              
90             # Public methods
91              
92             sub reset {
93 0     0 1 0 my ($self) = @_;
94 0         0 delete @$self{qw(iat exp body signature)};
95 0         0 return;
96             }
97              
98             sub encode {
99 0     0 1 0 my $self = shift;
100 0 0       0 if (ref $self) {
101             # Encoding an existing object
102 0 0       0 %$self = (%$self, @_) if @_;
103             }
104             else {
105             # Class method => create object
106 0         0 $self = $self->new(@_);
107             }
108 0         0 return join q{.}, $self->header, $self->body, $self->signature;
109             }
110              
111             sub decode {
112 1     1 1 3078 my ($self, $triplet) = @_;
113 1         7 my ($header, $body, $signature) = split /\./, $triplet;
114              
115 1         3 my %param = %{ $self->demogrify($header) };
  1         3  
116 1         138 %param = ( %param, %{ $self->demogrify($body) } );
  1         3  
117 1         239 return $self->new(%param);
118             }
119              
120             sub verify_signature {
121 0     0 1 0 my $self = shift;
122 0         0 my $plaintext = $self->header .q{.}. $self->body;
123 0         0 my $plainsign = MIME::Base64::decode_base64url( $self->signature );
124 0         0 return $self->cipher->verify($plaintext, $plainsign);
125             }
126              
127             sub mogrify {
128 8     8 1 18 my ($self, $hashref) = @_;
129 8 50 33     50 return '' unless ref $hashref && ref $hashref eq 'HASH';
130 8         26 return MIME::Base64::encode_base64url(encode_json $hashref);
131             }
132              
133             sub demogrify {
134 10     10 1 839 my ($self, $safestring) = @_;
135 10 50 33     54 return {} unless defined $safestring && length $safestring;
136 10         28 return decode_json(MIME::Base64::decode_base64url($safestring));
137             }
138              
139             package Mojo::JSON;
140             # Need json keys to be sorted => s/keys/sort keys/
141 4     4   49 no warnings 'redefine';
  4         10  
  4         525  
142             sub _encode_object {
143 8     8   69 my $object = shift;
144 8         37 my @pairs = map { _encode_string($_) . ':' . _encode_value($object->{$_}) }
  25         390  
145             sort keys %$object;
146 8         212 return '{' . join(',', @pairs) . '}';
147             };
148              
149             1;
150             __END__