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.

Align account password edge semantics

alice ca5c2e28 8454b8e4

+53 -5
+4 -3
docs/TEST_AUDIT.md
··· 13 13 The current baseline for saying "the audited suite is green" is: 14 14 15 15 - `prove -lr t` 16 - - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2898` 16 + - latest full green result in the realigned Meridian worktree: `Files=48, Tests=2918` 17 17 - `prove -lv t/server-auth.t` 18 18 - `perl -c script/differential-validate` 19 19 - `PERLSKY_RUN_REFERENCE_DIFF=1 prove -lv t/reference-differential.t` ··· 59 59 - `com.atproto.repo.createRecord` follows the reference runtime by ignoring a stray `swapRecord` field, and direct reference coverage now pins `putRecord` / `deleteRecord` `swapCommit` and `swapRecord` mismatch semantics explicitly. 60 60 - App-password sessions follow the official runtime more closely than the older local assumptions did: access-token scopes use the `com.atproto.appPass` / `com.atproto.appPassPrivileged` names, standard app-password sessions may list app passwords, privileged-only `getServiceAuth` failures report `InvalidRequest`, and revoked refresh tokens on `refreshSession` fail with `400 ExpiredToken`. 61 61 - `com.atproto.server.requestPasswordReset` and `com.atproto.server.deleteAccount` now follow the reference form-token flow, with focused regression coverage for missing-account and bearerless deletion semantics. 62 + - Password-bearing account endpoints need the same bounded-length behavior as the official runtime: `createAccount` rejects passwords longer than 256 characters, `createSession` rejects passwords longer than 512 characters with the reset hint, and `resetPassword` / `deleteAccount` reject overlong password inputs with `Invalid password length.` 62 63 - `com.atproto.server.createAccount` with an explicit `did` must behave like an authenticated migration flow: require auth from that same DID, keep the existing DID document, and start the new account deactivated until activation catches the DID document up to the new PDS. 63 64 - `com.atproto.server.checkAccountStatus` must validate the stored DID document against the PDS service endpoint and signing key, and `com.atproto.repo.describeRepo` must derive `didDoc` / `handleIsCorrect` from that document instead of hardcoding success. 64 65 - `com.atproto.sync.getBlob` should ship the same download-hardening headers as the reference PDS (`X-Content-Type-Options`, `Content-Disposition`, `Content-Security-Policy`). ··· 116 117 | `t/oauth-permissions.t` | audited local regression | granular OAuth permission enforcement across account/email, identity, repo, blob, and rpc scope families | 117 118 | `t/oauth-scopes.t` | audited local regression | OAuth scope parsing, normalization, and token-grant shaping | 118 119 | `t/oauth.t` | audited local regression | OAuth provider metadata, PAR, PKCE, DPoP, and token lifecycle coverage | 119 - | `t/password-reset.t` | audited local regression | password reset token issuance and missing-email rejection semantics | 120 + | `t/password-reset.t` | audited local regression | password reset token issuance, case-insensitive email lookup, and overlong-password rejection semantics | 120 121 | `t/pds_smoke.t` | local correctness/infrastructure | broad local PDS smoke; still intentionally optimistic and should only carry a small number of negative assertions | 121 122 | `t/plc-identity.t` | direct reference differential | PLC mock driven by official library semantics | 122 123 | `t/reference-differential-plc.t` | direct reference differential | official runtime comparison in PLC mode | ··· 125 126 | `t/repo-api.t` | audited local regression | record mutation and read semantics, but still lighter than ideal on some negative/reference edge cases | 126 127 | `t/repo-firehose-car.t` | audited local regression | repo commit CAR shape and firehose interactions | 127 128 | `t/repo_formats.t` | audited local regression | direct repo wire-format and CAR expectations | 128 - | `t/server-auth.t` | direct reference differential | auth/session/service-auth behavior repeatedly compared to official runtime | 129 + | `t/server-auth.t` | direct reference differential | auth/session/service-auth behavior repeatedly compared to official runtime, including bounded create-session password semantics | 129 130 | `t/service-proxy-local.t` | audited local regression | local appview fallback behavior | 130 131 | `t/service-proxy.t` | audited local regression | upstream proxy behavior plus conservative local appview fallback and preference semantics | 131 132 | `t/sqlite-binary.t` | local correctness/infrastructure | SQLite binary round-trip correctness |
+12 -1
lib/ATProto/PDS/API/Server.pm
··· 40 40 41 41 our @EXPORT_OK = qw(register_server_handlers require_auth require_access_or_service_auth session_view); 42 42 43 + my $OLD_PASSWORD_MAX_LENGTH = 512; 44 + my $NEW_PASSWORD_MAX_LENGTH = 256; 45 + 43 46 my %PROTECTED_SERVICE_AUTH_METHOD = map { lc($_) => 1 } qw( 44 47 com.atproto.identity.requestPlcOperationSignature 45 48 com.atproto.identity.signPlcOperation ··· 72 75 my $password = $body->{password} // q(); 73 76 xrpc_error(400, 'InvalidPassword', 'Passwords must be at least 8 characters long') 74 77 if length($password) < 8; 78 + xrpc_error(400, 'InvalidRequest', "Password too long. Maximum length is $NEW_PASSWORD_MAX_LENGTH characters.") 79 + if length($password) > $NEW_PASSWORD_MAX_LENGTH; 75 80 76 81 my $invite; 77 82 if (defined($body->{inviteCode}) && length($body->{inviteCode})) { ··· 205 210 206 211 $registry->register('com.atproto.server.createSession', sub ($c, $endpoint) { 207 212 my $body = $c->req->json || {}; 213 + xrpc_error(401, 'AuthRequired', 'Password too long. Consider resetting your password.') 214 + if length($body->{password} // q()) > $OLD_PASSWORD_MAX_LENGTH; 208 215 my $account = find_account($c, $body->{identifier} // q()); 209 216 xrpc_error(401, 'AuthRequired', 'Invalid identifier or password') unless $account; 210 217 xrpc_error(401, 'AuthRequired', 'Invalid identifier or password') ··· 442 449 my $body = $c->req->json || {}; 443 450 xrpc_error(400, 'InvalidPassword', 'Passwords must be at least 8 characters long') 444 451 if length($body->{password} // q()) < 8; 452 + xrpc_error(400, 'InvalidRequest', 'Invalid password length.') 453 + if length($body->{password} // q()) > $NEW_PASSWORD_MAX_LENGTH; 445 454 my $token = _require_action_token($c, 446 455 token => $body->{token}, 447 456 purpose => ACTION_TOKEN_PASSWORD_RESET, ··· 596 605 my $account = $c->store->get_account_by_did($did); 597 606 xrpc_error(400, 'InvalidRequest', 'account not found') 598 607 unless $account && !defined($account->{deleted_at}); 599 - xrpc_error(401, 'AuthRequired', 'Invalid identifier or password') 608 + xrpc_error(400, 'InvalidRequest', 'Invalid password length.') 609 + if length($body->{password} // q()) > $OLD_PASSWORD_MAX_LENGTH; 610 + xrpc_error(401, 'AuthRequired', 'Invalid did or password') 600 611 unless verify_account_password($c, $account, $body->{password} // q()); 601 612 my $token = _require_action_token($c, 602 613 token => $body->{token},
+8
t/app.t
··· 82 82 83 83 my $user_did = $fresh->tx->res->json->{did}; 84 84 85 + $fresh->post_ok('/xrpc/com.atproto.server.createAccount' => json => { 86 + handle => 'toolong.localhost', 87 + email => 'toolong@example.test', 88 + password => ('x' x 257), 89 + })->status_is(400) 90 + ->json_is('/error' => 'InvalidRequest') 91 + ->json_is('/message' => 'Password too long. Maximum length is 256 characters.'); 92 + 85 93 $fresh->get_ok("/xrpc/com.atproto.identity.resolveHandle?handle=alice.localhost") 86 94 ->status_is(200) 87 95 ->json_is('/did' => $user_did);
+10 -1
t/delete-account.t
··· 64 64 password => 'wrong-password', 65 65 token => $token->{token}, 66 66 })->status_is(401) 67 - ->json_is('/error' => 'AuthRequired'); 67 + ->json_is('/error' => 'AuthRequired') 68 + ->json_is('/message' => 'Invalid did or password'); 69 + 70 + $t->post_ok('/xrpc/com.atproto.server.deleteAccount' => json => { 71 + did => $did, 72 + password => ('x' x 513), 73 + token => $token->{token}, 74 + })->status_is(400) 75 + ->json_is('/error' => 'InvalidRequest') 76 + ->json_is('/message' => 'Invalid password length.'); 68 77 69 78 $t->post_ok('/xrpc/com.atproto.server.deleteAccount' => json => { 70 79 did => 'did:web:example.test:users:missing',
+12
t/password-reset.t
··· 66 66 })->status_is(200) 67 67 ->json_is({}); 68 68 69 + $t->post_ok('/xrpc/com.atproto.server.requestPasswordReset' => json => { 70 + email => 'ALICE@example.test', 71 + })->status_is(200) 72 + ->json_is({}); 73 + 74 + $t->post_ok('/xrpc/com.atproto.server.resetPassword' => json => { 75 + token => $token->{token}, 76 + password => ('x' x 257), 77 + })->status_is(400) 78 + ->json_is('/error' => 'InvalidRequest') 79 + ->json_is('/message' => 'Invalid password length.'); 80 + 69 81 $t->post_ok('/xrpc/com.atproto.server.createSession' => json => { 70 82 identifier => 'alice.example.test', 71 83 password => 'hunter22',
+7
t/server-auth.t
··· 73 73 ->json_is('/did' => $did) 74 74 ->json_is('/email' => 'alice@example.com'); 75 75 76 + $t->post_ok('/xrpc/com.atproto.server.createSession' => json => { 77 + identifier => 'alice@example.com', 78 + password => ('x' x 513), 79 + })->status_is(401) 80 + ->json_is('/error' => 'AuthRequired') 81 + ->json_is('/message' => 'Password too long. Consider resetting your password.'); 82 + 76 83 $t->get_ok('/xrpc/com.atproto.admin.getInviteCodes' => { 77 84 Authorization => 'Bearer admin-secret', 78 85 })->status_is(403)