From 563ad15549f25ccd89dc9b8d2644d2f79743ec53 Mon Sep 17 00:00:00 2001 From: cono Date: Fri, 25 Jul 2014 15:00:59 +0000 Subject: [PATCH 1/3] Add new OOP interface to work with JWKs --- Build.PL | 10 + lib/JSON/WebToken.pm | 368 ++++++++++++++++++++++++++++----- lib/JSON/WebToken/Algorithm.pm | 69 +++++++ lib/JSON/WebToken/Constants.pm | 6 + lib/JSON/WebToken/Crypt.pm | 12 ++ lib/JSON/WebToken/Crypt/RSA.pm | 37 ++++ lib/JSON/WebToken/JWK.pm | 28 +++ lib/JSON/WebToken/JWKSet.pm | 45 ++++ t/05_jwk_interface.t | 150 ++++++++++++++ tmp/MEMO | 5 + 10 files changed, 678 insertions(+), 52 deletions(-) create mode 100644 lib/JSON/WebToken/Algorithm.pm create mode 100644 lib/JSON/WebToken/JWK.pm create mode 100644 lib/JSON/WebToken/JWKSet.pm create mode 100644 t/05_jwk_interface.t diff --git a/Build.PL b/Build.PL index 3e11aa6..0499031 100644 --- a/Build.PL +++ b/Build.PL @@ -23,6 +23,16 @@ my %args = ( 'Module::Build' => 0.38, }, + requires => { + 'JSON' => 0, + 'Crypt::OpenSSL::Bignum' => 0, + 'Crypt::OpenSSL::RSA' => 0, + }, + + test_requires => { + 'Test::Mock::Guard' => 0, + }, + name => 'JSON-WebToken', module_name => 'JSON::WebToken', allow_pureperl => 0, diff --git a/lib/JSON/WebToken.pm b/lib/JSON/WebToken.pm index 426d777..2363019 100644 --- a/lib/JSON/WebToken.pm +++ b/lib/JSON/WebToken.pm @@ -4,48 +4,21 @@ use strict; use warnings; use 5.008_001; -our $VERSION = '0.08'; +our $VERSION = '0.09'; use parent 'Exporter'; use Carp qw(croak); use JSON qw(encode_json decode_json); use MIME::Base64 qw(encode_base64 decode_base64); -use Module::Runtime qw(use_module); use JSON::WebToken::Constants; use JSON::WebToken::Exception; +use JSON::WebToken::JWKSet; +use JSON::WebToken::Algorithm; our @EXPORT = qw(encode_jwt decode_jwt); -our $ALGORITHM_MAP = { - # for JWS - HS256 => 'HMAC', - HS384 => 'HMAC', - HS512 => 'HMAC', - RS256 => 'RSA', - RS384 => 'RSA', - RS512 => 'RSA', -# ES256 => 'EC', -# ES384 => 'EC', -# ES512 => 'EC', - none => 'NONE', - - # for JWE - RSA1_5 => 'RSA', -# 'RSA-OAEP' => 'OAEP', -# A128KW => '', -# A256KW => '', - dir => 'NONE', -# 'ECDH-ES' => '', -# 'ECDH-ES+A128KW' => '', -# 'ECDH-ES+A256KW' => '', - - # for JWK -# EC => 'EC', - RSA => 'RSA', -}; - #our $ENCRIPTION_ALGORITHM_MAP = { # 'A128CBC+HS256' => 'AES_CBC', # 'A256CBC+HS512' => 'AES_CBC', @@ -53,6 +26,7 @@ our $ALGORITHM_MAP = { # A256GCM => '', #}; +# old interface (working with secret) sub encode { my ($class, $claims, $secret, $algorithm, $extra_headers) = @_; unless (ref $claims eq 'HASH') { @@ -172,7 +146,8 @@ sub add_signing_algorithm { message => 'Usage: JSON::WebToken->add_signing_algorithm($algorithm, $signing_class)', ); } - $ALGORITHM_MAP->{$algorithm} = $signing_class; + + JSON::WebToken::Algorithm->add($algorithm, $signing_class); } sub _sign { @@ -180,7 +155,8 @@ sub _sign { return '' if $algorithm eq 'none'; local $Carp::CarpLevel = $Carp::CarpLevel + 1; - $class->_ensure_class_loaded($algorithm)->sign($algorithm, $message, $secret); + my $alg_class = JSON::WebToken::Algorithm->get_class($algorithm); + $alg_class->sign($algorithm, $message, $secret); } sub _verify { @@ -188,37 +164,222 @@ sub _verify { return 1 if $algorithm eq 'none'; local $Carp::CarpLevel = $Carp::CarpLevel + 1; - $class->_ensure_class_loaded($algorithm)->verify($algorithm, $message, $secret, $signature); + my $alg_class = JSON::WebToken::Algorithm->get_class($algorithm); + $alg_class->verify($algorithm, $message, $secret, $signature); +} + +# New interface (working with JWK sets) +sub new { + my $class = shift; + my $param = { + _jwt => '', + _header_dec => {}, + _header_enc => '', + _claims_dec => {}, + _cliams_enc => '', + _jwk => 0, # false + }; + + return bless($param, $class); +} + +sub set_jwt { + my ($self, $jwt) = @_; + + unless (defined $jwt) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_INVALID_PARAMETER, + message => 'Usage: JSON::WebToken->set_jwt($jwt)', + ); + } + + $self->{'_jwt'} = $jwt; + + my @segments = split /\./, $jwt; + $segments[3] = 1; # from_jwt + + $self->set_jws( @segments ); +} + +sub _decode { + my ($self, $part) = @_; + + eval { + $self->{"_${part}_dec"} = decode_json(decode_base64url($self->{"_${part}_enc"})); + }; + if (my $e = $@) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_INVALID_SEGMENT_ENCODING, + message => "Invalid segment encoding ($part)", + ); + } +} + +sub set_jws { + my ($self, $header, $claims, $sign, $from_jwt) = @_; + + unless (defined $header && defined $claims) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_INVALID_PARAMETER, + message => 'Usage: JSON::WebToken->set_jwt($first, $second, [ $third ])', + ); + } + + $self->{'_jwt'} = '' unless $from_jwt; + $self->{'_sign'} = $sign if defined $sign; + $self->{'_header_dec'} = ''; + $self->{'_header_enc'} = ''; + $self->{'_claims_dec'} = ''; + $self->{'_claims_enc'} = ''; + + if (ref($header) eq 'HASH') { + $self->{'_header_dec'} = $header; + } else { + $self->{'_header_enc'} = $header; + } + if (ref($claims) eq 'HASH') { + $self->{'_claims_dec'} = $claims; + } else { + $self->{'_claims_enc'} = $claims; + } +} + +sub header { + my ($self, $key, $value) = @_; + + $self->_decode('header') unless $self->{'_header_dec'}; + + unless (defined $key) { + return $self->{'_header_dec'}; + } + + unless (defined $value) { + return $self->{'_header_dec'}->{$key}; + } + + $self->{'_jwt'} = ''; + $self->{'_header_enc'} = ''; + $self->{'_header_dec'}->{$key} = $value; +} + +sub claims { + my ($self, $key, $value) = @_; + + $self->_decode('claims') unless $self->{'_claims_dec'}; + + unless (defined $key) { + return $self->{'_claims_dec'}; + } + + unless (defined $value) { + return $self->{'_claims_dec'}->{$key}; + } + + $self->{'_jwt'} = ''; + $self->{'_claims_enc'} = ''; + $self->{'_claims_dec'}->{$key} = $value; +} + +sub _encode { + my $self = shift; + my $jwk = $self->{'_jwk'}; + + unless ($jwk) { + JSON::WebToken::Exception->throw( + code => ERROR_JWK_IS_NOT_DEFINED, + message => 'JWK is not defined. Define it with JSON::WebToken->jwk($json)', + ); + } + + $self->{'_header_enc'} = encode_base64url(encode_json($self->{'_header_dec'}), ''); + $self->{'_claims_enc'} = encode_base64url(encode_json($self->{'_claims_dec'}), ''); + + my $for_sig = $self->{'_header_enc'} .'.'. $self->{'_claims_enc'}; + $self->{'_jwt'} = $for_sig .'.'; + + my $alg = $self->header('alg'); + return if $alg eq 'none'; + + my $kid = $self->header('kid'); + my $key = $jwk->get_key($kid); + + unless ($key) { + JSON::WebToken::Exception->throw( + code => ERROR_JWK_MISSING_FOR_KID, + message => 'There are no JWK for given KID', + ); + } + + my $alg_class = JSON::WebToken::Algorithm->get_class($alg); + $self->{'_jwt'} .= encode_base64url($alg_class->sign_with_jwk($alg, $for_sig, $key), ''); +} + +sub encoded { + my $self = shift; + + $self->_encode unless $self->{'_jwt'}; + + return $self->{'_jwt'}; } -my (%class_loaded, %alg_to_class); -sub _ensure_class_loaded { - my ($class, $algorithm) = @_; - return $alg_to_class{$algorithm} if $alg_to_class{$algorithm}; +sub verify { + my $self = shift; + my $jwk = $self->{'_jwk'}; - my $klass = $ALGORITHM_MAP->{$algorithm}; - unless ($klass) { + unless ($jwk) { JSON::WebToken::Exception->throw( - code => ERROR_JWT_NOT_SUPPORTED_SIGNING_ALGORITHM, - message => "`$algorithm` is Not supported siging algorithm", + code => ERROR_JWK_IS_NOT_DEFINED, + message => 'JWK is not defined. Define it with JSON::WebToken->jwk($json)', ); } - my $signing_class = $klass =~ s/^\+// ? $klass : "JSON::WebToken::Crypt::$klass"; - return $signing_class if $class_loaded{$signing_class}; + my $kid = $self->header('kid'); + my $key = $jwk->get_key($kid); - use_module $signing_class unless $class->_is_inner_package($signing_class); + unless ($key) { + JSON::WebToken::Exception->throw( + code => ERROR_JWK_MISSING_FOR_KID, + message => 'There are no JWK for given KID', + ); + } + + unless ($self->{'_header_enc'} && $self->{'_claims_enc'}) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_MISSING_ENCODED_DATA, + message => 'There are not encoded header and claims', + ); + } - $class_loaded{$signing_class} = 1; - $alg_to_class{$algorithm} = $signing_class; + my $for_sig = $self->{'_header_enc'} .'.'. $self->{'_claims_enc'}; + my $alg = $self->header('alg'); + my $sign_dec = decode_base64url($self->{'_sign'}); - return $signing_class; + my $alg_class = JSON::WebToken::Algorithm->get_class($alg); + return $alg_class->verify_with_jwk($alg, $for_sig, $key, $sign_dec); } -sub _is_inner_package { - my ($class, $klass) = @_; - no strict 'refs'; - %{ "$klass\::" } ? 1 : 0; +sub jwk { + my ($self, $data) = @_; + my $jwk_set = JSON::WebToken::JWKSet->new; + + unless (defined $data) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_INVALID_PARAMETER, + message => 'Usage: JSON::WebToken->jwk($json)', + ); + } + + eval { + $jwk_set->parse($data); + }; + if (my $err = $@) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_INVALID_PARAMETER, + message => 'Invalid JWK, should be in JSON format', + ); + } + + $self->{'_jwk'} = $jwk_set; } #################################################### @@ -336,6 +497,91 @@ This method is adding signing algorithm. SEE ALSO L<< JSON::WebToken::Crypt::HMAC >> or L<< JSON::WebToken::Crypt::RAS >>. +=head2 new() + +Default constructor for OOP interface. Does not need any additional parameter to be passed into. + +e.g. + + my $jwt = JSON::WebToken->new; + +=head2 set_jwt($jwt) + +Accepts JWT defined in L<< http://tools.ietf.org/html/draft-jones-json-web-token-10 >> and sets internal variables. Does not return anything. + +Trig exception only if no C<< $jwt >> is passed. + +e.g. + + $jwt->set_jwt("eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + +=head2 set_jws($header, $claims, [ $signature ]) + +Accepts 3 JWS parameters in different formats. This method sets internal variables for future use. + +e.g. + + # for decoding purposes + $jwt->set_jws("eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9", "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ", "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"); + + # for encoding purposes + $jwt->set_jws({kid => "example", alg => 'RS256'}, {iss => 'example.com}); + +=head2 header([$key, [ $value ]]) + +Method to manipulate with header data. This method transparently decode data if needed. ERROR_JWT_INVALID_SEGMENT_ENCODING - exception will be raised if decode will fail. + +e.g. + # without any paramters return header as a hash reference + my $header = $jwt->header; + + # or you can get value via $key + my $kid = $jwt->header('kid'); + + # or you can set value + $jwt->header('alg' => 'RS256'); + +=head2 claims([$key, [ $value ]]) + +Method to manipulate with claims data. This method transparently decode data if needed. ERROR_JWT_INVALID_SEGMENT_ENCODING - exception will be raised if decode will fail. + +e.g. + + # get all claims hash + my $claims = $jwt->claims; + + # get by $key + my $email = $jwt->claims('email'); + + # set value + $jwt->claims('email' => 'example@example.com'); + +=head2 encoded() + +Returns JWT string in a format acceptable by method C<< set_jws() >>. Transparently encode JWT. ERROR_JWK_IS_NOT_DEFINED or ERROR_JWK_MISSING_FOR_KID can be raised. + +e.g. + + my $jwt_string = $jwt->encoded; + +=head2 verify() + +Verifies signature of JWT. The following list of exceptions can be triggered: ERROR_JWK_IS_NOT_DEFINED, ERROR_JWK_MISSING_FOR_KID, ERROR_JWT_MISSING_ENCODED_DATA. + +e.g. + + if ($jwt->verify) { + print "Verified"; + } + +=head2 jwk( $data ) + +Accepts $data as a json string. Parse json JWK and sets internal variables. Can accept 1 key or set of keys (JWK Sets). + +e.g. + + $jwt->jwk({ "kty":"oct", "alg":"A128KW", "k":"GawgguFyGrWKav7AX4VKUg" }); + =head1 FUNCTIONS =head2 encode_jwt($claims [, $secret, $algorithm, $extra_headers ]) : String @@ -378,15 +624,29 @@ When JWT signature is invalid. When given signing algorithm is not supported. +=head2 ERROR_JWK_IS_NOT_DEFINED + +When you try to encode/decode data without defining JWK via method C<< jwk() >> + +=head2 ERROR_JWK_MISSING_FOR_KID + +When you JWK could not be found by provided KID in header. + +=head2 ERROR_JWT_MISSING_ENCODED_DATA + +When you try to verify data which are not encoded. + =head1 AUTHOR xaicron Exaicron@cpan.orgE zentooo +cono Econo@cpan.orgE + =head1 COPYRIGHT -Copyright 2012 - xaicron +Copyright 2014 - xaicron =head1 LICENSE @@ -397,4 +657,8 @@ it under the same terms as Perl itself. L<< http://tools.ietf.org/html/draft-ietf-oauth-json-web-token >> +L<< http://tools.ietf.org/html/draft-ietf-jose-json-web-key-31 >> + +L<< http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-16 >> + =cut diff --git a/lib/JSON/WebToken/Algorithm.pm b/lib/JSON/WebToken/Algorithm.pm new file mode 100644 index 0000000..30fa3b1 --- /dev/null +++ b/lib/JSON/WebToken/Algorithm.pm @@ -0,0 +1,69 @@ +package JSON::WebToken::Algorithm; + +use strict; +use warnings; + +use Class::Load qw(load_class); +# local +use JSON::WebToken::Constants; +use JSON::WebToken::Exception; + +our $ALGORITHM_MAP = { + # for JWS + HS256 => 'HMAC', + HS384 => 'HMAC', + HS512 => 'HMAC', + RS256 => 'RSA', + RS384 => 'RSA', + RS512 => 'RSA', +# ES256 => 'EC', +# ES384 => 'EC', +# ES512 => 'EC', + none => 'NONE', + + # for JWE + RSA1_5 => 'RSA', +# 'RSA-OAEP' => 'OAEP', +# A128KW => '', +# A256KW => '', + dir => 'NONE', +# 'ECDH-ES' => '', +# 'ECDH-ES+A128KW' => '', +# 'ECDH-ES+A256KW' => '', + + # for JWK +# EC => 'EC', + RSA => 'RSA', +}; + +sub get_class { + my ($class, $algorithm) = @_; + + return $class->_ensure_class_loaded($algorithm); +} + +sub add { + my (undef, $algorithm, $class) = @_; + + $ALGORITHM_MAP->{$algorithm} = $class; +} + +my %alg_to_class; +sub _ensure_class_loaded { + my ($class, $algorithm) = @_; + return $alg_to_class{$algorithm} if $alg_to_class{$algorithm}; + + my $klass = $ALGORITHM_MAP->{$algorithm}; + unless ($klass) { + JSON::WebToken::Exception->throw( + code => ERROR_JWT_NOT_SUPPORTED_SIGNING_ALGORITHM, + message => "`$algorithm` is Not supported siging algorithm", + ); + } + + my $signing_class = $klass =~ s/^\+// ? $klass : "JSON::WebToken::Crypt::$klass"; + + return $alg_to_class{$algorithm} = load_class($signing_class); +} + +42; diff --git a/lib/JSON/WebToken/Constants.pm b/lib/JSON/WebToken/Constants.pm index 31bf84d..687b028 100644 --- a/lib/JSON/WebToken/Constants.pm +++ b/lib/JSON/WebToken/Constants.pm @@ -12,6 +12,9 @@ my @error_code = qw/ ERROR_JWT_UNWANTED_SIGNATURE ERROR_JWT_INVALID_SIGNATURE ERROR_JWT_NOT_SUPPORTED_SIGNING_ALGORITHM + ERROR_JWK_IS_NOT_DEFINED + ERROR_JWK_MISSING_FOR_KID + ERROR_JWT_MISSING_ENCODED_DATA /; our @EXPORT = @error_code; @@ -29,6 +32,9 @@ use constant { ERROR_JWT_UNWANTED_SIGNATURE => "unwanted_signature", ERROR_JWT_INVALID_SIGNATURE => "invalid_signature", ERROR_JWT_NOT_SUPPORTED_SIGNING_ALGORITHM => "not_supported_signing_algorithm", + ERROR_JWK_IS_NOT_DEFINED => "jwk_is_not_defined", + ERROR_JWK_MISSING_FOR_KID => "missing_for_kid", + ERROR_JWT_MISSING_ENCODED_DATA => "missing_encoded_data", }; 1; diff --git a/lib/JSON/WebToken/Crypt.pm b/lib/JSON/WebToken/Crypt.pm index 3ba9e37..4728c00 100644 --- a/lib/JSON/WebToken/Crypt.pm +++ b/lib/JSON/WebToken/Crypt.pm @@ -13,5 +13,17 @@ sub verify { die 'verify method must be implements!' } +sub sign_with_jwk { + my ($class, $alg, $msg, $key) = @_; + + $class->sign($alg, $msg, $key->decode_param('k')); +} + +sub verify_with_jwk { + my ($class, $alg, $msg, $key, $sign) = @_; + + return $class->sign($alg, $msg, $key->decode_param('k')) eq $sign; +} + 1; __END__ diff --git a/lib/JSON/WebToken/Crypt/RSA.pm b/lib/JSON/WebToken/Crypt/RSA.pm index 5f7d846..453a7f8 100644 --- a/lib/JSON/WebToken/Crypt/RSA.pm +++ b/lib/JSON/WebToken/Crypt/RSA.pm @@ -4,6 +4,7 @@ use strict; use warnings; use parent 'JSON::WebToken::Crypt'; +use Crypt::OpenSSL::Bignum; use Crypt::OpenSSL::RSA (); our $ALGORITHM2SIGNING_METHOD_MAP = { @@ -31,5 +32,41 @@ sub verify { return $public_key->verify($message, $signature) ? 1 : 0; } +sub sign_with_jwk { + my ($class, $alg, $msg, $key) = @_; + + my @param; + for my $k ( qw|n e d p q| ) { + my $val = $key->decode_param($k); + last unless $val; + + push @param, Crypt::OpenSSL::Bignum->new_from_bin( $val ); + } + my $crypt = Crypt::OpenSSL::RSA->new_key_from_parameters(@param); + my $method = $ALGORITHM2SIGNING_METHOD_MAP->{$alg}; + + $crypt->$method; + + return $crypt->sign($msg); +} + +sub verify_with_jwk { + my ($class, $alg, $msg, $key, $sign) = @_; + + my @param; + for my $k ( qw|n e d p q| ) { + my $val = $key->decode_param($k); + last unless $val; + + push @param, Crypt::OpenSSL::Bignum->new_from_bin( $val ); + } + my $crypt = Crypt::OpenSSL::RSA->new_key_from_parameters(@param); + my $method = $ALGORITHM2SIGNING_METHOD_MAP->{$alg}; + + $crypt->$method; + + return $crypt->verify($msg, $sign); +} + 1; __END__ diff --git a/lib/JSON/WebToken/JWK.pm b/lib/JSON/WebToken/JWK.pm new file mode 100644 index 0000000..5dd0571 --- /dev/null +++ b/lib/JSON/WebToken/JWK.pm @@ -0,0 +1,28 @@ +package JSON::WebToken::JWK; + +use strict; +use warnings; + +use MIME::Base64 qw(decode_base64url); + +sub new { + my ($class, $key) = @_; + + return bless($key, $class); +} + +sub get_param { + my ($self, $key) = @_; + + return $self->{$key}; +} + +sub decode_param { + my ($self, $key) = @_; + + return unless exists $self->{$key}; + + return decode_base64url($self->{$key}); +} + +42; diff --git a/lib/JSON/WebToken/JWKSet.pm b/lib/JSON/WebToken/JWKSet.pm new file mode 100644 index 0000000..ad8700c --- /dev/null +++ b/lib/JSON/WebToken/JWKSet.pm @@ -0,0 +1,45 @@ +package JSON::WebToken::JWKSet; + +use strict; +use warnings; + +use JSON; +# local +use JSON::WebToken::JWK; + +sub new { + my $class = shift; + my $param = { + _json => JSON->new, + _set => {} + }; + + return bless($param, $class); +} + +sub parse { + my ($self, $data) = @_; + my $json = $self->{'_json'}; + + my $set = $json->decode($data); + # JWK RFC 4.1. "keys" Parameter + my $keys = exists $set->{'keys'} ? + $set->{'keys'} : + [ $set ]; + + for my $single ( @$keys ) { + my $key = JSON::WebToken::JWK->new($single); + my $kid = $key->get_param('kid'); # JWK RFC 3.4. "kid" (Key ID) Parameter + + $self->{'_set'}->{$kid} = $key; + } +} + +sub get_key { + my $self = shift; + my $kid = shift; + + return $self->{'_set'}->{$kid}; +} + +42; diff --git a/t/05_jwk_interface.t b/t/05_jwk_interface.t new file mode 100644 index 0000000..b78a23d --- /dev/null +++ b/t/05_jwk_interface.t @@ -0,0 +1,150 @@ +use strict; +use warnings; + +use Test::More; +use JSON::WebToken; + +sub KEY_DATA() { + return <<'EOT'; +{ + "keys": [ + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "c8cc7d0be76efcaad0cac2b67d9bf04d05ffa3b3", + "n": "ALjTgs7MwLiBTwbv1YFp4C9LnRl2wzHTAFaIctAYqvqCNSd3CR6qZQYloFckafqjvpWRZve30Dlm9BxuRV27gcdyVvfC/C1qQW3DexxuDWFfm13AJWqWOIJoOv1mYdPdJ+7r9Leasoj3eN38bwLnuvd+jNqHTHzAetP6a+zzmHRV", + "e": "AQAB" + }, + { + "kty": "RSA", + "alg": "RS256", + "use": "sig", + "kid": "bf3289859b038f80b2bf70bc21fd0520ab274fd3", + "n": "AL1JGScV3IL+FXaRBDVizMkAWCfecuDRQMt3cuDKG7weLmUYZ/Q0Dk/sGNioHv6I0fMR/wD5GoBB8wwghuhiaobVJvfpYr7HWCrQxxrQWhDwXVPQbtAIPqRtRj2L/c+U0d4lrhuERQoRd2Swx3Nv9flNXQomMzFuLWwX/ZUqA1vv", + "e": "AQAB" + }, + { + "kty":"RSA", + "q":"xexvbU3YHoS7bBT4skSWlQrM8WoJTltq7R3mLukMTtA4YZ0lzsrtFC68DA1Q7MeXisITP7XkTazVSTd/OIURBepZlAA+vXCyq0JI8siSd+aR0P/yhMGFWHSC0EuLNU4UrERKLPFb7hljSPNoXAy2O2zwOX4rtNXwZ5jWqTKk368=","p":"+e2U2O+czumaiu/JDnMZzgsgKULXFYzGuLH1k3HrI7fCeFX79sG1EluXks0kaGJ8tioyA3Q8OdaRG3QHEh3DawbHe1KKG8I5fDKIzfS6QNpQPbryUY04KKjwugwVw6Si6hQQ0Z1lg1tarYAiMMr4bvPKZa9Uhy58BSXGgkfcErM=","e":"AQAB","d":"kYeqaFAEU/Tehp6kPZeY7yp1VCH0S0bmCWO2Bps2ea2KGEjqoy+8WnkRwNbryIuowMh01dO3Msz/GlY7y+gHeX+rhu/dcIneMD9+G/DQFQMK9PHZc39CPWb3PUv8aZgA0GUXH1x9QLAUmymVnyuQaDAE0XVy2q/BShXFgZpHWpn/ndEQIaihwfg8aauYv1oV1JcNRM8RfrayXDkGxom5JkKvZlMEWjg+dyymZq1E3o7Ow72KyI4q44y+nbIBOi6QxiJMfMNt9CGywAlsy8L5rMB7zCRIcaJhbaYekSmFqL6cScjusmfSFmykKZbszAKCXwIbf3kP7X5Of8k9aUCpxQ==","n":"wTqnWOISdBHKy1j9NKhYiZA2g1lu0NXF8aR55QFr6Nj7GQ0B33OWxGYnf/WcCGYzi68EIpYc3D166+tuWot7dkNbakCi0QBp21xo2N9p1FXoq80vUB6RXCo9+tyB4L+UqZZekqQNSJTb5z2dSMR2Ss1BYiU1olsbE+1Dp48QXjNVWeydA4hk/5AM57PBspBbMgADbxTLz+FzCn0QcjG8msO2Ru/5ePglaBtVoynXSDtQkst4gMBnvUeA3GJxLtqI+tAM0uVI1Kao45aQVGgrveGBZBvXyQlrWklZc2HWHKFs8ti6T3O3Pyi0J1OAtK4kyB51jH09HVCRby51xGa1XQ==", + "alg":"RS256", + "kid":"example" + } + ] +} +EOT +} + +sub JWT() { + return "eyJhbGciOiJSUzI1NiIsImtpZCI6ImV4YW1wbGUifQ==.eyJhdF9oYXNoIjoic3VlY2llVzBBaEs4QWVjaGllbWFleiIsImF1ZCI6IndvOGFobjhvb2QzdmlmYWlQaGllaHVvY2gyYWhtYWVGYWl4ZXBlTGFraWV5aS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6MTIzNDU2Nzg5MDA5ODc2NTQzMjEwLCJleHAiOjE0MDU2OTQ4MTUsImVtYWlsIjoiZXhhbXBsZUBleGFtcGxlLmNvbSIsImlhdCI6MTQwNTY5MDkxNSwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6ImFjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiJ3bzhhaG44b29kM3ZpZmFpUGhpZWh1b2NoMmFobWFlRmFpeGVwZUxha2lleWkuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJoZCI6ImV4YW1wbGUuY29tIn0=.diQGbXuSzLh+fnMQc6wbKwwQ4QLkVu1yyex7II7mMfb7PUmsPmmpG7aMT0xXePsl6f+grzmWPtqY5z2Z0n+UV4CxEMM7tYnFb2C8mExIvBXm1skjhdVFKpmbVTkzOuPrNfUO1T9mXZ3uylqKYkVqlg1rwRzvL32ZotKxyU5OwOIh9TDUXhHF+oFtg7jJ9K85euFKqOUPZtaes9KdtaOCAJUyEfZLfuiOTtCNtmuMpY4zK9spm0H/iVNp6gLKUqwjUWbiyUuxmu+DeE0h507OKybMb0JSBIxc9WJ6WlTtuiv2RMTyGR6F5npJdsrOnKtkcud5Ix1yUxPHi4bBqk3GvQ=="; +} + +subtest 'basic OOP interface' => sub { + my $jwt = JSON::WebToken->new; + + isa_ok($jwt, 'JSON::WebToken'); + + my @methods = qw( set_jwt set_jws header claims encoded verify jwk ); + can_ok($jwt, @methods); + + done_testing; +}; + +subtest 'jwk' => sub { + my $jwt = JSON::WebToken->new; + + my $jwk_set = $jwt->jwk(KEY_DATA); + isa_ok($jwk_set, 'JSON::WebToken::JWKSet'); + can_ok($jwk_set, qw| parse get_key |); + + ok(!$jwk_set->get_key('42'), 'get_key return empty key'); + + my $jwk = $jwk_set->get_key('c8cc7d0be76efcaad0cac2b67d9bf04d05ffa3b3'); + isa_ok($jwk, 'JSON::WebToken::JWK'); + can_ok($jwk, qw| get_param decode_param |); + + is($jwk->get_param('alg'), 'RS256', 'get_param returns algorithm'); + + my $n = $jwk->get_param('n'); + my $n_decoded = $jwk->decode_param('n'); + + ok($n, 'get_param n'); + ok($n_decoded, 'decode_param n'); + + isnt($n, $n_decoded, 'decoded not equal to actual value'); + + done_testing; +}; + +subtest 'decode + verify' => sub { + my $jwt = JSON::WebToken->new; + my $jwk_set = $jwt->jwk(KEY_DATA); + + eval { + $jwt->set_jwt(JWT); + }; + + ok(!$@, 'parse successful'); + is($jwt->header('kid'), 'example', 'get kid from header'); + is($jwt->claims('email'), 'example@example.com', 'get email from claims'); + ok($jwt->verify, 'verify successful'); + + done_testing; +}; + +subtest 'encode' => sub { + my $jwt = JSON::WebToken->new; + my $jwk_set = $jwt->jwk(KEY_DATA); + + my $header = { + alg => 'RS256', + kid => 'test' + }; + my $claims = { + iss => 'example.com', + exp => time, + aud => 'aaa.example.com' + }; + + $jwt->set_jws($header, $claims); + + eval { + $jwt->encoded + }; + ok($@, 'encoded fail due to wrong kid in header'); + + $jwt->header(kid => 'example'); + ok($jwt->encoded, 'encoded data'); + + done_testing; +}; + +subtest 'encode + decode + verify' => sub { + my $jwt = JSON::WebToken->new; + my $jwk_set = $jwt->jwk(KEY_DATA); + + my $header = { + alg => 'RS256', + kid => 'example' + }; + my $claims = { + iss => 'example.com', + exp => time, + aud => 'aaa.example.com', + email => 'test@example.com' + }; + + $jwt->set_jws($header, $claims); + my $token = $jwt->encoded; + + $jwt = JSON::WebToken->new; + $jwt->jwk(KEY_DATA); + + $jwt->set_jwt($token); + + is_deeply($jwt->claims, $claims, 'claims decrypted successfully'); + + done_testing; +}; + +done_testing; diff --git a/tmp/MEMO b/tmp/MEMO index 2ab7f71..fa793ad 100644 --- a/tmp/MEMO +++ b/tmp/MEMO @@ -1,3 +1,8 @@ +JWK +http://tools.ietf.org/html/draft-ietf-jose-json-web-key-18 +JWA +http://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-31 + 1. JWE Header 2. JWE Encrypted Key 3. JWE Ciphertext From 71d04e8a98bcef4ebb8cf64aa4670839050148fd Mon Sep 17 00:00:00 2001 From: cono Date: Fri, 25 Jul 2014 16:58:59 +0000 Subject: [PATCH 2/3] Use internal decode_base64url implementation for backward compatibility --- lib/JSON/WebToken/JWK.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/JSON/WebToken/JWK.pm b/lib/JSON/WebToken/JWK.pm index 5dd0571..d9baff5 100644 --- a/lib/JSON/WebToken/JWK.pm +++ b/lib/JSON/WebToken/JWK.pm @@ -3,7 +3,7 @@ package JSON::WebToken::JWK; use strict; use warnings; -use MIME::Base64 qw(decode_base64url); +use JSON::WebToken; sub new { my ($class, $key) = @_; @@ -22,7 +22,7 @@ sub decode_param { return unless exists $self->{$key}; - return decode_base64url($self->{$key}); + return JSON::WebToken::decode_base64url($self->{$key}); } 42; From 6fb53165ffa4f442859016c8310c0ee745b01e52 Mon Sep 17 00:00:00 2001 From: cono Date: Fri, 25 Jul 2014 17:15:12 +0000 Subject: [PATCH 3/3] Add skip tests if Crypt::OpenSSL::RSA module not found --- t/05_jwk_interface.t | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/t/05_jwk_interface.t b/t/05_jwk_interface.t index b78a23d..e0b8171 100644 --- a/t/05_jwk_interface.t +++ b/t/05_jwk_interface.t @@ -2,8 +2,13 @@ use strict; use warnings; use Test::More; +use Class::Load qw|try_load_class|; use JSON::WebToken; +sub TRY_RSA() { + return try_load_class('Crypt::OpenSSL::RSA') && try_load_class('Crypt::OpenSSL::Bignum'); +} + sub KEY_DATA() { return <<'EOT'; { @@ -87,12 +92,18 @@ subtest 'decode + verify' => sub { ok(!$@, 'parse successful'); is($jwt->header('kid'), 'example', 'get kid from header'); is($jwt->claims('email'), 'example@example.com', 'get email from claims'); - ok($jwt->verify, 'verify successful'); + + SKIP: { + skip "Crypt::OpenSSL::RSA && Bignum required to run this test", 1 unless TRY_RSA; + ok($jwt->verify, 'verify successful'); + } done_testing; }; subtest 'encode' => sub { + plan skip_all => "Crypt::OpenSSL::RSA && Bignum require to run these tests" unless TRY_RSA; + my $jwt = JSON::WebToken->new; my $jwk_set = $jwt->jwk(KEY_DATA); @@ -120,6 +131,8 @@ subtest 'encode' => sub { }; subtest 'encode + decode + verify' => sub { + plan skip_all => "Crypt::OpenSSL::RSA && Bignum require to run these tests" unless TRY_RSA; + my $jwt = JSON::WebToken->new; my $jwk_set = $jwt->jwk(KEY_DATA);