Mojo-ATProto-OAuth

 view release on metacpan or  search on metacpan

t/resolver.t  view on Meta::CPAN

use Test2::V0;

use Mojo::ATProto::OAuth::Resolver qw//;
use Mojo::JSON qw/true false/;

# Validation rules mirror indigo's AuthServerMetadata.Validate()
# (atproto/auth/oauth/types.go), read directly from source - each
# subtest below corresponds to one of its checks.

my $resolver   = Mojo::ATProto::OAuth::Resolver->new;
my $server_url = 'https://pds.example.com';

sub valid_metadata {
    return {
        issuer                                              => 'https://pds.example.com',
        authorization_endpoint                              => 'https://pds.example.com/oauth/authorize',
        token_endpoint                                       => 'https://pds.example.com/oauth/token',
        response_types_supported                            => ['code'],
        grant_types_supported                               => ['authorization_code', 'refresh_token'],
        code_challenge_methods_supported                    => ['S256'],
        token_endpoint_auth_methods_supported               => ['none', 'private_key_jwt'],
        token_endpoint_auth_signing_alg_values_supported    => ['ES256'],
        scopes_supported                                    => ['atproto', 'transition:email'],
        authorization_response_iss_parameter_supported      => true,
        require_pushed_authorization_requests               => true,
        pushed_authorization_request_endpoint               => 'https://pds.example.com/oauth/par',
        dpop_signing_alg_values_supported                   => ['ES256'],
        client_id_metadata_document_supported               => true,
    };
}

subtest 'a fully valid document passes' => sub {
    ok(lives { $resolver->_validate_auth_server_metadata(valid_metadata(), $server_url) }, 'no exception') or note($@);
};

subtest 'empty issuer is rejected' => sub {
    my $meta = valid_metadata();
    $meta->{issuer} = '';
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/empty issuer/, 'rejected');
};

subtest 'issuer must match the server URL fetched from' => sub {
    my $meta = valid_metadata();
    $meta->{issuer} = 'https://someone-elses-server.example.com';
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/issuer must match request URL/, 'rejected');
};

subtest 'issuer with a path is rejected' => sub {
    my $meta = valid_metadata();
    $meta->{issuer} = 'https://pds.example.com/some/path';
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/issuer URL/, 'rejected');
};

subtest 'non-https authorization_endpoint is rejected' => sub {
    my $meta = valid_metadata();
    $meta->{authorization_endpoint} = 'http://pds.example.com/oauth/authorize';
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/authorization_endpoint/, 'rejected');
};

subtest 'response_types_supported must include code' => sub {
    my $meta = valid_metadata();
    $meta->{response_types_supported} = ['token'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/response_types_supported/, 'rejected');
};

subtest 'grant_types_supported must include authorization_code' => sub {
    my $meta = valid_metadata();
    $meta->{grant_types_supported} = ['refresh_token'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/authorization_code/, 'rejected');
};

subtest 'grant_types_supported must include refresh_token' => sub {
    my $meta = valid_metadata();
    $meta->{grant_types_supported} = ['authorization_code'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/refresh_token/, 'rejected');
};

subtest 'code_challenge_methods_supported must include S256' => sub {
    my $meta = valid_metadata();
    $meta->{code_challenge_methods_supported} = ['plain'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/S256/, 'rejected');
};

subtest 'token_endpoint_auth_methods_supported must include none' => sub {
    my $meta = valid_metadata();
    $meta->{token_endpoint_auth_methods_supported} = ['private_key_jwt'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/must include 'none'/, 'rejected');
};

subtest 'token_endpoint_auth_methods_supported must include private_key_jwt' => sub {
    my $meta = valid_metadata();
    $meta->{token_endpoint_auth_methods_supported} = ['none'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/private_key_jwt/, 'rejected');
};

subtest 'token_endpoint_auth_signing_alg_values_supported must include ES256' => sub {
    my $meta = valid_metadata();
    $meta->{token_endpoint_auth_signing_alg_values_supported} = ['RS256'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/token_endpoint_auth_signing_alg_values_supported/, 'rejected');
};

subtest 'scopes_supported must include atproto' => sub {
    my $meta = valid_metadata();
    $meta->{scopes_supported} = ['transition:email'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/scopes_supported/, 'rejected');
};

subtest 'authorization_response_iss_parameter_supported must be true' => sub {
    my $meta = valid_metadata();
    $meta->{authorization_response_iss_parameter_supported} = false;
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/authorization_response_iss_parameter_supported/, 'rejected');
};

subtest 'require_pushed_authorization_requests must be true' => sub {
    my $meta = valid_metadata();
    $meta->{require_pushed_authorization_requests} = false;
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/require_pushed_authorization_requests/, 'rejected');
};

subtest 'pushed_authorization_request_endpoint is required' => sub {
    my $meta = valid_metadata();
    delete $meta->{pushed_authorization_request_endpoint};
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/pushed_authorization_request_endpoint/, 'rejected');
};

subtest 'dpop_signing_alg_values_supported must include ES256' => sub {
    my $meta = valid_metadata();
    $meta->{dpop_signing_alg_values_supported} = ['RS256'];
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/dpop_signing_alg_values_supported/, 'rejected');
};

subtest 'require_request_uri_registration=false is rejected, but absent is fine' => sub {
    my $meta = valid_metadata();
    $meta->{require_request_uri_registration} = false;
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/require_request_uri_registration/, 'false is rejected');

    my $meta2 = valid_metadata();
    $meta2->{require_request_uri_registration} = true;
    ok(lives { $resolver->_validate_auth_server_metadata($meta2, $server_url) }, 'true is fine') or note($@);
};

subtest 'client_id_metadata_document_supported must be true' => sub {
    my $meta = valid_metadata();
    $meta->{client_id_metadata_document_supported} = false;
    like(dies { $resolver->_validate_auth_server_metadata($meta, $server_url) }, qr/client_id_metadata_document_supported/, 'rejected');
};

subtest 'resolve_auth_server_url and resolve_auth_server_metadata reject non-public host URLs up front' => sub {
    like(dies { $resolver->resolve_auth_server_url('http://pds.example.com') }, qr/not a valid public host URL/, 'http rejected');
    like(dies { $resolver->resolve_auth_server_url('https://pds.example.com:8080') }, qr/not a valid public host URL/, 'explicit port rejected');
    like(dies { $resolver->resolve_auth_server_metadata('http://pds.example.com') }, qr/not a valid public host URL/, 'http rejected');
};

done_testing;



( run in 0.357 second using v1.01-cache-2.11-cpan-ad19def0cd9 )