This is an API that can be consumed with basic auth requests and there won't be login or logout features. Users are allowed to access URIs that they are permitted to otherwise "401 Unauthorised" error gets produced. Users' credentials are stored in database.


End-points, users and access right



An authenticated user can send a JSON request to get response back in either JSON or XML format. Response format is defined by looking at the resource extension students.json|.xml. If the extension is not provided (like above), by default JSON response is used.


Security.yml


security:

encoders:
Api\SchoolBundle\Entity\User:
algorithm: bcrypt
cost: 12

role_hierarchy:
ROLE_STUDENT: ROLE_USER
ROLE_LECTURER: ROLE_USER
ROLE_ADMIN: ROLE_USER

providers:
user:
entity:
class: ApiSchoolBundle:User
property: username

firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false

secured:
pattern: ^/.*
stateless: true
http_basic:
realm: "School API"
provider: user

Routing.yml


api_school:
resource: "@ApiSchoolBundle/Controller/"
prefix: /1/
type: annotation

Controllers


StudentController


namespace Api\SchoolBundle\Controller;

use Api\SchoolBundle\Util\ApiUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/students", service="api_school.controller.student")
*/
class StudentController
{
private $apiUser;

public function __construct(ApiUser $apiUser)
{
$this->apiUser = $apiUser;
}

/**
* @param string $format
*
* @Security("has_role('ROLE_USER')")
* @Method({"GET"})
* @Route(".{format}", requirements={"format"="json|xml"}, defaults={"format"="json"})
*
* @return Response
*/
public function getAllAction($format)
{
$user = $this->apiUser->get();

return new Response(json_encode(['roles' => $user->getRoles(), 'response' => $format]));
}

/**
* @param string $format
* @param Request $request
*
* @Security("has_role('ROLE_ADMIN')")
* @Method({"POST"})
* @Route(".{format}", requirements={"format"="json|xml"}, defaults={"format"="json"})
*
* @return Response
*/
public function createOneAction(Request $request, $format)
{
$user = $this->apiUser->get();

return new Response(json_encode(['roles' => $user->getRoles(), 'response' => $format]));
}
}

LecturerController


namespace Api\SchoolBundle\Controller;

use Api\SchoolBundle\Util\ApiUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/lecturers", service="api_school.controller.lecturer")
*/
class LecturerController
{
private $apiUser;

public function __construct(ApiUser $apiUser)
{
$this->apiUser = $apiUser;
}

/**
* @param string $format
*
* @Security("has_role('ROLE_LECTURER')")
* @Method({"GET"})
* @Route(".{format}", requirements={"format"="json|xml"}, defaults={"format"="json"})
*
* @return Response
*/
public function getAllAction($format)
{
$user = $this->apiUser->get();

return new Response(json_encode(['roles' => $user->getRoles(), 'response' => $format]));
}

/**
* @param string $format
* @param Request $request
*
* @Security("has_role('ROLE_ADMIN')")
* @Method({"POST"})
* @Route(".{format}", requirements={"format"="json|xml"}, defaults={"format"="json"})
*
* @return Response
*/
public function createOneAction(Request $request, $format)
{
$user = $this->apiUser->get();

return new Response(json_encode(['roles' => $user->getRoles(), 'response' => $format]));
}
}

AdminController


namespace Api\SchoolBundle\Controller;

use Api\SchoolBundle\Util\ApiUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/admins", service="api_school.controller.admin")
*/
class AdminController
{
private $apiUser;

public function __construct(ApiUser $apiUser)
{
$this->apiUser = $apiUser;
}

/**
* @param string $format
*
* @Security("has_role('ROLE_ADMIN')")
* @Method({"GET"})
* @Route(".{format}", requirements={"format"="json|xml"}, defaults={"format"="json"})
*
* @return Response
*/
public function getAllAction($format)
{
$user = $this->apiUser->get();

return new Response(json_encode(['roles' => $user->getRoles(), 'response' => $format]));
}

/**
* @param string $format
* @param Request $request
*
* @Security("has_role('ROLE_ADMIN')")
* @Method({"POST"})
* @Route(".{format}", requirements={"format"="json|xml"}, defaults={"format"="json"})
*
* @return Response
*/
public function createOneAction(Request $request, $format)
{
$user = $this->apiUser->get();

return new Response(json_encode(['roles' => $user->getRoles(), 'response' => $format]));
}
}

