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   21930 use Mojo::Base -base;
  4         5  
  4         33  
3              
4             our $VERSION = 0.032;
5              
6 4     4   714 use Carp 'croak';
  4         5  
  4         174  
7 4     4   1629 use Crypt::OpenSSL::RSA ();
  4         12063  
  4         81  
8 4     4   1636 use MIME::Base64 ();
  4         1922  
  4         93  
9 4     4   1445 use Mojar::ClassShare 'have';
  4         1596  
  4         49  
10 4     4   1641 use Mojo::JSON 'encode_json', 'decode_json';
  4         326647  
  4         3472  
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 2691 my $self = shift;
33              
34 7 100       15 if (@_ == 0) {
35 5         19 my @h = map +( ($_, $self->$_) ), qw(typ alg);
36 5         54 return $self->{header} = $self->mogrify( { @h } );
37             }
38             else {
39 2         12 %$self = ( %$self, @_ );
40             }
41 2         8 return $self;
42             }
43              
44             sub body {
45 4     4 1 2090 my $self = shift;
46              
47 4 50       10 if (@_ == 0) {
48 4         6 foreach (qw(iss scope)) {
49 7 100       30 croak "Missing required field ($_)" unless defined $self->$_;
50             }
51 3 50       21 $self->{scope} = join ' ', @{$self->{scope}} if ref $self->{scope};
  0         0  
52 3         9 my @c = map +( ($_, $self->$_) ), qw(iss scope aud exp iat);
53 3         50 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 1151 my $self = shift;
63              
64 1 50       3 if (@_ == 0) {
65 1 50       3 croak 'Unrecognised algorithm (not RS256)' unless $self->alg eq 'RS256';
66 1         8 my $input = $self->header .q{.}. $self->body;
67              
68 1         17 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 1207 my ($self, $triplet) = @_;
113 1         5 my ($header, $body, $signature) = split /\./, $triplet;
114              
115 1         2 my %param = %{ $self->demogrify($header) };
  1         3  
116 1         82 %param = ( %param, %{ $self->demogrify($body) } );
  1         3  
117 1         145 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 9 my ($self, $hashref) = @_;
129 8 50 33     36 return '' unless ref $hashref && ref $hashref eq 'HASH';
130 8         18 return MIME::Base64::encode_base64url(encode_json $hashref);
131             }
132              
133             sub demogrify {
134 10     10 1 503 my ($self, $safestring) = @_;
135 10 50 33     41 return {} unless defined $safestring && length $safestring;
136 10         19 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   32 no warnings 'redefine';
  4         5  
  4         454  
142             sub _encode_object {
143 8     8   41 my $object = shift;
144 8         28 my @pairs = map { _encode_string($_) . ':' . _encode_value($object->{$_}) }
  25         214  
145             sort keys %$object;
146 8         127 return '{' . join(',', @pairs) . '}';
147             };
148              
149             1;
150             __END__