diff --git a/ChangeLog.md b/ChangeLog.md index dd0db6c..840f346 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ CAS Server change log ## ?.?.? / ????-??-?? +## 0.9.3 / 2022-06-17 + * Removed user tokens from session - @thekid * Fixed `RemoveToken` admin command - @thekid diff --git a/src/test/php/de/thekid/cas/unittest/LoginTest.php b/src/test/php/de/thekid/cas/unittest/LoginTest.php index bbf97c8..c1efd04 100755 --- a/src/test/php/de/thekid/cas/unittest/LoginTest.php +++ b/src/test/php/de/thekid/cas/unittest/LoginTest.php @@ -1,28 +1,32 @@ persistence= new TestingPersistence(users: new TestingUsers(['root' => 'secret'])); + $this->encryption= new Encryption(random_bytes(32)); + $this->persistence= new TestingPersistence(users: new TestingUsers(['root' => ['password' => 'secret']])); $this->signed= new Signed('secret'); $this->flow= new Flow([ new UseService(new class() implements Services { public fn validate($url) => LoginTest::SERVICE === $url; }), new EnterCredentials($this->persistence), + new QueryMFACode($this->persistence, $this->encryption), new RedirectToService($this->persistence, $this->signed), new DisplaySuccess(), ]); @@ -166,7 +170,7 @@ public function displays_success() { Assert::equals( [ 'token' => $token, - 'flow' => $this->signed->id(3), + 'flow' => $this->signed->id(4), 'user' => [ 'username' => 'root', 'mfa' => false, @@ -177,6 +181,71 @@ public function displays_success() { ); } + #[Test] + public function queries_mfa_code() { + $this->persistence->users()->newToken('root', 'CAS', $this->encryption->encrypt(self::TOTP_SECRET)); + try { + $this->templates= new TestingTemplates(); + $session= $this->session(['token' => $token= uniqid()]); + + $this->handle($session, 'GET', '/login'); + $this->handle($session, 'POST', '/login', [ + 'flow' => $this->templates->rendered()['login']['flow'], + 'token' => $token, + 'username' => 'root', + 'password' => 'secret', + ]); + + Assert::equals( + [ + 'token' => $token, + 'flow' => $this->signed->id(2), + 'service' => null + ], + $this->templates->rendered()['mfa'] + ); + } finally { + $this->persistence->users()->removeToken('root', 'CAS'); + } + } + + #[Test, Values(['current', 'previous', 'next'])] + public function continues_and_displays_success_after_querying_mfa_code($method) { + $this->persistence->users()->newToken('root', 'CAS', $this->encryption->encrypt(self::TOTP_SECRET)); + try { + $this->templates= new TestingTemplates(); + $session= $this->session(['token' => $token= uniqid()]); + + $this->handle($session, 'GET', '/login'); + $this->handle($session, 'POST', '/login', [ + 'flow' => $this->templates->rendered()['login']['flow'], + 'token' => $token, + 'username' => 'root', + 'password' => 'secret', + ]); + $this->handle($session, 'POST', '/login', [ + 'flow' => $this->templates->rendered()['mfa']['flow'], + 'token' => $token, + 'code' => new TimeBased(new SecretBytes(self::TOTP_SECRET))->{$method}(), + ]); + + Assert::equals( + [ + 'token' => $token, + 'flow' => $this->signed->id(4), + 'user' => [ + 'username' => 'root', + 'mfa' => true, + 'attributes' => null, + ], + ], + $this->templates->rendered()['success'] + ); + } finally { + $this->persistence->users()->removeToken('root', 'CAS'); + } + } + #[Test] public function issues_ticket_and_redirect_to_service() { $session= $this->session(['token' => $token= uniqid()]); diff --git a/src/test/php/de/thekid/cas/unittest/TestingUsers.php b/src/test/php/de/thekid/cas/unittest/TestingUsers.php index e5ed5b1..12befda 100755 --- a/src/test/php/de/thekid/cas/unittest/TestingUsers.php +++ b/src/test/php/de/thekid/cas/unittest/TestingUsers.php @@ -8,8 +8,8 @@ class TestingUsers extends Users { private $backing= []; public function __construct(array $users= []) { - foreach ($users as $username => $password) { - $this->backing[$username]= $this->hash($password); + foreach ($users as $username => $details) { + $this->backing[$username]= ['hash' => $this->hash($details['password']), 'tokens' => $details['tokens'] ?? []]; } } @@ -20,13 +20,13 @@ public function all(?string $filter= null): iterable { /** Returns a user by a given username */ public function named(string $username): ?User { - $hash= $this->backing[$username] ?? null; - return $hash ? new User($username, $hash, []) : null; + $user= $this->backing[$username] ?? null; + return $user ? new User($username, $user['hash'], $user['tokens']) : null; } /** Creates a new user with a given username and password. */ public function create(string $username, string|Secret $password): User { - $this->backing[$username]= $this->hash($password); + $this->backing[$username]= ['hash' => $this->hash($password), 'tokens' => []]; return new User($username, $this->backing[$username], []); } @@ -39,6 +39,16 @@ public function remove(string|User $user): void { /** Changes a user's password. */ public function password(string|User $user, string|Secret $password): void { $username= $user instanceof User ? $user->username() : $user; - $this->backing[$username]= $this->hash($password); + $this->backing[$username]['hash']= $this->hash($password); + } + + public function newToken(string|User $user, string $name, string|Secret $secret) { + $username= $user instanceof User ? $user->username() : $user; + $this->backing[$username]['tokens'][$name]= $secret instanceof Secret ? $secret->reveal() : $secret; + } + + public function removeToken(string|User $user, string $name) { + $username= $user instanceof User ? $user->username() : $user; + unset($this->backing[$username]['tokens'][$name]); } } \ No newline at end of file