#!/usr/bin/env perl
my
$class
=
'OIDC::Client'
;
use_ok
$class
;
my
$test
= OIDCClientTest->new();
launch_tests();
done_testing;
sub
test_build_with_exceptions {
subtest
"BUILD with exceptions"
=>
sub
{
throws_ok {
$class
->new(
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
log
=>
$log
,
);
}
qr/no provider/
,
'provider is missing'
;
throws_ok {
$class
->new(
provider
=>
'my_provider'
,
secret
=>
'my_client_secret'
,
log
=>
$log
,
);
}
qr/no id/
,
'id is missing'
;
throws_ok {
$class
->new(
id
=>
'my_client_id'
,
log
=>
$log
,
);
}
qr/no provider/
,
'provider is missing'
;
throws_ok {
$class
->new(
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
log
=>
$log
,
);
}
qr/no secret/
,
'secret is missing'
;
throws_ok {
$class
->new(
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
log
=>
$log
,
);
}
qr/jwks_url not found in provider metadata/
,
'jwks_url is missing'
;
throws_ok {
$class
->new(
log
=>
$log
,
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
audience
=>
'my_audience'
,
audience_alias
=> {
alias1
=> {
audience
=>
'audience1'
,
},
alias2
=> {
audience
=>
'audience2'
,
},
alias3
=> {
audience
=>
'audience1'
,
},
alias4
=> {
audience
=>
'my_audience'
,
},
},
},
);
}
qr/these configured audiences are duplicated: my_audience, audience1/
,
'duplicates audiences'
;
};
}
sub
test_secret_from_config {
subtest
"secret from config"
=>
sub
{
my
%config
= (
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
provider
=>
'my_provider'
,
);
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> \
%config
,
);
is(
$client
->secret,
'my_client_secret'
,
'from config'
);
};
}
sub
test_secret_from_env {
subtest
"secret from ENV"
=>
sub
{
my
%config
= (
id
=>
'my_client_id'
,
provider
=>
'my_provider'
,
);
throws_ok {
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> \
%config
,
);
}
qr/OIDC: no secret configured or set up in environment/
,
'missing secret'
;
local
$ENV
{OIDC_MY_PROVIDER_SECRET} =
'secret'
;
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> \
%config
,
);
is(
$client
->secret,
'secret'
,
'from environment variable'
);
};
}
sub
test_user_agent {
subtest
"user_agent"
=>
sub
{
my
%config
= (
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
proxy_detect
=> 1,
user_agent
=>
'my_user_agent'
,
);
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> \
%config
,
);
is(
$client
->user_agent->transactor->name,
'my_user_agent'
,
'expected user agent name'
);
};
}
sub
test_claim_mapping_from_config {
subtest
"claim_mapping from config"
=>
sub
{
my
%claim_key
= (
login
=>
'sub'
,
lastname
=>
'lastName'
,
firstname
=>
'firstName'
,
);
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
claim_mapping
=> \
%claim_key
,
},
);
my
$claim_mapping
=
$client
->claim_mapping;
cmp_deeply(
$claim_mapping
, \
%claim_key
,
'from config'
);
};
}
sub
test_claim_mapping_from_default_value {
subtest
"claim_mapping from default value"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$claim_mapping
=
$client
->claim_mapping;
my
%expected
= ();
cmp_deeply(
$claim_mapping
, \
%expected
,
'from default value'
);
};
}
sub
test_decode_jwt_options_from_config {
subtest
"decode_jwt_options from config"
=>
sub
{
my
%options
= (
verify_exp
=> 1,
leeway
=> 20,
);
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
decode_jwt_options
=> \
%options
,
},
);
my
$decode_jwt_options
=
$client
->decode_jwt_options;
cmp_deeply(
$decode_jwt_options
, \
%options
,
'from config'
);
};
}
sub
test_decode_jwt_options_from_default_value {
subtest
"decode_jwt_options from default value"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$decode_jwt_options
=
$client
->decode_jwt_options;
my
%expected
= (
leeway
=> 60);
cmp_deeply(
$decode_jwt_options
, \
%expected
,
'from default value'
);
};
}
sub
test_provider_metadata_from_config {
subtest
"provider_metadata from config"
=>
sub
{
$log
->clear();
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
authorize_url
=>
'my_authorize_url'
,
jwks_url
=>
'my_jwks_url'
,
},
);
my
$provider_metadata
=
$client
->provider_metadata;
my
%expected_provider_metadata
= (
authorize_url
=>
'my_authorize_url'
,
jwks_url
=>
'my_jwks_url'
,
);
cmp_deeply(
$provider_metadata
, \
%expected_provider_metadata
,
'retrieved from config'
);
$log
->empty_ok(
'no log'
);
};
}
sub
test_provider_metadata_from_well_known_url {
$test
->mock_user_agent(
to_mock
=> {
get
=> {
authorization_endpoint
=>
'provider_authorize_url'
,
end_session_endpoint
=>
'provider_end_session_url'
,
issuer
=>
'provider_issuer'
,
token_endpoint
=>
'provider_token_url'
,
userinfo_endpoint
=>
'provider_userinfo_url'
,
jwks_uri
=>
'provider_jwks_url'
,
},
}
);
$test
->mock_response_parser();
subtest
"provider_metadata from well_known url"
=>
sub
{
$log
->clear();
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
user_agent
=>
$test
->mocked_user_agent,
response_parser
=>
$test
->mocked_response_parser,
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
well_known_url
=>
'my_well_known_url'
,
},
);
my
$provider_metadata
=
$client
->provider_metadata;
my
%expected_provider_metadata
= (
authorize_url
=>
'provider_authorize_url'
,
end_session_url
=>
'provider_end_session_url'
,
issuer
=>
'provider_issuer'
,
token_url
=>
'provider_token_url'
,
userinfo_url
=>
'provider_userinfo_url'
,
jwks_url
=>
'provider_jwks_url'
,
);
cmp_deeply(
$provider_metadata
, \
%expected_provider_metadata
,
'retrieved from well-known url'
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
[
'get'
, [
$test
->mocked_user_agent,
'my_well_known_url'
] ],
'expected call to user agent'
);
cmp_deeply(
$log
->msgs,
[
superhashof({
message
=>
'OIDC/my_provider: fetching OpenID configuration from my_well_known_url'
,
level
=>
'info'
,
}),
],
'expected log'
);
};
subtest
"provider_metadata from well_known url + config"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
user_agent
=>
$test
->mocked_user_agent,
response_parser
=>
$test
->mocked_response_parser,
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
well_known_url
=>
'my_well_known_url'
,
authorize_url
=>
'my_authorize_url'
,
},
);
my
$provider_metadata
=
$client
->provider_metadata;
my
%expected_provider_metadata
= (
authorize_url
=>
'my_authorize_url'
,
end_session_url
=>
'provider_end_session_url'
,
issuer
=>
'provider_issuer'
,
token_url
=>
'provider_token_url'
,
userinfo_url
=>
'provider_userinfo_url'
,
jwks_url
=>
'provider_jwks_url'
,
);
cmp_deeply(
$client
->provider_metadata, \
%expected_provider_metadata
,
'retrieved from well-known url + config'
);
};
}
sub
test_kid_keys {
$test
->mock_user_agent(
to_mock
=> {
get
=> {
keys
=> [
'a'
] },
}
);
$test
->mock_response_parser();
subtest
"kid_keys"
=>
sub
{
$log
->clear();
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
response_parser
=>
$test
->mocked_response_parser,
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
jwks_url
=>
'my_jwks_url'
,
claim_mapping
=> {
login
=>
'sub'
,
}
},
);
my
$kid_keys
=
$client
->kid_keys;
cmp_deeply(
$kid_keys
, {
keys
=> [
'a'
] },
'retrieved from jwks url'
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
[
'get'
, [
$test
->mocked_user_agent,
'my_jwks_url'
] ],
'expected call to user agent'
);
cmp_deeply(
$log
->msgs,
[
superhashof({
message
=>
'OIDC/my_provider: fetching JWT kid keys'
,
level
=>
'info'
,
}),
],
'expected log'
);
};
}
sub
test_auth_url_croaks_without_authorize_url {
subtest
"auth_url() croaks without authorize_url"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
throws_ok {
$client
->auth_url() }
qr/OIDC: authorize url not found in provider metadata/
,
'missing authorize url'
;
};
}
sub
test_auth_url_returning_string {
subtest
"auth_url() returning string"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
{
my
$auth_url
=
$client
->auth_url();
ok(!
ref
$auth_url
,
'is a scalar'
);
my
$mojo_auth_url
= Mojo::URL->new(
$auth_url
);
is(
$mojo_auth_url
->protocol,
'https'
,
'expected scheme'
);
is(
$mojo_auth_url
->host,
'my-provider'
,
'expected host'
);
is(
$mojo_auth_url
->path,
'/authorize'
,
'expected path'
);
my
%expected_query_params
= (
response_type
=>
'code'
,
client_id
=>
'my_client_id'
,
);
cmp_deeply(
$mojo_auth_url
->query->to_hash, \
%expected_query_params
,
'expected query params with minimum arguments'
);
}
{
my
$auth_url
=
$client
->auth_url(
redirect_uri
=>
'my_redirect_uri'
,
state
=>
'my_state'
,
scope
=>
'my scope'
,
audience
=>
'my_audience'
,
extra_params
=> {
other_param1
=>
'my_other_param1'
,
other_param2
=>
'my_other_param2'
},
);
my
$mojo_auth_url
= Mojo::URL->new(
$auth_url
);
my
%expected_query_params
= (
response_type
=>
'code'
,
client_id
=>
'my_client_id'
,
redirect_uri
=>
'my_redirect_uri'
,
state
=>
'my_state'
,
scope
=>
'my scope'
,
audience
=>
'my_audience'
,
other_param1
=>
'my_other_param1'
,
other_param2
=>
'my_other_param2'
,
);
cmp_deeply(
$mojo_auth_url
->query->to_hash, \
%expected_query_params
,
'expected query params with maximum arguments'
);
}
};
}
sub
test_auth_url_returning_mojo_url {
subtest
"auth_url() returning a mojo url"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
signin_redirect_uri
=>
'my_signin_redirect_uri'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
authorize_endpoint_extra_params
=> {
other_param
=>
'my_other_param'
},
},
);
my
$auth_url
=
$client
->auth_url(
want_mojo_url
=> 1);
isa_ok(
$auth_url
,
'Mojo::URL'
);
is(
$auth_url
->protocol,
'https'
,
'expected scheme'
);
is(
$auth_url
->host,
'my-provider'
,
'expected host'
);
is(
$auth_url
->path,
'/authorize'
,
'expected path'
);
my
%expected_query_params
= (
response_type
=>
'code'
,
client_id
=>
'my_client_id'
,
redirect_uri
=>
'my_signin_redirect_uri'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
other_param
=>
'my_other_param'
,
);
cmp_deeply(
$auth_url
->query->to_hash, \
%expected_query_params
,
'expected query params with config parameters'
);
};
}
sub
test_get_token_croaks_without_token_url {
subtest
"get_token() croaks without token_url"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
throws_ok {
$client
->get_token() }
qr/OIDC: token url not found in provider metadata/
,
'missing token url'
;
};
}
sub
test_get_token_authorization_code {
$test
->mock_user_agent(
to_mock
=> {
post
=> {
access_token
=>
'my_access_token'
},
}
);
$test
->mock_token_response_parser();
subtest
"get_token() authorization_code grant type"
=>
sub
{
$log
->clear();
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$token_response
=
$client
->get_token(
code
=>
'my_code'
,
redirect_uri
=>
'my_redirect_uri'
);
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'authorization_code'
,
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
code
=>
'my_code'
,
redirect_uri
=>
'my_redirect_uri'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
cmp_deeply(
$log
->msgs,
[
superhashof({
message
=>
'OIDC: calling provider to get token'
,
level
=>
'debug'
,
}),
],
'expected log'
);
};
subtest
"get_token() authorization_code grant type from config + basic"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
token_endpoint_grant_type
=>
'authorization_code'
,
token_endpoint_auth_method
=>
'basic'
,
signin_redirect_uri
=>
'my_signin_redirect_uri'
,
audience
=>
'my_audience'
,
},
);
my
$token_response
=
$client
->get_token(
code
=>
'my_code'
,
);
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'authorization_code'
,
code
=>
'my_code'
,
redirect_uri
=>
'my_signin_redirect_uri'
,
audience
=>
'my_audience'
,
);
my
%expected_headers
= (
Authorization
=>
'Basic bXlfY2xpZW50X2lkOm15X2NsaWVudF9zZWNyZXQ='
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
}
sub
test_get_token_client_credentials {
$test
->mock_user_agent(
to_mock
=> {
post
=> {
access_token
=>
'my_access_token'
},
}
);
$test
->mock_token_response_parser();
subtest
"get_token() client_credentials grant type"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$token_response
=
$client
->get_token(
grant_type
=>
'client_credentials'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
);
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'client_credentials'
,
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
subtest
"get_token() client_credentials grant type from config + basic"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
token_endpoint_grant_type
=>
'client_credentials'
,
token_endpoint_auth_method
=>
'basic'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
},
);
my
$token_response
=
$client
->get_token();
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'client_credentials'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
);
my
%expected_headers
= (
Authorization
=>
'Basic bXlfY2xpZW50X2lkOm15X2NsaWVudF9zZWNyZXQ='
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
}
sub
test_get_token_password {
$test
->mock_user_agent(
to_mock
=> {
post
=> {
access_token
=>
'my_access_token'
},
}
);
$test
->mock_token_response_parser();
subtest
"get_token() password grant type"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$token_response
=
$client
->get_token(
grant_type
=>
'password'
,
username
=>
'my_username'
,
password
=>
'my_password'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
);
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'password'
,
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
username
=>
'my_username'
,
password
=>
'my_password'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
subtest
"get_token() password grant type from config + basic"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
token_endpoint_grant_type
=>
'password'
,
token_endpoint_auth_method
=>
'basic'
,
username
=>
'my_username'
,
password
=>
'my_password'
,
scope
=>
'my_scope1 my_scope2'
,
audience
=>
'my_audience'
,
},
);
my
$token_response
=
$client
->get_token();
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'password'
,
username
=>
'my_username'
,
password
=>
'my_password'
,
scope
=>
'my_scope1 my_scope2'
,
audience
=>
'my_audience'
,
);
my
%expected_headers
= (
Authorization
=>
'Basic bXlfY2xpZW50X2lkOm15X2NsaWVudF9zZWNyZXQ='
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
}
sub
test_get_token_refresh_token {
$test
->mock_user_agent(
to_mock
=> {
post
=> {
access_token
=>
'my_access_token'
},
}
);
$test
->mock_token_response_parser();
subtest
"get_token() password grant type"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$token_response
=
$client
->get_token(
grant_type
=>
'refresh_token'
,
refresh_token
=>
'my_refresh_token'
,
);
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'refresh_token'
,
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
refresh_token
=>
'my_refresh_token'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
subtest
"get_token() password grant type with basic auth"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
token_endpoint_grant_type
=>
'client_credentials'
,
token_endpoint_auth_method
=>
'basic'
,
scope
=>
'my_scope'
,
audience
=>
'my_audience'
,
},
);
my
$token_response
=
$client
->get_token(
grant_type
=>
'refresh_token'
,
refresh_token
=>
'my_refresh_token'
,
);
is(
$token_response
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
grant_type
=>
'refresh_token'
,
refresh_token
=>
'my_refresh_token'
,
);
my
%expected_headers
= (
Authorization
=>
'Basic bXlfY2xpZW50X2lkOm15X2NsaWVudF9zZWNyZXQ='
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
}
sub
test_verify_token {
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
provider_metadata
=> {
issuer
=>
'my_issuer'
},
);
subtest
"verify_token() missing 'aud' claim"
=>
sub
{
$test
->mock_decode_jwt(
claims
=> {
iss
=>
'my_issuer'
,
}
);
throws_ok {
$client
->verify_token(
token
=>
'my_token'
,
);
}
qr/OIDC: the audience is not defined/
,
'missing claim'
;
};
subtest
"verify_token() 'aud' is the default client id"
=>
sub
{
my
%claims
= (
iss
=>
'my_issuer'
,
aud
=>
'my_client_id'
,
);
$test
->mock_decode_jwt(
claims
=> \
%claims
);
my
$token_claims
=
$client
->verify_token(
token
=>
'my_token'
,
);
cmp_deeply(
$token_claims
, \
%claims
,
'expected claims'
);
};
subtest
"verify_token() 'aud' is different from the default client id"
=>
sub
{
$test
->mock_decode_jwt(
claims
=> {
iss
=>
'my_issuer'
,
aud
=>
'other_client_id'
,
}
);
throws_ok {
$client
->verify_token(
token
=>
'my_token'
,
);
}
qr/OIDC: unexpected audience, expected 'my_client_id' but got 'other_client_id'/
,
'exception is thrown'
;
};
subtest
"verify_token() 'aud' is the expected audience"
=>
sub
{
my
%claims
= (
iss
=>
'my_issuer'
,
aud
=>
'my_audience'
,
);
$test
->mock_decode_jwt(
claims
=> \
%claims
);
my
$token_claims
=
$client
->verify_token(
token
=>
'my_token'
,
expected_audience
=>
'my_audience'
,
);
cmp_deeply(
$token_claims
, \
%claims
,
'expected claims'
);
};
subtest
"verify_token() 'aud' is different from the expected client id"
=>
sub
{
$test
->mock_decode_jwt(
claims
=> {
iss
=>
'my_issuer'
,
aud
=>
'other_audience'
,
}
);
throws_ok {
$client
->verify_token(
token
=>
'my_token'
,
expected_audience
=>
'my_audience'
,
);
}
qr/OIDC: unexpected audience, expected 'my_audience' but got 'other_audience'/
,
'exception is thrown'
;
};
subtest
"verify_token() 'sub' is the expected subject"
=>
sub
{
my
%claims
= (
iss
=>
'my_issuer'
,
aud
=>
'my_client_id'
,
sub
=>
'my_subject'
,
);
$test
->mock_decode_jwt(
claims
=> \
%claims
);
my
$token_claims
=
$client
->verify_token(
token
=>
'my_token'
,
expected_subject
=>
'my_subject'
,
);
cmp_deeply(
$token_claims
, \
%claims
,
'expected claims'
);
};
subtest
"verify_token() 'sub' is different from the expected subject"
=>
sub
{
$test
->mock_decode_jwt(
claims
=> {
iss
=>
'my_issuer'
,
aud
=>
'my_client_id'
,
sub
=>
'other_subject'
,
}
);
throws_ok {
$client
->verify_token(
token
=>
'my_token'
,
expected_subject
=>
'my_subject'
,
);
}
qr/OIDC: unexpected subject, expected 'my_subject' but got 'other_subject'/
,
'exception is thrown'
;
};
}
sub
test_verify_token_with_standard_decode_exception {
subtest
"verify_token() with a standard decode exception"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
provider_metadata
=> {
issuer
=>
'my_issuer'
,
},
);
$test
->mock_decode_jwt(
callback
=>
sub
{ croak(
'whatever'
) });
throws_ok {
$client
->verify_token(
token
=>
'my_token'
);
}
qr/whatever/
,
'expected exception'
;
isa_ok($@,
'OIDC::Client::Error::TokenValidation'
);
};
}
sub
test_verify_token_with_kid_keys_exception {
subtest
"verify_token() with 'kid_keys' exception"
=>
sub
{
$test
->mock_user_agent(
to_mock
=> {
get
=> {
keys
=> [
'a'
] },
}
);
$test
->mock_response_parser();
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
user_agent
=>
$test
->mocked_user_agent,
response_parser
=>
$test
->mocked_response_parser,
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
provider_metadata
=> {
issuer
=>
'my_issuer'
,
jwks_url
=>
'my_jwks_url'
,
},
);
$test
->mock_decode_jwt(
callback
=>
sub
{ croak(
'JWE: kid_keys lookup failed'
) });
throws_ok {
$client
->verify_token(
token
=>
'my_token'
);
}
qr/JWE: kid_keys lookup failed/
,
'expected exception'
;
isa_ok($@,
'OIDC::Client::Error::TokenValidation'
);
};
}
sub
test_verify_token_renewing_kid_keys {
subtest
"verify_token() renewing the kid_keys"
=>
sub
{
$test
->mock_user_agent(
to_mock
=> {
get
=> {
keys
=> [
'a'
,
'b'
,
'c'
] },
}
);
$test
->mock_response_parser();
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
user_agent
=>
$test
->mocked_user_agent,
response_parser
=>
$test
->mocked_response_parser,
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
provider_metadata
=> {
issuer
=>
'my_issuer'
,
jwks_url
=>
'my_jwks_url'
,
},
);
my
%claims
= (
iss
=>
'my_issuer'
,
aud
=>
'my_client_id'
,
);
$test
->mock_decode_jwt(
callback
=>
sub
{
state
$i
= 1;
croak(
'JWE: kid_keys lookup failed'
)
if
$i
++ == 1;
return
\
%claims
;
}
);
my
$token_claims
=
$client
->verify_token(
token
=>
'my_token'
,
);
cmp_deeply(
$token_claims
, \
%claims
,
'expected claims'
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
[
'get'
, [
$test
->mocked_user_agent,
'my_jwks_url'
] ],
'expected call to renew kid keys'
);
cmp_deeply(
$client
->kid_keys, {
keys
=> [
'a'
,
'b'
,
'c'
] },
'kid keys have been updated'
);
};
}
sub
test_has_expired_without_configured_leeway {
subtest
"has_expired() without configured leeway"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
{
my
$has_expired
=
$client
->has_expired(
time
- 1);
ok(
$has_expired
,
'has expired'
);
}
{
my
$has_expired
=
$client
->has_expired(
time
+ 2);
ok(!
$has_expired
,
'has not expired'
);
}
};
}
sub
test_has_expired_with_configured_leeway {
subtest
"has_expired() with configured leeway"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
expiration_leeway
=> 20,
},
);
{
my
$has_expired
=
$client
->has_expired(
time
- 1);
ok(
$has_expired
,
'has expired'
);
}
{
my
$has_expired
=
$client
->has_expired(
time
+ 18);
ok(
$has_expired
,
'has expired due to the leeway'
);
}
{
my
$has_expired
=
$client
->has_expired(
time
+ 22);
ok(!
$has_expired
,
'has not expired'
);
}
};
}
sub
test_get_userinfo {
my
%userinfo
= (
lastname
=>
'Doe'
);
$test
->mock_user_agent(
to_mock
=> {
get
=> \
%userinfo
,
}
);
$test
->mock_response_parser();
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
response_parser
=>
$test
->mocked_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
provider_metadata
=> {
userinfo_url
=>
$userinfo_url
},
);
subtest
"get_userinfo() without token type"
=>
sub
{
my
$userinfo
=
$client
->get_userinfo(
access_token
=>
'my_access_token'
,
token_type
=>
undef
,
);
cmp_deeply(
$userinfo
, \
%userinfo
,
'expected userinfo'
);
my
%expected_headers
= (
Authorization
=>
'Bearer my_access_token'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
[
'get'
, [
$test
->mocked_user_agent,
$userinfo_url
, \
%expected_headers
] ],
'expected call to user agent'
);
};
subtest
"get_userinfo() with token type"
=>
sub
{
my
$userinfo
=
$client
->get_userinfo(
access_token
=>
'my_access_token'
,
token_type
=>
'my_token_type'
,
);
cmp_deeply(
$userinfo
, \
%userinfo
,
'expected userinfo'
);
my
%expected_headers
= (
Authorization
=>
'my_token_type my_access_token'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
[
'get'
, [
$test
->mocked_user_agent,
$userinfo_url
, \
%expected_headers
] ],
'expected call to user agent'
);
};
}
sub
test_get_audience_for_alias {
subtest
"get_audience_for_alias()"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
audience_alias
=> {
alias1
=> {
audience
=>
'audience1'
,
},
alias2
=> {
audience
=>
'audience2'
,
},
},
},
);
{
my
$audience
=
$client
->get_audience_for_alias(
'alias2'
);
is(
$audience
,
'audience2'
,
'expected audience'
);
}
{
my
$audience
=
$client
->get_audience_for_alias(
'alias3'
);
is(
$audience
,
undef
,
'audience not found'
);
}
};
}
sub
test_get_scope_for_audience {
subtest
"get_scope_for_audience()"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
audience_alias
=> {
alias1
=> {
audience
=>
'audience1'
,
},
alias2
=> {
audience
=>
'audience2'
,
scope
=>
'scope_audience2'
,
},
},
},
);
{
my
$scope
=
$client
->get_scope_for_audience(
'audience1'
);
is(
$scope
,
undef
,
'no scope'
);
}
{
my
$scope
=
$client
->get_scope_for_audience(
'audience2'
);
is(
$scope
,
'scope_audience2'
,
'expected scope'
);
}
};
}
sub
test_exchange_token {
$test
->mock_user_agent(
to_mock
=> {
post
=> {
access_token
=>
'my_access_token'
},
}
);
$test
->mock_token_response_parser();
subtest
"exchange_token() without scope"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$exchanged_token
=
$client
->exchange_token(
token
=>
'my_token'
,
audience
=>
'my_audience'
,
);
is(
$exchanged_token
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
audience
=>
'my_audience'
,
grant_type
=>
'urn:ietf:params:oauth:grant-type:token-exchange'
,
subject_token
=>
'my_token'
,
subject_token_type
=>
'urn:ietf:params:oauth:token-type:access_token'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
subtest
"exchange_token() with scope in parameters"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$exchanged_token
=
$client
->exchange_token(
token
=>
'my_token'
,
audience
=>
'my_audience'
,
scope
=>
'my_scope1 my_scope2'
,
);
is(
$exchanged_token
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
audience
=>
'my_audience'
,
scope
=>
'my_scope1 my_scope2'
,
grant_type
=>
'urn:ietf:params:oauth:grant-type:token-exchange'
,
subject_token
=>
'my_token'
,
subject_token_type
=>
'urn:ietf:params:oauth:token-type:access_token'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
subtest
"exchange_token() with scope in config"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
audience_alias
=> {
my_alias
=> {
audience
=>
'my_audience'
,
scope
=>
'my_scope1 my_scope2'
,
},
},
},
);
my
$exchanged_token
=
$client
->exchange_token(
token
=>
'my_token'
,
audience
=>
'my_audience'
,
);
is(
$exchanged_token
->access_token,
'my_access_token'
,
'expected access token'
);
my
%expected_args
= (
client_id
=>
'my_client_id'
,
client_secret
=>
'my_client_secret'
,
audience
=>
'my_audience'
,
scope
=>
'my_scope1 my_scope2'
,
grant_type
=>
'urn:ietf:params:oauth:grant-type:token-exchange'
,
subject_token
=>
'my_token'
,
subject_token_type
=>
'urn:ietf:params:oauth:token-type:access_token'
,
);
cmp_deeply([
$test
->mocked_user_agent->next_call() ],
'expected call to user agent'
);
};
}
sub
test_build_api_useragent {
subtest
"build_api_useragent() with token parameter"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
my
$token_type
=
'my_token_type'
;
my
$token
=
'my_token'
;
my
$ua
=
$client
->build_api_useragent(
token_type
=>
$token_type
,
token
=>
$token
,
);
isa_ok(
$ua
,
'Mojo::UserAgent'
);
my
$tx
=
$ua
->build_tx(
GET
=>
'localhost'
);
$tx
=
$ua
->start(
$tx
);
is(
$tx
->req->headers->authorization,
'my_token_type my_token'
);
};
subtest
"build_api_useragent() without token parameter"
=>
sub
{
$test
->mock_user_agent(
to_mock
=> {
post
=> {
access_token
=>
'my_access_token'
},
}
);
$test
->mock_token_response_parser();
my
$client
=
$class
->new(
log
=>
$log
,
user_agent
=>
$test
->mocked_user_agent,
token_response_parser
=>
$test
->mocked_token_response_parser,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
audience
=>
'my_audience'
,
scope
=>
'roles'
,
token_endpoint_grant_type
=>
'password'
,
username
=>
'TSTUSER'
,
password
=>
'XXXXXXX'
,
},
);
my
$ua
=
$client
->build_api_useragent();
isa_ok(
$ua
,
'Mojo::UserAgent'
);
my
$tx
=
$ua
->build_tx(
GET
=>
'localhost'
);
$tx
=
$ua
->start(
$tx
);
is(
$tx
->req->headers->authorization,
'Bearer my_access_token'
);
};
}
sub
test_logout_url_croaks_without_end_session_url {
subtest
"logout_url() croaks without end_session_url"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
throws_ok {
$client
->logout_url() }
qr/OIDC: end_session_url not found in provider metadata/
,
'exception'
;
};
}
sub
test_logout_url_returning_string {
subtest
"logout_url() returning string"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
},
);
{
my
$logout_url
=
$client
->logout_url();
ok(!
ref
$logout_url
,
'is a scalar'
);
my
$mojo_logout_url
= Mojo::URL->new(
$logout_url
);
is(
$mojo_logout_url
->protocol,
'https'
,
'expected scheme'
);
is(
$mojo_logout_url
->host,
'my-provider'
,
'expected host'
);
is(
$mojo_logout_url
->path,
'/logout'
,
'expected path'
);
my
%expected_query_params
= (
client_id
=>
'my_client_id'
,
);
cmp_deeply(
$mojo_logout_url
->query->to_hash, \
%expected_query_params
,
'expected query params with minimum arguments'
);
}
{
my
$logout_url
=
$client
->logout_url(
id_token
=>
'my_id_token'
,
state
=>
'my_state'
,
post_logout_redirect_uri
=>
'my_post_logout_redirect_uri'
,
extra_params
=> {
other_param
=>
'my_other_param'
},
);
my
$mojo_logout_url
= Mojo::URL->new(
$logout_url
);
my
%expected_query_params
= (
client_id
=>
'my_client_id'
,
id_token_hint
=>
'my_id_token'
,
state
=>
'my_state'
,
post_logout_redirect_uri
=>
'my_post_logout_redirect_uri'
,
other_param
=>
'my_other_param'
,
);
cmp_deeply(
$mojo_logout_url
->query->to_hash, \
%expected_query_params
,
'expected query params with maximum arguments'
);
}
};
}
sub
test_logout_url_returning_mojo_url {
subtest
"logout_url() returning a mojo url"
=>
sub
{
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
post_logout_redirect_uri
=>
'my_post_logout_redirect_uri'
,
logout_extra_params
=> {
other_param
=>
'my_other_param'
},
},
);
my
$logout_url
=
$client
->logout_url(
id_token
=>
'my_id_token'
,
want_mojo_url
=> 1
);
isa_ok(
$logout_url
,
'Mojo::URL'
);
is(
$logout_url
->protocol,
'https'
,
'expected scheme'
);
is(
$logout_url
->host,
'my-provider'
,
'expected host'
);
is(
$logout_url
->path,
'/logout'
,
'expected path'
);
my
%expected_query_params
= (
client_id
=>
'my_client_id'
,
id_token_hint
=>
'my_id_token'
,
post_logout_redirect_uri
=>
'my_post_logout_redirect_uri'
,
other_param
=>
'my_other_param'
,
);
cmp_deeply(
$logout_url
->query->to_hash, \
%expected_query_params
,
'expected query params with config parameters'
);
};
}
sub
test_get_claim_value {
subtest
"get_claim_value()"
=>
sub
{
my
%claims
= (
'sub'
=>
'my_subject'
,
);
my
%claim_key
= (
login
=>
'sub'
,
last_name
=>
'lastName'
,
);
my
$client
=
$class
->new(
log
=>
$log
,
kid_keys
=> {},
config
=> {
provider
=>
'my_provider'
,
id
=>
'my_client_id'
,
secret
=>
'my_client_secret'
,
claim_mapping
=> \
%claim_key
,
},
);
{
my
$claim_value
=
$client
->get_claim_value(
name
=>
'login'
,
claims
=> \
%claims
,
);
is(
$claim_value
,
'my_subject'
,
'expected claim value'
);
}
{
my
$claim_value
=
$client
->get_claim_value(
name
=>
'last_name'
,
claims
=> \
%claims
,
optional
=> 1,
);
is(
$claim_value
,
undef
,
'not present in claims and optional'
);
}
{
throws_ok {
$client
->get_claim_value(
name
=>
'last_name'
,
claims
=> \
%claims
,
);
}
qr/OIDC: the 'lastName' claim is not present/
,
'not present in claims and required'
;
}
{
throws_ok {
$client
->get_claim_value(
name
=>
'first_name'
,
claims
=> \
%claims
,
optional
=> 1,
);
}
qr/OIDC: no claim key in config for name 'first_name'/
,
'claim key not present in config'
;
}
};
}