diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php
index 4b0f59cd..333bf0e6 100644
--- a/src/Controllers/UserController.php
+++ b/src/Controllers/UserController.php
@@ -3,35 +3,36 @@ namespace Szurubooru\Controllers;
 
 final class UserController extends AbstractController
 {
-	private $userService;
-	private $passwordService;
 	private $inputReader;
+	private $userService;
 
 	public function __construct(
 		\Szurubooru\Services\UserService $userService,
-		\Szurubooru\Services\PasswordService $passwordService,
 		\Szurubooru\Helpers\InputReader $inputReader)
 	{
 		$this->inputReader = $inputReader;
 		$this->userService = $userService;
-		$this->passwordService = $passwordService;
 	}
 
 	public function registerRoutes(\Szurubooru\Router $router)
 	{
-		$router->post('/api/users', [$this, 'create']);
+		$router->post('/api/users', [$this, 'register']);
 		$router->get('/api/users', [$this, 'getAll']);
 		$router->get('/api/users/:id', [$this, 'getById']);
 		$router->put('/api/users/:id', [$this, 'update']);
 		$router->delete('/api/users/:id', [$this, 'delete']);
 	}
 
-	public function create()
+	public function register()
 	{
-		$this->userService->validateUserName($this->inputReader->userName);
-		$this->passwordService->validatePassword($this->inputReader->password);
+		$input = new \Szurubooru\FormData\RegistrationFormData;
+		$input->name = $this->inputReader->userName;
+		$input->password = $this->inputReader->password;
+		$input->email = $this->inputReader->email;
 
-		throw new \BadMethodCallException('Not implemented');
+		$user = $this->userService->register($input);
+
+		return new \Szurubooru\ViewProxies\User($user);
 	}
 
 	public function update($id)
diff --git a/src/Entities/User.php b/src/Entities/User.php
index 35174187..dcbbaac9 100644
--- a/src/Entities/User.php
+++ b/src/Entities/User.php
@@ -12,4 +12,7 @@ final class User extends Entity
 
 	public $name;
 	public $passwordHash;
+	public $email;
+	public $registrationDate;
+	public $lastLoginTime;
 }
diff --git a/src/FormData/RegistrationFormData.php b/src/FormData/RegistrationFormData.php
new file mode 100644
index 00000000..99f2a9b2
--- /dev/null
+++ b/src/FormData/RegistrationFormData.php
@@ -0,0 +1,9 @@
+<?php
+namespace Szurubooru\FormData;
+
+class RegistrationFormData
+{
+	public $name;
+	public $password;
+	public $email;
+}
diff --git a/src/Services/AuthService.php b/src/Services/AuthService.php
index 9ddcfc50..25c17378 100644
--- a/src/Services/AuthService.php
+++ b/src/Services/AuthService.php
@@ -7,19 +7,21 @@ final class AuthService
 	private $loginToken = null;
 
 	private $passwordService;
+	private $timeService;
 	private $userDao;
 	private $tokenDao;
 
 	public function __construct(
 		\Szurubooru\Services\PasswordService $passwordService,
+		\Szurubooru\Services\TimeService $timeService,
 		\Szurubooru\Dao\TokenDao $tokenDao,
 		\Szurubooru\Dao\UserDao $userDao)
 	{
-		$this->loggedInUser = new \Szurubooru\Entities\User();
-		$this->loggedInUser->name = 'Anonymous';
-		$this->userDao = $userDao;
-		$this->tokenDao = $tokenDao;
+		$this->loggedInUser = $this->getAnonymousUser();
 		$this->passwordService = $passwordService;
+		$this->timeService = $timeService;
+		$this->tokenDao = $tokenDao;
+		$this->userDao = $userDao;
 	}
 
 	public function isLoggedIn()
@@ -47,8 +49,9 @@ final class AuthService
 		if ($user->passwordHash != $passwordHash)
 			throw new \InvalidArgumentException('Specified password is invalid.');
 
-		$this->loggedInUser = $user;
 		$this->loginToken = $this->createAndSaveLoginToken($user);
+		$this->loggedInUser = $user;
+		$this->updateLoginTime($user);
 	}
 
 	public function loginFromToken($loginTokenName)
@@ -59,6 +62,8 @@ final class AuthService
 
 		$this->loginToken = $loginToken;
 		$this->loggedInUser = $this->userDao->getById($loginToken->additionalData);
+		$this->updateLoginTime($this->loggedInUser);
+
 		if (!$this->loggedInUser)
 		{
 			$this->logout();
@@ -66,10 +71,18 @@ final class AuthService
 		}
 	}
 
+	public function getAnonymousUser()
+	{
+		$user = new \Szurubooru\Entities\User();
+		$user->name = 'Anonymous user';
+		$user->accessRank = \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS;
+		return $user;
+	}
+
 	public function loginAnonymous()
 	{
 		$this->loginToken = null;
-		$this->loggedInUser = $this->userService->getAnonymousUser();
+		$this->loggedInUser = $this->getAnonymousUser();
 	}
 
 	public function logout()