Controllers.yml


services:

api_school.controller.student:
class: Api\SchoolBundle\Controller\StudentController
arguments:
- "@api_school.util.api_user"

api_school.controller.lecturer:
class: Api\SchoolBundle\Controller\LecturerController
arguments:
- "@api_school.util.api_user"

api_school.controller.admin:
class: Api\SchoolBundle\Controller\AdminController
arguments:
- "@api_school.util.api_user"

User entity


namespace Api\SchoolBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Serializable;
use Symfony\Component\Security\Core\User\UserInterface;

/**
* @ORM\Table(name="user",
* uniqueConstraints={
* @ORM\UniqueConstraint(name="username_unq", columns={"username"}),
* @ORM\UniqueConstraint(name="email_unq", columns={"email"})
* }
* )
* @ORM\Entity(repositoryClass="Api\SchoolBundle\Repository\UserRepository")
*/
class User implements UserInterface, Serializable
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

/**
* @ORM\Column(name="username", type="string", length=100)
*/
private $username;

/**
* @ORM\Column(name="password", type="string", length=100)
*/
private $password;

/**
* @ORM\Column(name="email", type="string", length=100)
*/
private $email;

/**
* @ORM\Column(name="roles", type="array")
*/
private $roles;

/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive = true;

/**
* @return integer
*/
public function getId()
{
return $this->id;
}

public function getUsername()
{
return $this->username;
}

public function setUsername($username)
{
$this->username = $username;

return $this;
}

public function getPassword()
{
return $this->password;
}

public function setPassword($password)
{
$this->password = $password;

return $this;
}

public function getEmail()
{
return $this->email;
}

public function setEmail($email)
{
$this->email = $email;

return $this;
}

public function getIsActive()
{
return $this->isActive;
}

public function setIsActive($isActive)
{
$this->isActive = $isActive;

return $this;
}

public function getRoles()
{
return $this->roles;
}

public function setRoles(array $roles)
{
return $this->roles = $roles;
}

public function getSalt()
{
return null;
}

public function eraseCredentials()
{
}

public function serialize()
{
return serialize([$this->id, $this->username, $this->password]);
}

public function unserialize($serialized)
{
list ($this->id, $this->username, $this->password) = unserialize($serialized);
}
}

This is how our entity looks like in database.


