perlsky is a Perl 5 implementation of an AT Protocol Personal Data Server.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Strengthen auth and preference regression coverage

alice 01bdc71b d0952a7c

+182
+110
t/auth-jwt.t
··· 3 3 4 4 use Config (); 5 5 use File::Spec; 6 + use File::Temp qw(tempdir); 6 7 use FindBin qw($Bin); 8 + use Mojo::Headers; 9 + use Mojo::Message::Request; 7 10 use Test2::V0; 8 11 use JSON::PP qw(decode_json); 9 12 use MIME::Base64 qw(decode_base64); ··· 19 22 } 20 23 21 24 use Crypt::PK::ECC; 25 + use ATProto::PDS::API::Server qw(require_auth); 22 26 use ATProto::PDS::Auth::JWT qw(decode_jwt encode_jwt encode_service_jwt); 23 27 use ATProto::PDS::Crypto::Secp256k1 qw(generate_keypair); 28 + use ATProto::PDS::Store::SQLite; 29 + use ATProto::PDS::Constants qw(TOKEN_AUD_ACCESS); 24 30 25 31 my $token = encode_jwt( 26 32 { ··· 77 83 'service token signature verifies', 78 84 ); 79 85 86 + { 87 + my $tmp = tempdir(CLEANUP => 1); 88 + my $store = ATProto::PDS::Store::SQLite->new( 89 + path => File::Spec->catfile($tmp, 'perlsky.sqlite'), 90 + )->bootstrap; 91 + 92 + my $account = $store->create_account( 93 + id => 'acct-auth-jwt', 94 + did => 'did:web:example.test:users:alice', 95 + handle => 'alice.example.test', 96 + email => 'alice@example.test', 97 + password_hash => 'sha256:abc', 98 + did_doc => { id => 'did:web:example.test:users:alice' }, 99 + ); 100 + 101 + my $app_session = $store->create_session( 102 + id => 'sess-app', 103 + did => $account->{did}, 104 + kind => 'app_password', 105 + scope => 'app_password', 106 + expires_at => 1_900_000_000, 107 + ); 108 + my $full_session = $store->create_session( 109 + id => 'sess-full', 110 + did => $account->{did}, 111 + kind => 'account', 112 + scope => TOKEN_AUD_ACCESS, 113 + expires_at => 1_900_000_000, 114 + ); 115 + 116 + my $secret = 'auth-jwt-secret'; 117 + my $app_token = encode_jwt({ 118 + iss => 'did:web:example.test', 119 + sub => $account->{did}, 120 + aud => TOKEN_AUD_ACCESS, 121 + scope => 'app_password', 122 + typ => TOKEN_AUD_ACCESS, 123 + jti => $app_session->{id}, 124 + exp => 1_900_000_000, 125 + }, $secret); 126 + my $full_token = encode_jwt({ 127 + iss => 'did:web:example.test', 128 + sub => $account->{did}, 129 + aud => TOKEN_AUD_ACCESS, 130 + scope => TOKEN_AUD_ACCESS, 131 + typ => TOKEN_AUD_ACCESS, 132 + jti => $full_session->{id}, 133 + exp => 1_900_000_000, 134 + }, $secret); 135 + 136 + my $app_error = dies { 137 + require_auth( 138 + _mock_controller($store, $app_token, $secret), 139 + audience => TOKEN_AUD_ACCESS, 140 + required_scope => 'full', 141 + ); 142 + }; 143 + is( 144 + $app_error, 145 + { 146 + status => 400, 147 + error => 'InvalidToken', 148 + message => 'Bad token scope', 149 + }, 150 + 'full-scope auth gates reject app-password sessions', 151 + ); 152 + 153 + my ($claims, $resolved_account, $resolved_session) = require_auth( 154 + _mock_controller($store, $full_token, $secret), 155 + audience => TOKEN_AUD_ACCESS, 156 + required_scope => 'full', 157 + ); 158 + is($claims->{jti}, $full_session->{id}, 'full-scope auth accepts a normal account session token'); 159 + is($resolved_account->{did}, $account->{did}, 'full-scope auth returns the account'); 160 + is($resolved_session->{id}, $full_session->{id}, 'full-scope auth returns the matched session'); 161 + } 162 + 80 163 done_testing; 81 164 82 165 sub _b64url_decode { ··· 87 170 $b64 .= '=' x (4 - $pad) if $pad; 88 171 return decode_base64($b64); 89 172 } 173 + 174 + sub _mock_controller { 175 + my ($store, $token, $secret) = @_; 176 + return bless { 177 + store => $store, 178 + req => _mock_request($token), 179 + config => { 180 + jwt_secret => $secret, 181 + }, 182 + }, 'Local::AuthJWT::Controller'; 183 + } 184 + 185 + sub _mock_request { 186 + my ($token) = @_; 187 + my $req = Mojo::Message::Request->new; 188 + $req->headers->authorization("Bearer $token"); 189 + return $req; 190 + } 191 + 192 + package Local::AuthJWT::Controller; 193 + 194 + sub req { shift->{req} } 195 + sub store { shift->{store} } 196 + sub config_value { 197 + my ($self, $key, $default) = @_; 198 + return exists $self->{config}{$key} ? $self->{config}{$key} : $default; 199 + }
+15
t/oauth-include.t
··· 69 69 resource => 'identity', 70 70 attr => 'handle', 71 71 }, 72 + { 73 + type => 'permission', 74 + resource => 'account', 75 + attr => 'email', 76 + action => 'manage', 77 + }, 72 78 ], 73 79 } if $nsid eq 'app.bsky.authManageNotifications'; 74 80 return undef; ··· 135 141 attr => 'handle', 136 142 ), 137 143 'compiled scope ignores identity permissions from permission sets', 144 + ); 145 + ok( 146 + !oauth_scope_allows_permission( 147 + $compiled, 148 + type => 'account', 149 + attr => 'email', 150 + action => 'manage', 151 + ), 152 + 'compiled scope ignores account permissions from permission sets', 138 153 ); 139 154 } 140 155
+30
t/plc-identity.t
··· 130 130 my $did = $created->{did}; 131 131 my $access = $created->{accessJwt}; 132 132 133 + $t->post_ok('/xrpc/com.atproto.server.createAppPassword' => { 134 + Authorization => "Bearer $access", 135 + } => json => { 136 + name => 'plc-helper', 137 + })->status_is(200) 138 + ->json_like('/password' => qr/\w/); 139 + 140 + my $app_password = $t->tx->res->json->{password}; 141 + 142 + $t->post_ok('/xrpc/com.atproto.server.createSession' => json => { 143 + identifier => 'alice.test', 144 + password => $app_password, 145 + })->status_is(200); 146 + 147 + my $app_password_access = $t->tx->res->json->{accessJwt}; 148 + 133 149 like($did, qr/\Adid:plc:/, 'createAccount returns a did:plc identifier'); 134 150 is($created->{didDoc}{id}, $did, 'didDoc matches the created did'); 135 151 is($created->{didDoc}{alsoKnownAs}[0], 'at://alice.test', 'didDoc carries the handle'); ··· 155 171 handle => 'alice-renamed.test', 156 172 })->status_is(200); 157 173 174 + $t->post_ok('/xrpc/com.atproto.identity.requestPlcOperationSignature' => { 175 + Authorization => "Bearer $app_password_access", 176 + })->status_is(400) 177 + ->json_is('/error', 'InvalidToken') 178 + ->json_is('/message', 'Bad token scope'); 179 + 158 180 $t->get_ok('/xrpc/com.atproto.identity.resolveHandle' => form => { 159 181 handle => 'alice-renamed.test', 160 182 })->status_is(200) ··· 174 196 purpose => 'plc_operation', 175 197 ); 176 198 ok($token && $token->{token}, 'requestPlcOperationSignature issues a PLC email token'); 199 + 200 + $t->post_ok('/xrpc/com.atproto.identity.signPlcOperation' => { 201 + Authorization => "Bearer $app_password_access", 202 + } => json => { 203 + token => 'plc-token', 204 + })->status_is(400) 205 + ->json_is('/error', 'InvalidToken') 206 + ->json_is('/message', 'Bad token scope'); 177 207 178 208 $t->post_ok('/xrpc/com.atproto.identity.signPlcOperation' => { 179 209 Authorization => "Bearer $access",
+27
t/service-proxy.t
··· 334 334 })->status_is(400) 335 335 ->json_is('/error' => 'InvalidRequest'); 336 336 337 + $t->post_ok('/xrpc/app.bsky.notification.putPreferencesV2' => { 338 + Authorization => "Bearer $access", 339 + } => json => { 340 + like => { 341 + unknown => JSON::PP::true, 342 + }, 343 + })->status_is(400) 344 + ->json_is('/error' => 'InvalidRequest'); 345 + 346 + $t->post_ok('/xrpc/app.bsky.notification.putPreferencesV2' => { 347 + Authorization => "Bearer $access", 348 + } => json => { 349 + like => { 350 + include => ['follows'], 351 + }, 352 + })->status_is(400) 353 + ->json_is('/error' => 'InvalidRequest'); 354 + 355 + $t->post_ok('/xrpc/app.bsky.notification.putPreferencesV2' => { 356 + Authorization => "Bearer $access", 357 + } => json => { 358 + verified => { 359 + push => 'false', 360 + }, 361 + })->status_is(400) 362 + ->json_is('/error' => 'InvalidRequest'); 363 + 337 364 $t->get_ok('/xrpc/app.bsky.notification.getPreferences' => { 338 365 Authorization => "Bearer $access", 339 366 })->status_is(200)