@@ -90,4 +103,10 @@ final class AuthService
 		$this->tokenDao->save($loginToken);
 		return $loginToken;
 	}
+
+	private function updateLoginTime($user)
+	{
+		$user->lastLoginTime = $this->timeService->getCurrentTime();
+		$this->userDao->save($user);
+	}
 }
diff --git a/src/Services/UserService.php b/src/Services/UserService.php
index 9e4951d7..0433de4e 100644
--- a/src/Services/UserService.php
+++ b/src/Services/UserService.php
@@ -3,34 +3,52 @@ namespace Szurubooru\Services;
 
 class UserService
 {
-	private $userDao;
 	private $config;
+	private $userDao;
+	private $passwordService;
+	private $emailService;
+	private $timeService;
 
 	public function __construct(
+		\Szurubooru\Config $config,
 		\Szurubooru\Dao\UserDao $userDao,
-		\Szurubooru\Config $config)
+		\Szurubooru\Services\PasswordService $passwordService,
+		\Szurubooru\Services\EmailService $emailService,
+		\Szurubooru\Services\TimeService $timeService)
 	{
-		$this->userDao = $userDao;
 		$this->config = $config;
+		$this->userDao = $userDao;
+		$this->passwordService = $passwordService;
+		$this->emailService = $emailService;
+		$this->timeService = $timeService;
 	}
 
-	public function getById($userId)
+	public function register(\Szurubooru\FormData\RegistrationFormData $formData)
 	{
-		return $this->userDao->getById($userId);
-	}
+		$this->validateUserName($formData->name);
+		$this->passwordService->validatePassword($formData->password);
+		$this->emailService->validateEmail($formData->email);
 
-	public function getByName($userName)
-	{
-		return $this->userDao->getByName($userName);
-	}
+		if ($this->userDao->getByName($formData->name))
+			throw new \DomainException('User with this name already exists.');
+
+		//todo: privilege checking
+
+		$user = new \Szurubooru\Entities\User();
+		$user->name = $formData->name;
+		$user->email = $formData->email;
+		$user->passwordHash = $this->passwordService->getHash($formData->password);
+		$user->registrationTime = $this->timeService->getCurrentTime();
+
+		//todo: send activation mail if necessary
 
-	public function save($user)
-	{
 		return $this->userDao->save($user);
 	}
 
-	public function validateUserName($userName)
+	public function validateUserName(&$userName)
 	{
+		$userName = trim($userName);
+
 		if (!$userName)
 			throw new \DomainException('User name cannot be empty.');
 
@@ -41,11 +59,4 @@ class UserService
 		if (strlen($userName) < $maxUserNameLength)
 			throw new \DomainException('User name must have at most ' . $minUserNameLength . ' character(s).');
 	}
-
-	public function getAnonymousUser()
-	{
-		$user = new \Szurubooru\Entities\User();
-		$user->name = 'Anonymous user';
-		$user->accessRank = \Szurubooru\Entities\User::ACCESS_RANK_ANONYMOUS;
-	}
 }
diff --git a/src/ViewProxies/User.php b/src/ViewProxies/User.php
index 6a27b21f..9e7bc49d 100644
--- a/src/ViewProxies/User.php
+++ b/src/ViewProxies/User.php
@@ -3,6 +3,7 @@ namespace Szurubooru\ViewProxies;
 
 class User
 {
+	public $id;
 	public $name;
 
 	public function __construct($user)
@@ -10,6 +11,7 @@ class User
 		if (!$user)
 			return;
 
+		$this->id = $user->id;
 		$this->name = $user->name;
 	}
 }