CREATE TABLE IF NOT EXISTS `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`password` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`email` varchar(100) COLLATE utf8_unicode_ci NOT NULL,
`is_active` tinyint(1) NOT NULL,
`roles` longtext COLLATE utf8_unicode_ci NOT NULL COMMENT '(DC2Type:array)',
PRIMARY KEY (`id`),
UNIQUE KEY `username_unq` (`username`),
UNIQUE KEY `email_unq` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;

UserRepository


namespace Api\SchoolBundle\Repository;

use Doctrine\ORM\EntityRepository;

class UserRepository extends EntityRepository
{
}

services:

api_school.repository.user:
class: Api\SchoolBundle\Repository\UserRepository
factory: ["@doctrine.orm.entity_manager", "getRepository"]
arguments:
- Api\SchoolBundle\Entity\User

Every request to API runs query below however if you want the user to be able to use any other field, not only username as defined in security.yml file, you can implement your own query in repository. For more information, read Using a Custom Query to Load the User.


SELECT id, username, password, email, roles, is_active FROM user WHERE username = ? LIMIT 1 ["admin"] []

ApiUser helper


namespace Api\SchoolBundle\Util;

use Api\SchoolBundle\Entity\User;
use Api\SchoolBundle\Exception\ApiException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class ApiUser
{
private $tokenStorage;

public function __construct(
TokenStorageInterface $tokenStorage
) {
$this->tokenStorage = $tokenStorage;
}

/**
* @return User
*/
public function get()
{
$user = $this->tokenStorage->getToken()->getUser();
if (!$user instanceof User) {
throw new ApiException('API user cannot be found.');
}

return $user;
}
}

services:

api_school.util.api_user:
class: Api\SchoolBundle\Util\ApiUser
arguments:
- "@security.token_storage"

CreateUserCommand


This is the command we use to create users.


namespace Api\SchoolBundle\Command;

use Api\SchoolBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class CreateUserCommand extends Command
{
private $entityManager;
private $userPasswordEncoder;

public function __construct(
EntityManagerInterface $entityManager,
UserPasswordEncoderInterface $userPasswordEncoder
) {
parent::__construct();

$this->entityManager = $entityManager;
$this->userPasswordEncoder = $userPasswordEncoder;
}

protected function configure()
{
$this
->setName('school:create-user')
->addOption(
'username',
null,
InputOption::VALUE_REQUIRED,
'The username of the user.'
)
->addOption(
'email',
null,
InputOption::VALUE_REQUIRED,
'The email of the user.'
)
->addOption(
'roles',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'The roles of the user.'
);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$password = bin2hex(random_bytes(16));

$this->createUser($input, $password);
$this->outputCredentials($output, $input->getOption('username'), $password);
}

private function createUser(InputInterface $input, $password)
{
$user = new User();
$user->setUsername($input->getOption('username'));
$user->setEmail($input->getOption('email'));
$user->setRoles($input->getOption('roles'));
$password = $this->userPasswordEncoder->encodePassword($user, $password);
$user->setPassword($password);

$this->entityManager->persist($user);
$this->entityManager->flush();
}

private function outputCredentials(OutputInterface $output, $username, $password)
{
$output->writeln(PHP_EOL.'CREDENTIALS');
$output->writeln('-----------');
$output->writeln('Username: '.$username);
$output->writeln('Password: '.$password.PHP_EOL);
}
}

services:

api_school.command.create_user:
class: Api\SchoolBundle\Command\CreateUserCommand
arguments:
- "@doctrine.orm.entity_manager"
- "@security.password_encoder"
tags:
- { name: console.command }

Let's create our users.


$ app/console school:create-user --username="student" --email="s@s.com" --roles="ROLE_USER" --roles="ROLE_STUDENT"

CREDENTIALS
-----------
Username: student
Password: 7bac6226bc57bed59559853cc2769f0a

$ app/console school:create-user --username="lecturer" --email="l@l.com" --roles="ROLE_USER" --roles="ROLE_LECTURER"

CREDENTIALS
-----------
Username: lecturer
Password: 68e31a9c7f47de3f1bd95d04f19dd935

$ app/console school:create-user --username="admin" --email="a@a.com" --roles="ROLE_USER" --roles="ROLE_STUDENT" --roles="ROLE_LECTURER" --roles="ROLE_ADMIN"

CREDENTIALS
-----------
Username: admin
Password: 52bb7533ef8bcd523ca7dc09d6c8cd3d

Test


You can use plain credentials above to send requests to endpoints I listed above or use Authorization header. See examples below.


curl -v -X GET -u "student:7bac6226bc57bed59559853cc2769f0a" "http://192.168.50.10:8083/app_dev.php/1/students.xml"

curl -v -X GET -H "Authorization: Basic c3R1ZGVudDo3YmFjNjIyNmJjNTdiZWQ1OTU1OTg1M2NjMjc2OWYwYQ==" "http://192.168.50.10:8083/app_dev.php/1/students.xml"

curl -v -X POST -u "admin:52bb7533ef8bcd523ca7dc09d6c8cd3d" -H "Content-Type: application/json" -d '{"key_1": "value", "key_1": "value"}' "http://192.168.50.10:8083/app_dev.php/1/students.xml"

curl -v -X POST -H "Authorization: Basic YWRtaW46NTJiYjc1MzNlZjhiY2Q1MjNjYTdkYzA5ZDZjOGNkM2Q=" "Content-Type: application/json" -d '{"key_1": "value", "key_1": "value"}' "http://192.168.50.10:8083/app_dev.php/1/students.xml"