Skip to content

Commit

Permalink
Add tests for MFA
Browse files Browse the repository at this point in the history
  • Loading branch information
thekid committed Jun 17, 2022
1 parent 4ef0245 commit f1a9ee5
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 12 deletions.
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
81 changes: 75 additions & 6 deletions src/test/php/de/thekid/cas/unittest/LoginTest.php
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
<?php namespace de\thekid\cas\unittest;

use de\thekid\cas\Signed;
use de\thekid\cas\flow\{DisplaySuccess, EnterCredentials, Flow, RedirectToService, UseService};
use com\google\authenticator\{TimeBased, SecretBytes};
use de\thekid\cas\flow\{DisplaySuccess, EnterCredentials, Flow, RedirectToService, UseService, QueryMFACode};
use de\thekid\cas\impl\Login;
use de\thekid\cas\services\Services;
use de\thekid\cas\tickets\Tickets;
use de\thekid\cas\users\{NoSuchUser, PasswordMismatch};
use de\thekid\cas\{Signed, Encryption};
use unittest\{Assert, Test};
use util\Random;

class LoginTest extends HandlerTest {
public const SERVICE = 'https://example.org/';
const SERVICE = 'https://example.org/';
const TOTP_SECRET = 'U7YOJLCYMSOQDGI6';

private $persistence, $templates, $signed, $flow;
private $encryption, $persistence, $templates, $signed, $flow;

#[Before]
public function initialize() {
$this->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(),
]);
Expand Down Expand Up @@ -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,
Expand All @@ -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()]);
Expand Down
22 changes: 16 additions & 6 deletions src/test/php/de/thekid/cas/unittest/TestingUsers.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ?? []];
}
}

Expand All @@ -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], []);
}

Expand All @@ -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]);
}
}

0 comments on commit f1a9ee5

Please sign in to comment.