diff --git a/tests/Dao/TokenDaoTest.php b/tests/Dao/TokenDaoTest.php
new file mode 100644
index 00000000..7763cf91
--- /dev/null
+++ b/tests/Dao/TokenDaoTest.php
@@ -0,0 +1,28 @@
+<?php
+namespace Szurubooru\Tests\Dao;
+
+final class TokenDaoTest extends \Szurubooru\Tests\AbstractDatabaseTest
+{
+	public function testRetrievingByValidName()
+	{
+		$tokenDao = new \Szurubooru\Dao\TokenDao($this->databaseConnection);
+
+		$token = new \Szurubooru\Entities\Token();
+		$token->name = 'test';
+
+		$tokenDao->save($token);
+		$expected = $token;
+		$actual = $tokenDao->getByName($token->name);
+
+		$this->assertEquals($actual, $expected);
+	}
+
+	public function testRetrievingByInvalidName()
+	{
+		$tokenDao = new \Szurubooru\Dao\TokenDao($this->databaseConnection);
+
+		$actual = $tokenDao->getByName('rubbish');
+
+		$this->assertNull($actual);
+	}
+}
diff --git a/tests/Services/AuthServiceTest.php b/tests/Services/AuthServiceTest.php
index 966f34b9..a1bfc4f6 100644
--- a/tests/Services/AuthServiceTest.php
+++ b/tests/Services/AuthServiceTest.php
@@ -6,18 +6,20 @@ class AuthServiceTest extends \PHPUnit_Framework_TestCase
 	public function testInvalidUser()
 	{
 		$passwordServiceMock = $this->getPasswordServiceMock();
+		$timeServiceMock = $this->getTimeServiceMock();
 		$tokenDaoMock = $this->getTokenDaoMock();
 		$userDaoMock = $this->getUserDaoMock();
 
 		$this->setExpectedException(\InvalidArgumentException::class, 'User not found');
 
-		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
+		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $timeServiceMock, $tokenDaoMock, $userDaoMock);
 		$authService->loginFromCredentials('dummy', 'godzilla');
 	}
 
 	public function testInvalidPassword()
 	{
 		$passwordServiceMock = $this->getPasswordServiceMock();
+		$timeServiceMock = $this->getTimeServiceMock();
 		$tokenDaoMock = $this->getTokenDaoMock();
 		$userDaoMock = $this->getUserDaoMock();
 
@@ -28,7 +30,7 @@ class AuthServiceTest extends \PHPUnit_Framework_TestCase
 		$testUser->passwordHash = 'hash';
 		$userDaoMock->expects($this->once())->method('getByName')->willReturn($testUser);
 
-		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
+		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $timeServiceMock, $tokenDaoMock, $userDaoMock);
 		$this->setExpectedException(\InvalidArgumentException::class, 'Specified password is invalid');
 		$authService->loginFromCredentials('dummy', 'godzilla');
 	}
@@ -36,6 +38,7 @@ class AuthServiceTest extends \PHPUnit_Framework_TestCase
 	public function testValidCredentials()
 	{
 		$passwordServiceMock = $this->getPasswordServiceMock();
+		$timeServiceMock = $this->getTimeServiceMock();
 		$tokenDaoMock = $this->getTokenDaoMock();
 		$userDaoMock = $this->getUserDaoMock();
 
@@ -47,16 +50,33 @@ class AuthServiceTest extends \PHPUnit_Framework_TestCase
 		$testUser->passwordHash = 'hash';
 		$userDaoMock->expects($this->once())->method('getByName')->willReturn($testUser);
 
-		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
+		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $timeServiceMock, $tokenDaoMock, $userDaoMock);
 		$authService->loginFromCredentials('dummy', 'godzilla');
 
 		$this->assertTrue($authService->isLoggedIn());
 		$this->assertEquals($testUser, $authService->getLoggedInUser());
+		$this->assertNotNull($authService->getLoginToken());
+		$this->assertNotNull($authService->getLoginToken()->name);
+	}
+
+	public function testInvalidToken()
+	{
+		$passwordServiceMock = $this->getPasswordServiceMock();
+		$timeServiceMock = $this->getTimeServiceMock();
+		$tokenDaoMock = $this->getTokenDaoMock();
+		$userDaoMock = $this->getUserDaoMock();
+
+		$tokenDaoMock->expects($this->once())->method('getByName')->willReturn(null);
+
+		$this->setExpectedException(\Exception::class);
+		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $timeServiceMock, $tokenDaoMock, $userDaoMock);
+		$authService->loginFromToken('');
 	}
 
 	public function testValidToken()
 	{
 		$passwordServiceMock = $this->getPasswordServiceMock();
+		$timeServiceMock = $this->getTimeServiceMock();
 		$tokenDaoMock = $this->getTokenDaoMock();
 		$userDaoMock = $this->getUserDaoMock();
 
@@ -71,11 +91,13 @@ class AuthServiceTest extends \PHPUnit_Framework_TestCase
 		$testToken->additionalData = $testUser->id;
 		$tokenDaoMock->expects($this->once())->method('getByName')->willReturn($testToken);
 
-		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $tokenDaoMock, $userDaoMock);
+		$authService = new \Szurubooru\Services\AuthService($passwordServiceMock, $timeServiceMock, $tokenDaoMock, $userDaoMock);
 		$authService->loginFromToken($testToken->name);
 
 		$this->assertTrue($authService->isLoggedIn());
 		$this->assertEquals($testUser, $authService->getLoggedInUser());
+		$this->assertNotNull($authService->getLoginToken());
+		$this->assertNotNull($authService->getLoginToken()->name);
 	}
 
 	private function getTokenDaoMock()
@@ -92,4 +114,9 @@ class AuthServiceTest extends \PHPUnit_Framework_TestCase
 	{
 		return $this->getMockBuilder(\Szurubooru\Services\PasswordService::class)->disableOriginalConstructor()->getMock();
 	}
+
+	private function getTimeServiceMock()
+	{
+		return $this->getMockBuilder(\Szurubooru\Services\TimeService::class)->disableOriginalConstructor()->getMock();
+	}
 }