├── .gitignore ├── README.md ├── _tuts ├── README.md ├── apitoken-configuration.diff ├── apitoken-creating-empty-apitokenauthenticator.diff ├── apitoken-custom-user-provider.diff ├── apitoken-fill-in-authenticator.diff ├── apitoken-test-it.diff ├── facebook-add-abstract-class.diff ├── facebook-create-empty-authenticator.diff ├── facebook-finish-registration.diff ├── facebook-full-implementation.diff ├── facebook-install-libs.diff ├── facebook-trait-experimentation.diff ├── facebook-use-state.diff ├── formlogin-add-logout.diff ├── formlogin-ajax-on-failure.diff ├── formlogin-ajax-on-success.diff ├── formlogin-create-empty-authenticator.diff ├── formlogin-filling-in-the-basics.diff ├── formlogin-inject-container.diff ├── formlogin-last-login-subscriber.diff ├── formlogin-login-with-username-email.diff ├── formlogin-register-as-a-service.diff ├── formlogin-security-yml-setup.diff ├── formlogin-using-customauthenticationexception.diff ├── install-install-guard.diff ├── login-adding-login-processing-controller.diff ├── login-creating-login-form.diff ├── steps.json └── userprovider-basic-security-yml-config.diff ├── app ├── .htaccess ├── AppCache.php ├── AppKernel.php ├── Resources │ └── views │ │ ├── base.html.twig │ │ └── default │ │ └── index.html.twig ├── SymfonyRequirements.php ├── autoload.php ├── cache │ └── .gitkeep ├── check.php ├── config │ ├── config.yml │ ├── config_dev.yml │ ├── config_prod.yml │ ├── config_test.yml │ ├── parameters.yml │ ├── parameters.yml.dist │ ├── routing.yml │ ├── routing_dev.yml │ ├── security.yml │ └── services.yml ├── console ├── logs │ └── .gitkeep └── phpunit.xml.dist ├── composer.json ├── composer.lock ├── knpu ├── api-token.md ├── error-messages.md ├── failure-handling.md ├── fos-user-bundle.md ├── guard-auth.png ├── install.md ├── login-form-csrf.md ├── login-form-customize-user.md ├── login-form.md ├── manually-authenticating.md ├── metadata.yml ├── multiple-authenticators.md ├── notes.md ├── silex.md ├── social-login.md └── success-handling.md ├── src ├── .htaccess └── AppBundle │ ├── AppBundle.php │ ├── Controller │ └── DefaultController.php │ ├── DataFixtures │ └── ORM │ │ └── LoadUserData.php │ ├── Entity │ └── User.php │ ├── Repository │ └── UserRepository.php │ └── Tests │ └── Controller │ └── DefaultControllerTest.php └── web ├── .htaccess ├── app.php ├── app_dev.php ├── apple-touch-icon.png ├── config.php ├── favicon.ico └── robots.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /app/bootstrap.php.cache 2 | /app/cache/* 3 | !app/cache/.gitkeep 4 | /app/config/parameters.yml 5 | /app/logs/* 6 | !app/logs/.gitkeep 7 | /app/phpunit.xml 8 | /bin/ 9 | /composer.phar 10 | /vendor/ 11 | /web/bundles/ 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tutorial project for KnpUGuard 2 | ============================== 3 | 4 | ... 5 | -------------------------------------------------------------------------------- /_tuts/README.md: -------------------------------------------------------------------------------- 1 | # Hello there! 2 | 3 | The files in this directory cannot be modified directly: we use an internal tool 4 | to manage them. If you find an issue with the code, you can open an issue on the 5 | repository. In fact, that would be awesome :). 6 | -------------------------------------------------------------------------------- /_tuts/apitoken-configuration.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/config/security.yml b/app/config/security.yml 2 | index 8185402..f1a8f1f 100644 3 | --- a/app/config/security.yml 4 | +++ b/app/config/security.yml 5 | @@ -22,3 +22,6 @@ security: 6 | guard: 7 | authenticators: 8 | - app.form_login_authenticator 9 | + - app.api_token_authenticator 10 | + # by default, use the start() function from FormLoginAuthenticator 11 | + entry_point: app.form_login_authenticator 12 | diff --git a/app/config/services.yml b/app/config/services.yml 13 | index 6df21da..5dadd04 100644 14 | --- a/app/config/services.yml 15 | +++ b/app/config/services.yml 16 | @@ -7,3 +7,7 @@ services: 17 | app.form_login_authenticator: 18 | class: AppBundle\Security\FormLoginAuthenticator 19 | arguments: ["@service_container"] 20 | + 21 | + app.api_token_authenticator: 22 | + class: AppBundle\Security\ApiTokenAuthenticator 23 | + arguments: ["@doctrine.orm.entity_manager"] 24 | -------------------------------------------------------------------------------- /_tuts/apitoken-creating-empty-apitokenauthenticator.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/ApiTokenAuthenticator.php b/src/AppBundle/Security/ApiTokenAuthenticator.php 2 | new file mode 100644 3 | index 0000000..2774c5d 4 | --- /dev/null 5 | +++ b/src/AppBundle/Security/ApiTokenAuthenticator.php 6 | @@ -0,0 +1,49 @@ 7 | +em = $em; 27 | + } 28 | + 29 | public function getCredentials(Request $request) 30 | { 31 | - // TODO: Implement getCredentials() method. 32 | + return $request->headers->get('X-TOKEN'); 33 | } 34 | 35 | public function getUser($credentials, UserProviderInterface $userProvider) 36 | { 37 | - // TODO: Implement getUser() method. 38 | + $user = $this->em->getRepository('AppBundle:User') 39 | + ->findOneBy(array('apiToken' => $credentials)); 40 | + 41 | + // we could just return null, but this allows us to control the message a bit more 42 | + if (!$user) { 43 | + throw new AuthenticationCredentialsNotFoundException(); 44 | + } 45 | + 46 | + return $user; 47 | } 48 | 49 | public function checkCredentials($credentials, UserInterface $user) 50 | { 51 | - // TODO: Implement checkCredentials() method. 52 | + // the fact that they had a valid token that *was* attached to a user 53 | + // means that their credentials are correct. So, there's nothing 54 | + // additional (like a password) to check here. 55 | + return true; 56 | } 57 | 58 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception) 59 | { 60 | - // TODO: Implement onAuthenticationFailure() method. 61 | + return new JsonResponse( 62 | + // you could translate the message 63 | + array('message' => $exception->getMessageKey()), 64 | + 403 65 | + ); 66 | } 67 | 68 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) 69 | { 70 | - // TODO: Implement onAuthenticationSuccess() method. 71 | + // do nothing - let the request just continue! 72 | + return; 73 | } 74 | 75 | public function supportsRememberMe() 76 | { 77 | - // TODO: Implement supportsRememberMe() method. 78 | + return false; 79 | } 80 | 81 | public function start(Request $request, AuthenticationException $authException = null) 82 | { 83 | - // TODO: Implement start() method. 84 | + return new JsonResponse( 85 | + // you could translate the message 86 | + array('message' => 'Authentication required'), 87 | + 401 88 | + ); 89 | } 90 | -} 91 | \ No newline at end of file 92 | +} 93 | -------------------------------------------------------------------------------- /_tuts/apitoken-test-it.diff: -------------------------------------------------------------------------------- 1 | diff --git a/composer.json b/composer.json 2 | index 4edc9f9..9a24044 100644 3 | --- a/composer.json 4 | +++ b/composer.json 5 | @@ -20,7 +20,8 @@ 6 | "sensio/distribution-bundle": "~4.0", 7 | "sensio/framework-extra-bundle": "~3.0,>=3.0.2", 8 | "incenteev/composer-parameter-handler": "~2.0", 9 | - "doctrine/doctrine-fixtures-bundle": "^2.2" 10 | + "doctrine/doctrine-fixtures-bundle": "^2.2", 11 | + "guzzlehttp/guzzle": "~6.0" 12 | }, 13 | "require-dev": { 14 | "sensio/generator-bundle": "~2.3" 15 | diff --git a/composer.lock b/composer.lock 16 | index 65bab9a..c2a77da 100644 17 | --- a/composer.lock 18 | +++ b/composer.lock 19 | @@ -821,6 +821,177 @@ 20 | "time": "2014-12-16 13:45:01" 21 | }, 22 | { 23 | + "name": "guzzlehttp/guzzle", 24 | + "version": "6.0.1", 25 | + "source": { 26 | + "type": "git", 27 | + "url": "https://github.com/guzzle/guzzle.git", 28 | + "reference": "f992b7b487a816c957d317442bed4966409873e0" 29 | + }, 30 | + "dist": { 31 | + "type": "zip", 32 | + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f992b7b487a816c957d317442bed4966409873e0", 33 | + "reference": "f992b7b487a816c957d317442bed4966409873e0", 34 | + "shasum": "" 35 | + }, 36 | + "require": { 37 | + "guzzlehttp/promises": "^1.0.0", 38 | + "guzzlehttp/psr7": "^1.0.0", 39 | + "php": ">=5.5.0" 40 | + }, 41 | + "require-dev": { 42 | + "ext-curl": "*", 43 | + "phpunit/phpunit": "^4.0", 44 | + "psr/log": "^1.0" 45 | + }, 46 | + "type": "library", 47 | + "extra": { 48 | + "branch-alias": { 49 | + "dev-master": "6.0-dev" 50 | + } 51 | + }, 52 | + "autoload": { 53 | + "files": [ 54 | + "src/functions.php" 55 | + ], 56 | + "psr-4": { 57 | + "GuzzleHttp\\": "src/" 58 | + } 59 | + }, 60 | + "notification-url": "https://packagist.org/downloads/", 61 | + "license": [ 62 | + "MIT" 63 | + ], 64 | + "authors": [ 65 | + { 66 | + "name": "Michael Dowling", 67 | + "email": "mtdowling@gmail.com", 68 | + "homepage": "https://github.com/mtdowling" 69 | + } 70 | + ], 71 | + "description": "Guzzle is a PHP HTTP client library", 72 | + "homepage": "http://guzzlephp.org/", 73 | + "keywords": [ 74 | + "client", 75 | + "curl", 76 | + "framework", 77 | + "http", 78 | + "http client", 79 | + "rest", 80 | + "web service" 81 | + ], 82 | + "time": "2015-05-27 16:57:51" 83 | + }, 84 | + { 85 | + "name": "guzzlehttp/promises", 86 | + "version": "1.0.1", 87 | + "source": { 88 | + "type": "git", 89 | + "url": "https://github.com/guzzle/promises.git", 90 | + "reference": "2ee5bc7f1a92efecc90da7f6711a53a7be26b5b7" 91 | + }, 92 | + "dist": { 93 | + "type": "zip", 94 | + "url": "https://api.github.com/repos/guzzle/promises/zipball/2ee5bc7f1a92efecc90da7f6711a53a7be26b5b7", 95 | + "reference": "2ee5bc7f1a92efecc90da7f6711a53a7be26b5b7", 96 | + "shasum": "" 97 | + }, 98 | + "require": { 99 | + "php": ">=5.5.0" 100 | + }, 101 | + "require-dev": { 102 | + "phpunit/phpunit": "~4.0" 103 | + }, 104 | + "type": "library", 105 | + "extra": { 106 | + "branch-alias": { 107 | + "dev-master": "1.0-dev" 108 | + } 109 | + }, 110 | + "autoload": { 111 | + "psr-4": { 112 | + "GuzzleHttp\\Promise\\": "src/" 113 | + }, 114 | + "files": [ 115 | + "src/functions.php" 116 | + ] 117 | + }, 118 | + "notification-url": "https://packagist.org/downloads/", 119 | + "license": [ 120 | + "MIT" 121 | + ], 122 | + "authors": [ 123 | + { 124 | + "name": "Michael Dowling", 125 | + "email": "mtdowling@gmail.com", 126 | + "homepage": "https://github.com/mtdowling" 127 | + } 128 | + ], 129 | + "description": "Guzzle promises library", 130 | + "keywords": [ 131 | + "promise" 132 | + ], 133 | + "time": "2015-06-24 16:16:25" 134 | + }, 135 | + { 136 | + "name": "guzzlehttp/psr7", 137 | + "version": "1.1.0", 138 | + "source": { 139 | + "type": "git", 140 | + "url": "https://github.com/guzzle/psr7.git", 141 | + "reference": "af0e1758de355eb113917ad79c3c0e3604bce4bd" 142 | + }, 143 | + "dist": { 144 | + "type": "zip", 145 | + "url": "https://api.github.com/repos/guzzle/psr7/zipball/af0e1758de355eb113917ad79c3c0e3604bce4bd", 146 | + "reference": "af0e1758de355eb113917ad79c3c0e3604bce4bd", 147 | + "shasum": "" 148 | + }, 149 | + "require": { 150 | + "php": ">=5.4.0", 151 | + "psr/http-message": "~1.0" 152 | + }, 153 | + "provide": { 154 | + "psr/http-message-implementation": "1.0" 155 | + }, 156 | + "require-dev": { 157 | + "phpunit/phpunit": "~4.0" 158 | + }, 159 | + "type": "library", 160 | + "extra": { 161 | + "branch-alias": { 162 | + "dev-master": "1.0-dev" 163 | + } 164 | + }, 165 | + "autoload": { 166 | + "psr-4": { 167 | + "GuzzleHttp\\Psr7\\": "src/" 168 | + }, 169 | + "files": [ 170 | + "src/functions.php" 171 | + ] 172 | + }, 173 | + "notification-url": "https://packagist.org/downloads/", 174 | + "license": [ 175 | + "MIT" 176 | + ], 177 | + "authors": [ 178 | + { 179 | + "name": "Michael Dowling", 180 | + "email": "mtdowling@gmail.com", 181 | + "homepage": "https://github.com/mtdowling" 182 | + } 183 | + ], 184 | + "description": "PSR-7 message implementation", 185 | + "keywords": [ 186 | + "http", 187 | + "message", 188 | + "stream", 189 | + "uri" 190 | + ], 191 | + "time": "2015-06-24 19:55:15" 192 | + }, 193 | + { 194 | "name": "incenteev/composer-parameter-handler", 195 | "version": "v2.1.1", 196 | "source": { 197 | @@ -1111,6 +1282,55 @@ 198 | "time": "2015-03-09 09:58:04" 199 | }, 200 | { 201 | + "name": "psr/http-message", 202 | + "version": "1.0", 203 | + "source": { 204 | + "type": "git", 205 | + "url": "https://github.com/php-fig/http-message.git", 206 | + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" 207 | + }, 208 | + "dist": { 209 | + "type": "zip", 210 | + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", 211 | + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", 212 | + "shasum": "" 213 | + }, 214 | + "require": { 215 | + "php": ">=5.3.0" 216 | + }, 217 | + "type": "library", 218 | + "extra": { 219 | + "branch-alias": { 220 | + "dev-master": "1.0.x-dev" 221 | + } 222 | + }, 223 | + "autoload": { 224 | + "psr-4": { 225 | + "Psr\\Http\\Message\\": "src/" 226 | + } 227 | + }, 228 | + "notification-url": "https://packagist.org/downloads/", 229 | + "license": [ 230 | + "MIT" 231 | + ], 232 | + "authors": [ 233 | + { 234 | + "name": "PHP-FIG", 235 | + "homepage": "http://www.php-fig.org/" 236 | + } 237 | + ], 238 | + "description": "Common interface for HTTP messages", 239 | + "keywords": [ 240 | + "http", 241 | + "http-message", 242 | + "psr", 243 | + "psr-7", 244 | + "request", 245 | + "response" 246 | + ], 247 | + "time": "2015-05-04 20:22:00" 248 | + }, 249 | + { 250 | "name": "paragonie/random_compat", 251 | "version": "v1.2.0", 252 | "source": { 253 | diff --git a/testAuth.php b/testAuth.php 254 | new file mode 100644 255 | index 0000000..1da31fa 256 | --- /dev/null 257 | +++ b/testAuth.php 258 | @@ -0,0 +1,17 @@ 259 | +get('http://localhost:8000/secure', [ 265 | + 'allow_redirects' => false, 266 | + 'http_errors' => false, 267 | + 'headers' => [ 268 | + // token for anna_admin in LoadUserData fixtures 269 | + 'X-Token' => 'ABCD1234' 270 | + ] 271 | +]); 272 | + 273 | +echo sprintf("Status Code: %s\n\n", $res->getStatusCode()); 274 | +echo $res->getBody(); 275 | +echo "\n\n"; 276 | -------------------------------------------------------------------------------- /_tuts/facebook-add-abstract-class.diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/_tuts/facebook-add-abstract-class.diff -------------------------------------------------------------------------------- /_tuts/facebook-create-empty-authenticator.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FacebookAuthenticator.php b/src/AppBundle/Security/FacebookAuthenticator.php 2 | new file mode 100644 3 | index 0000000..a48dd45 4 | --- /dev/null 5 | +++ b/src/AppBundle/Security/FacebookAuthenticator.php 6 | @@ -0,0 +1,39 @@ 7 | +Register! 14 | + {{ form_end(form) }} 15 | +{% endblock %} 16 | diff --git a/src/AppBundle/Controller/FacebookConnectController.php b/src/AppBundle/Controller/FacebookConnectController.php 17 | index c1819fc..f808750 100644 18 | --- a/src/AppBundle/Controller/FacebookConnectController.php 19 | +++ b/src/AppBundle/Controller/FacebookConnectController.php 20 | @@ -2,6 +2,9 @@ 21 | 22 | namespace AppBundle\Controller; 23 | 24 | +use AppBundle\Entity\User; 25 | +use AppBundle\Form\FacebookRegistrationType; 26 | +use League\OAuth2\Client\Provider\FacebookUser; 27 | use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; 28 | use Symfony\Bundle\FrameworkBundle\Controller\Controller; 29 | use Symfony\Component\HttpFoundation\Request; 30 | @@ -31,4 +34,51 @@ class FacebookConnectController extends Controller 31 | { 32 | // will not be reached! 33 | } 34 | + 35 | + /** 36 | + * @Route("/connect/facebook/registration", name="connect_facebook_registration") 37 | + */ 38 | + public function finishRegistration(Request $request) 39 | + { 40 | + /** @var FacebookUser $facebookUser */ 41 | + $facebookUser = $this->get('app.facebook_authenticator') 42 | + ->getUserInfoFromSession($request); 43 | + if (!$facebookUser) { 44 | + throw $this->createNotFoundException('How did you get here without user information!?'); 45 | + } 46 | + $user = new User(); 47 | + $user->setFacebookId($facebookUser->getId()); 48 | + $user->setEmail($facebookUser->getEmail()); 49 | + 50 | + $form = $this->createForm(new FacebookRegistrationType(), $user); 51 | + 52 | + $form->handleRequest($request); 53 | + if ($form->isValid()) { 54 | + // encode the password manually 55 | + $plainPassword = $form['plainPassword']->getData(); 56 | + $encodedPassword = $this->get('security.password_encoder') 57 | + ->encodePassword($user, $plainPassword); 58 | + $user->setPassword($encodedPassword); 59 | + 60 | + $em = $this->getDoctrine()->getManager(); 61 | + $em->persist($user); 62 | + $em->flush(); 63 | + 64 | + // remove the session information 65 | + $request->getSession()->remove('facebook_user'); 66 | + 67 | + // log the user in manually 68 | + $guardHandler = $this->container->get('security.authentication.guard_handler'); 69 | + return $guardHandler->authenticateUserAndHandleSuccess( 70 | + $user, 71 | + $request, 72 | + $this->container->get('app.facebook_authenticator'), 73 | + 'main' // the firewall key 74 | + ); 75 | + } 76 | + 77 | + return $this->render('facebook/registration.html.twig', array( 78 | + 'form' => $form->createView() 79 | + )); 80 | + } 81 | } 82 | diff --git a/src/AppBundle/Form/FacebookRegistrationType.php b/src/AppBundle/Form/FacebookRegistrationType.php 83 | new file mode 100644 84 | index 0000000..b3da268 85 | --- /dev/null 86 | +++ b/src/AppBundle/Form/FacebookRegistrationType.php 87 | @@ -0,0 +1,37 @@ 88 | +add('username', 'text') 102 | + ->add('email', 'text', array( 103 | + 'disabled' => true 104 | + )) 105 | + ->add('plainPassword', 'repeated', array( 106 | + 'mapped' => false, // allows this to not be a real property on User 107 | + 'type' => 'password', 108 | + 'first_options' => array('label' => 'Password'), 109 | + 'second_options' => array('label' => 'Password again'), 110 | + )); 111 | + } 112 | + 113 | + public function configureOptions(OptionsResolver $resolver) 114 | + { 115 | + $resolver->setDefaults(array( 116 | + 'data_class' => 'AppBundle\Entity\User' 117 | + )); 118 | + } 119 | + 120 | + public function getName() 121 | + { 122 | + return 'app_bundle_user_registration_type'; 123 | + } 124 | +} 125 | diff --git a/src/AppBundle/Security/FacebookAuthenticator.php b/src/AppBundle/Security/FacebookAuthenticator.php 126 | index 214481b..decf07a 100644 127 | --- a/src/AppBundle/Security/FacebookAuthenticator.php 128 | +++ b/src/AppBundle/Security/FacebookAuthenticator.php 129 | @@ -3,12 +3,15 @@ 130 | namespace AppBundle\Security; 131 | 132 | use AppBundle\Entity\User; 133 | +use KnpU\OAuth2ClientBundle\Security\Exception\FinishRegistrationException; 134 | +use KnpU\OAuth2ClientBundle\Security\Helper\FinishRegistrationBehavior; 135 | use Doctrine\ORM\EntityManager; 136 | use KnpU\OAuth2ClientBundle\Security\Helper\PreviousUrlHelper; 137 | use KnpU\OAuth2ClientBundle\Security\Helper\SaveAuthFailureMessage; 138 | use League\OAuth2\Client\Token\AccessToken; 139 | use League\OAuth2\Client\Provider\Exception\IdentityProviderException; 140 | use League\OAuth2\Client\Provider\Facebook; 141 | +use League\OAuth2\Client\Provider\FacebookUser; 142 | use Symfony\Component\HttpFoundation\RedirectResponse; 143 | use Symfony\Component\Routing\RouterInterface; 144 | use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator; 145 | @@ -23,6 +26,7 @@ class FacebookAuthenticator extends SocialAuthenticator 146 | { 147 | use PreviousUrlHelper; 148 | use SaveAuthFailureMessage; 149 | + use FinishRegistrationBehavior; 150 | 151 | /** 152 | * @var Facebook 153 | @@ -78,15 +82,10 @@ class FacebookAuthenticator extends SocialAuthenticator 154 | $user = $this->em->getRepository('AppBundle:User') 155 | ->findOneBy(array('email' => $email)); 156 | 157 | - // 3) no user? Perhaps you just want to create one 158 | - // or maybe you want to redirect to a registration (in that case, keep reading_ 159 | + // 3) no user? Redirect to finish registration 160 | if (!$user) { 161 | - $user = new User(); 162 | - $user->setUsername($email); 163 | - $user->setEmail($email); 164 | - // set an un-encoded password, which basically makes it *not* possible 165 | - // to login with any password 166 | - $user->setPassword('no password'); 167 | + // throw a special exception we created - see onAuthenticaitonFailure 168 | + throw new FinishRegistrationException($facebookUser); 169 | } 170 | 171 | // make sure the Facebook user is set 172 | @@ -106,6 +105,13 @@ class FacebookAuthenticator extends SocialAuthenticator 173 | 174 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception) 175 | { 176 | + if ($exception instanceof FinishRegistrationException) { 177 | + $this->saveUserInfoToSession($request, $exception); 178 | + 179 | + $registrationUrl = $this->router->generate('connect_facebook_registration'); 180 | + return new RedirectResponse($registrationUrl); 181 | + } 182 | + 183 | $this->saveAuthenticationErrorToSession($request, $exception); 184 | 185 | $loginUrl = $this->router->generate('security_login'); 186 | -------------------------------------------------------------------------------- /_tuts/facebook-full-implementation.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/Resources/views/security/login.html.twig b/app/Resources/views/security/login.html.twig 2 | index 0d5b3a4..23720ac 100644 3 | --- a/app/Resources/views/security/login.html.twig 4 | +++ b/app/Resources/views/security/login.html.twig 5 | @@ -20,5 +20,7 @@ 6 | 7 |
8 | 9 | + 10 | + Login with Facebook 11 | 12 | {% endblock %} 13 | diff --git a/app/config/config.yml b/app/config/config.yml 14 | index b4a2d0a..bddc6ec 100644 15 | --- a/app/config/config.yml 16 | +++ b/app/config/config.yml 17 | @@ -78,3 +78,12 @@ swiftmailer: 18 | username: "%mailer_user%" 19 | password: "%mailer_password%" 20 | spool: { type: memory } 21 | + 22 | +knpu_oauth2_client: 23 | + clients: 24 | + my_facebook_client: 25 | + type: facebook 26 | + client_id: %facebook_app_id% 27 | + client_secret: %facebook_app_secret% 28 | + graph_api_version: v2.5 29 | + redirect_route: connect_facebook_check 30 | diff --git a/app/config/parameters.yml b/app/config/parameters.yml 31 | index f5e0347..dd86a8a 100644 32 | --- a/app/config/parameters.yml 33 | +++ b/app/config/parameters.yml 34 | @@ -10,3 +10,5 @@ parameters: 35 | mailer_user: null 36 | mailer_password: null 37 | secret: a3250d79a622c2f1d034943d9ff4397d500552dd 38 | + facebook_app_id: XXXX 39 | + facebook_app_secret: XXXX 40 | diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist 41 | index 886c92b..2833651 100644 42 | --- a/app/config/parameters.yml.dist 43 | +++ b/app/config/parameters.yml.dist 44 | @@ -17,3 +17,5 @@ parameters: 45 | 46 | # A secret key that's used to generate certain security-related tokens 47 | secret: ThisTokenIsNotSoSecretChangeIt 48 | + facebook_app_id: XXXX 49 | + facebook_app_secret: XXXX 50 | diff --git a/app/config/security.yml b/app/config/security.yml 51 | index f1a8f1f..3ab12ae 100644 52 | --- a/app/config/security.yml 53 | +++ b/app/config/security.yml 54 | @@ -23,5 +23,6 @@ security: 55 | authenticators: 56 | - app.form_login_authenticator 57 | - app.api_token_authenticator 58 | + - app.facebook_authenticator 59 | # by default, use the start() function from FormLoginAuthenticator 60 | entry_point: app.form_login_authenticator 61 | diff --git a/app/config/services.yml b/app/config/services.yml 62 | index e10b621..4884a2b 100644 63 | --- a/app/config/services.yml 64 | +++ b/app/config/services.yml 65 | @@ -17,3 +17,7 @@ services: 66 | arguments: ["@doctrine.orm.entity_manager"] 67 | tags: 68 | - { name: kernel.event_subscriber } 69 | + 70 | + app.facebook_authenticator: 71 | + class: AppBundle\Security\FacebookAuthenticator 72 | + autowire: true 73 | diff --git a/src/AppBundle/Controller/FacebookConnectController.php b/src/AppBundle/Controller/FacebookConnectController.php 74 | new file mode 100644 75 | index 0000000..c1819fc 76 | --- /dev/null 77 | +++ b/src/AppBundle/Controller/FacebookConnectController.php 78 | @@ -0,0 +1,34 @@ 79 | +get('knpu.oauth2.registry') 98 | + ->getClient('my_facebook_client'); 99 | + 100 | + return $facebookClient->redirect([ 101 | + 'public_profile', 'email' 102 | + ]); 103 | + } 104 | + 105 | + /** 106 | + * @Route("/connect/facebook-check", name="connect_facebook_check") 107 | + */ 108 | + public function connectFacebookActionCheck() 109 | + { 110 | + // will not be reached! 111 | + } 112 | +} 113 | diff --git a/src/AppBundle/Entity/User.php b/src/AppBundle/Entity/User.php 114 | index 53297e6..eaf0425 100644 115 | --- a/src/AppBundle/Entity/User.php 116 | +++ b/src/AppBundle/Entity/User.php 117 | @@ -56,6 +56,11 @@ class User implements UserInterface 118 | */ 119 | private $lastLoginTime; 120 | 121 | + /** 122 | + * @ORM\Column(type="string", length=50, nullable=true) 123 | + */ 124 | + private $facebookId; 125 | + 126 | public function __construct() 127 | { 128 | $this->apiToken = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36); 129 | @@ -146,4 +151,14 @@ class User implements UserInterface 130 | { 131 | $this->lastLoginTime = $lastLoginTime; 132 | } 133 | + 134 | + public function getFacebookId() 135 | + { 136 | + return $this->facebookId; 137 | + } 138 | + 139 | + public function setFacebookId($facebookId) 140 | + { 141 | + $this->facebookId = $facebookId; 142 | + } 143 | } 144 | diff --git a/src/AppBundle/Security/FacebookAuthenticator.php b/src/AppBundle/Security/FacebookAuthenticator.php 145 | index a48dd45..13c9b3d 100644 146 | --- a/src/AppBundle/Security/FacebookAuthenticator.php 147 | +++ b/src/AppBundle/Security/FacebookAuthenticator.php 148 | @@ -2,8 +2,19 @@ 149 | 150 | namespace AppBundle\Security; 151 | 152 | -use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator; 153 | +use AppBundle\Entity\User; 154 | +use Doctrine\ORM\EntityManager; 155 | +use KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient; 156 | +use League\OAuth2\Client\Token\AccessToken; 157 | +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; 158 | +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; 159 | +use League\OAuth2\Client\Provider\Facebook; 160 | +use League\OAuth2\Client\Provider\FacebookUser; 161 | +use Symfony\Component\HttpFoundation\RedirectResponse; 162 | use Symfony\Component\HttpFoundation\Response; 163 | +use Symfony\Component\Routing\RouterInterface; 164 | +use Symfony\Component\Security\Core\Security; 165 | +use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator; 166 | use Symfony\Component\HttpFoundation\Request; 167 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 168 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 169 | @@ -12,28 +23,125 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; 170 | 171 | class FacebookAuthenticator extends SocialAuthenticator 172 | { 173 | + /** 174 | + * @var Facebook 175 | + */ 176 | + private $facebookClient; 177 | + /** 178 | + * @var EntityManager 179 | + */ 180 | + private $em; 181 | + /** 182 | + * @var RouterInterface 183 | + */ 184 | + private $router; 185 | + 186 | + public function __construct(FacebookClient $facebookClient, EntityManager $em, RouterInterface $router) 187 | + { 188 | + $this->facebookClient = $facebookClient; 189 | + $this->em = $em; 190 | + $this->router = $router; 191 | + } 192 | + 193 | public function getCredentials(Request $request) 194 | { 195 | - // todo 196 | + if ($request->getPathInfo() != '/connect/facebook-check') { 197 | + // skip authentication unless we're on this URL! 198 | + return null; 199 | + } 200 | + 201 | + try { 202 | + return $this->facebookClient->getAccessToken($request); 203 | + } catch (IdentityProviderException $e) { 204 | + // you could parse the response to see the problem 205 | + throw $e; 206 | + } 207 | } 208 | 209 | public function getUser($credentials, UserProviderInterface $userProvider) 210 | { 211 | - // todo 212 | + /** @var AccessToken $accessToken */ 213 | + $accessToken = $credentials; 214 | + 215 | + $facebookUser = $this->facebookClient->fetchUserFromToken($accessToken); 216 | + $email = $facebookUser->getEmail(); 217 | + 218 | + // 1) have they logged in with Facebook before? Easy! 219 | + $existingUser = $this->em->getRepository('AppBundle:User') 220 | + ->findOneBy(array('facebookId' => $facebookUser->getId())); 221 | + if ($existingUser) { 222 | + return $existingUser; 223 | + } 224 | + 225 | + // 2) do we have a matching user by email? 226 | + $user = $this->em->getRepository('AppBundle:User') 227 | + ->findOneBy(array('email' => $email)); 228 | + 229 | + // 3) no user? Perhaps you just want to create one 230 | + // or maybe you want to redirect to a registration (in that case, keep reading_ 231 | + if (!$user) { 232 | + $user = new User(); 233 | + $user->setUsername($email); 234 | + $user->setEmail($email); 235 | + // set an un-encoded password, which basically makes it *not* possible 236 | + // to login with any password 237 | + $user->setPassword('no password'); 238 | + } 239 | + 240 | + // make sure the Facebook user is set 241 | + $user->setFacebookId($facebookUser->getId()); 242 | + $this->em->persist($user); 243 | + $this->em->flush(); 244 | + 245 | + return $user; 246 | + } 247 | + 248 | + public function checkCredentials($credentials, UserInterface $user) 249 | + { 250 | + return true; 251 | + // do nothing - the fact that the access token worked means that 252 | + // our app has been authorized with Facebook 253 | } 254 | 255 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception) 256 | { 257 | - // todo 258 | + // this would happen if something went wrong in the OAuth flow 259 | + $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); 260 | + 261 | + $url = $this->router 262 | + ->generate('security_login'); 263 | + 264 | + return new RedirectResponse($url); 265 | } 266 | 267 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) 268 | { 269 | - // todo 270 | + // todo - remove needing this crazy thing 271 | + $targetPath = $request->getSession()->get('_security.'.$providerKey.'.target_path'); 272 | + 273 | + if (!$targetPath) { 274 | + $router = $this->router; 275 | + $targetPath = $router->generate('homepage'); 276 | + } 277 | + 278 | + return new RedirectResponse($targetPath); 279 | } 280 | 281 | + /** 282 | + * Called when an anonymous user tries to access an protected page. 283 | + * 284 | + * In our app, this is never actually called, because there is only *one* 285 | + * "entry_point" per firewall and in security.yml, we're using 286 | + * app.form_login_authenticator as the entry point (so it's start() method 287 | + * is the one that's called). 288 | + */ 289 | public function start(Request $request, AuthenticationException $authException = null) 290 | { 291 | - // todo 292 | + // not called in our app, but if it were, redirecting to the 293 | + // login page makes sense 294 | + $url = $this->router 295 | + ->generate('security_login'); 296 | + 297 | + return new RedirectResponse($url); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /_tuts/facebook-trait-experimentation.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FacebookAuthenticator.php b/src/AppBundle/Security/FacebookAuthenticator.php 2 | index 13c9b3d..214481b 100644 3 | --- a/src/AppBundle/Security/FacebookAuthenticator.php 4 | +++ b/src/AppBundle/Security/FacebookAuthenticator.php 5 | @@ -4,25 +4,26 @@ namespace AppBundle\Security; 6 | 7 | use AppBundle\Entity\User; 8 | use Doctrine\ORM\EntityManager; 9 | -use KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient; 10 | +use KnpU\OAuth2ClientBundle\Security\Helper\PreviousUrlHelper; 11 | +use KnpU\OAuth2ClientBundle\Security\Helper\SaveAuthFailureMessage; 12 | use League\OAuth2\Client\Token\AccessToken; 13 | -use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; 14 | use League\OAuth2\Client\Provider\Exception\IdentityProviderException; 15 | use League\OAuth2\Client\Provider\Facebook; 16 | -use League\OAuth2\Client\Provider\FacebookUser; 17 | use Symfony\Component\HttpFoundation\RedirectResponse; 18 | -use Symfony\Component\HttpFoundation\Response; 19 | use Symfony\Component\Routing\RouterInterface; 20 | -use Symfony\Component\Security\Core\Security; 21 | use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator; 22 | use Symfony\Component\HttpFoundation\Request; 23 | use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 24 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 25 | use Symfony\Component\Security\Core\User\UserInterface; 26 | use Symfony\Component\Security\Core\User\UserProviderInterface; 27 | +use KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient; 28 | 29 | class FacebookAuthenticator extends SocialAuthenticator 30 | { 31 | + use PreviousUrlHelper; 32 | + use SaveAuthFailureMessage; 33 | + 34 | /** 35 | * @var Facebook 36 | */ 37 | @@ -105,26 +106,19 @@ class FacebookAuthenticator extends SocialAuthenticator 38 | 39 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception) 40 | { 41 | - // this would happen if something went wrong in the OAuth flow 42 | - $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); 43 | + $this->saveAuthenticationErrorToSession($request, $exception); 44 | 45 | - $url = $this->router 46 | - ->generate('security_login'); 47 | - 48 | - return new RedirectResponse($url); 49 | + $loginUrl = $this->router->generate('security_login'); 50 | + return new RedirectResponse($loginUrl); 51 | } 52 | 53 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) 54 | { 55 | - // todo - remove needing this crazy thing 56 | - $targetPath = $request->getSession()->get('_security.'.$providerKey.'.target_path'); 57 | - 58 | - if (!$targetPath) { 59 | - $router = $this->router; 60 | - $targetPath = $router->generate('homepage'); 61 | + if (!$url = $this->getPreviousUrl($request, $providerKey)) { 62 | + $url = $this->router->generate('homepage'); 63 | } 64 | 65 | - return new RedirectResponse($targetPath); 66 | + return new RedirectResponse($url); 67 | } 68 | 69 | /** 70 | -------------------------------------------------------------------------------- /_tuts/facebook-use-state.diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/_tuts/facebook-use-state.diff -------------------------------------------------------------------------------- /_tuts/formlogin-add-logout.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/config/security.yml b/app/config/security.yml 2 | index f4f78fa..8185402 100644 3 | --- a/app/config/security.yml 4 | +++ b/app/config/security.yml 5 | @@ -18,6 +18,7 @@ security: 6 | 7 | main: 8 | anonymous: ~ 9 | + logout: ~ 10 | guard: 11 | authenticators: 12 | - app.form_login_authenticator 13 | diff --git a/src/AppBundle/Controller/SecurityController.php b/src/AppBundle/Controller/SecurityController.php 14 | index a72d572..4d2ea6f 100644 15 | --- a/src/AppBundle/Controller/SecurityController.php 16 | +++ b/src/AppBundle/Controller/SecurityController.php 17 | @@ -29,4 +29,12 @@ class SecurityController extends Controller 18 | { 19 | // will never be executed 20 | } 21 | + 22 | + /** 23 | + * @Route("/logout") 24 | + */ 25 | + public function logoutAction() 26 | + { 27 | + // will never be executed 28 | + } 29 | } 30 | -------------------------------------------------------------------------------- /_tuts/formlogin-ajax-on-failure.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FormLoginAuthenticator.php b/src/AppBundle/Security/FormLoginAuthenticator.php 2 | index 6623768..efe6487 100644 3 | --- a/src/AppBundle/Security/FormLoginAuthenticator.php 4 | +++ b/src/AppBundle/Security/FormLoginAuthenticator.php 5 | @@ -4,9 +4,11 @@ namespace AppBundle\Security; 6 | 7 | use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; 8 | use Symfony\Component\DependencyInjection\ContainerInterface; 9 | +use Symfony\Component\HttpFoundation\JsonResponse; 10 | use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; 11 | use Symfony\Component\HttpFoundation\Request; 12 | use Symfony\Component\Routing\RouterInterface; 13 | +use Symfony\Component\Security\Core\Exception\AuthenticationException; 14 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 15 | use Symfony\Component\Security\Core\Security; 16 | use Symfony\Component\Security\Core\User\UserInterface; 17 | @@ -67,6 +69,21 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator 18 | return true; 19 | } 20 | 21 | + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) 22 | + { 23 | + // AJAX! Maybe return some JSON 24 | + if ($request->isXmlHttpRequest()) { 25 | + return new JsonResponse( 26 | + // you could translate the message 27 | + array('message' => $exception->getMessageKey()), 28 | + 403 29 | + ); 30 | + } 31 | + 32 | + // for non-AJAX requests, return the normal redirect 33 | + return parent::onAuthenticationFailure($request, $exception); 34 | + } 35 | + 36 | protected function getLoginUrl() 37 | { 38 | return $this->container->get('router') 39 | -------------------------------------------------------------------------------- /_tuts/formlogin-ajax-on-success.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FormLoginAuthenticator.php b/src/AppBundle/Security/FormLoginAuthenticator.php 2 | index efe6487..d65e852 100644 3 | --- a/src/AppBundle/Security/FormLoginAuthenticator.php 4 | +++ b/src/AppBundle/Security/FormLoginAuthenticator.php 5 | @@ -8,6 +8,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; 6 | use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; 7 | use Symfony\Component\HttpFoundation\Request; 8 | use Symfony\Component\Routing\RouterInterface; 9 | +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; 10 | use Symfony\Component\Security\Core\Exception\AuthenticationException; 11 | use Symfony\Component\Security\Core\Exception\BadCredentialsException; 12 | use Symfony\Component\Security\Core\Security; 13 | @@ -84,6 +85,20 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator 14 | return parent::onAuthenticationFailure($request, $exception); 15 | } 16 | 17 | + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) 18 | + { 19 | + // AJAX! Return some JSON 20 | + if ($request->isXmlHttpRequest()) { 21 | + return new JsonResponse( 22 | + // maybe send back the user's id 23 | + array('userId' => $token->getUser()->getId()) 24 | + ); 25 | + } 26 | + 27 | + // for non-AJAX requests, return the normal redirect 28 | + return parent::onAuthenticationSuccess($request, $token, $providerKey); 29 | + } 30 | + 31 | protected function getLoginUrl() 32 | { 33 | return $this->container->get('router') 34 | -------------------------------------------------------------------------------- /_tuts/formlogin-create-empty-authenticator.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FormLoginAuthenticator.php b/src/AppBundle/Security/FormLoginAuthenticator.php 2 | new file mode 100644 3 | index 0000000..8cb5cac 4 | --- /dev/null 5 | +++ b/src/AppBundle/Security/FormLoginAuthenticator.php 6 | @@ -0,0 +1,36 @@ 7 | +getPathInfo() != '/login_check') { 21 | + return; 22 | + } 23 | + 24 | + $username = $request->request->get('_username'); 25 | + $request->getSession()->set(Security::LAST_USERNAME, $username); 26 | + $password = $request->request->get('_password'); 27 | + 28 | + return array( 29 | + 'username' => $username, 30 | + 'password' => $password 31 | + ); 32 | } 33 | 34 | public function getUser($credentials, UserProviderInterface $userProvider) 35 | { 36 | - // TODO: Implement getUser() method. 37 | + $username = $credentials['username']; 38 | + 39 | + return $userProvider->loadUserByUsername($username); 40 | } 41 | 42 | public function checkCredentials($credentials, UserInterface $user) 43 | { 44 | - // TODO: Implement checkCredentials() method. 45 | + $plainPassword = $credentials['password']; 46 | + $encoder = $this->container->get('security.password_encoder'); 47 | + if (!$encoder->isPasswordValid($user, $plainPassword)) { 48 | + return false; 49 | + } 50 | + 51 | + return true; 52 | } 53 | 54 | protected function getLoginUrl() 55 | { 56 | - // TODO: Implement getLoginUrl() method. 57 | + return $this->container->get('router') 58 | + ->generate('security_login'); 59 | } 60 | 61 | protected function getDefaultSuccessRedirectUrl() 62 | { 63 | - // TODO: Implement getDefaultSuccessRedirectUrl() method. 64 | + return $this->container->get('router') 65 | + ->generate('homepage'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /_tuts/formlogin-inject-container.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FormLoginAuthenticator.php b/src/AppBundle/Security/FormLoginAuthenticator.php 2 | index 8cb5cac..91e167f 100644 3 | --- a/src/AppBundle/Security/FormLoginAuthenticator.php 4 | +++ b/src/AppBundle/Security/FormLoginAuthenticator.php 5 | @@ -2,6 +2,7 @@ 6 | 7 | namespace AppBundle\Security; 8 | 9 | +use Symfony\Component\DependencyInjection\ContainerInterface; 10 | use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; 11 | use Symfony\Component\HttpFoundation\Request; 12 | use Symfony\Component\Security\Core\User\UserInterface; 13 | @@ -9,6 +10,13 @@ use Symfony\Component\Security\Core\User\UserProviderInterface; 14 | 15 | class FormLoginAuthenticator extends AbstractFormLoginAuthenticator 16 | { 17 | + private $container; 18 | + 19 | + public function __construct(ContainerInterface $container) 20 | + { 21 | + $this->container = $container; 22 | + } 23 | + 24 | public function getCredentials(Request $request) 25 | { 26 | // TODO: Implement getCredentials() method. 27 | -------------------------------------------------------------------------------- /_tuts/formlogin-last-login-subscriber.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/config/services.yml b/app/config/services.yml 2 | index 5dadd04..e10b621 100644 3 | --- a/app/config/services.yml 4 | +++ b/app/config/services.yml 5 | @@ -11,3 +11,9 @@ services: 6 | app.api_token_authenticator: 7 | class: AppBundle\Security\ApiTokenAuthenticator 8 | arguments: ["@doctrine.orm.entity_manager"] 9 | + 10 | + app.last_login_subscriber: 11 | + class: AppBundle\EventListener\LastLoginSubscriber 12 | + arguments: ["@doctrine.orm.entity_manager"] 13 | + tags: 14 | + - { name: kernel.event_subscriber } 15 | diff --git a/src/AppBundle/EventListener/LastLoginSubscriber.php b/src/AppBundle/EventListener/LastLoginSubscriber.php 16 | new file mode 100644 17 | index 0000000..2d1b91f 18 | --- /dev/null 19 | +++ b/src/AppBundle/EventListener/LastLoginSubscriber.php 20 | @@ -0,0 +1,33 @@ 21 | +em = $em; 38 | + } 39 | + 40 | + public function onInteractiveLogin(InteractiveLoginEvent $event) 41 | + { 42 | + /** @var User $user */ 43 | + $user = $event->getAuthenticationToken()->getUser(); 44 | + $user->setLastLoginTime(new \DateTime()); 45 | + $this->em->persist($user); 46 | + $this->em->flush($user); 47 | + } 48 | + 49 | + public static function getSubscribedEvents() 50 | + { 51 | + return array(SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'); 52 | + } 53 | +} 54 | -------------------------------------------------------------------------------- /_tuts/formlogin-login-with-username-email.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Repository/UserRepository.php b/src/AppBundle/Repository/UserRepository.php 2 | index d014ebc..568857f 100644 3 | --- a/src/AppBundle/Repository/UserRepository.php 4 | +++ b/src/AppBundle/Repository/UserRepository.php 5 | @@ -11,6 +11,7 @@ 6 | 7 | namespace AppBundle\Repository; 8 | 9 | +use AppBundle\Entity\User; 10 | use Doctrine\ORM\EntityRepository; 11 | 12 | /** 13 | @@ -24,4 +25,16 @@ use Doctrine\ORM\EntityRepository; 14 | */ 15 | class UserRepository extends EntityRepository 16 | { 17 | + /** 18 | + * @param string $username 19 | + * @return User 20 | + */ 21 | + public function findByUsernameOrEmail($username) 22 | + { 23 | + return $this->createQueryBuilder('u') 24 | + ->andWhere('u.username = :username OR u.email = :username') 25 | + ->setParameter('username', $username) 26 | + ->getQuery() 27 | + ->getOneOrNullResult(); 28 | + } 29 | } 30 | diff --git a/src/AppBundle/Security/FormLoginAuthenticator.php b/src/AppBundle/Security/FormLoginAuthenticator.php 31 | index 95231b3..277d6b9 100644 32 | --- a/src/AppBundle/Security/FormLoginAuthenticator.php 33 | +++ b/src/AppBundle/Security/FormLoginAuthenticator.php 34 | @@ -39,8 +39,12 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator 35 | public function getUser($credentials, UserProviderInterface $userProvider) 36 | { 37 | $username = $credentials['username']; 38 | + $userRepo = $this->container 39 | + ->get('doctrine') 40 | + ->getManager() 41 | + ->getRepository('AppBundle:User'); 42 | 43 | - return $userProvider->loadUserByUsername($username); 44 | + return $userRepo->findByUsernameOrEmail($username); 45 | } 46 | 47 | public function checkCredentials($credentials, UserInterface $user) 48 | -------------------------------------------------------------------------------- /_tuts/formlogin-register-as-a-service.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/config/services.yml b/app/config/services.yml 2 | index 5c76fc5..6df21da 100644 3 | --- a/app/config/services.yml 4 | +++ b/app/config/services.yml 5 | @@ -4,6 +4,6 @@ parameters: 6 | # parameter_name: value 7 | 8 | services: 9 | -# service_name: 10 | -# class: AppBundle\Directory\ClassName 11 | -# arguments: ["@another_service_name", "plain_value", "%parameter_name%"] 12 | + app.form_login_authenticator: 13 | + class: AppBundle\Security\FormLoginAuthenticator 14 | + arguments: ["@service_container"] 15 | -------------------------------------------------------------------------------- /_tuts/formlogin-security-yml-setup.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/config/security.yml b/app/config/security.yml 2 | index 9f38ef3..f4f78fa 100644 3 | --- a/app/config/security.yml 4 | +++ b/app/config/security.yml 5 | @@ -18,3 +18,6 @@ security: 6 | 7 | main: 8 | anonymous: ~ 9 | + guard: 10 | + authenticators: 11 | + - app.form_login_authenticator 12 | -------------------------------------------------------------------------------- /_tuts/formlogin-using-customauthenticationexception.diff: -------------------------------------------------------------------------------- 1 | diff --git a/src/AppBundle/Security/FormLoginAuthenticator.php b/src/AppBundle/Security/FormLoginAuthenticator.php 2 | index 277d6b9..6623768 100644 3 | --- a/src/AppBundle/Security/FormLoginAuthenticator.php 4 | +++ b/src/AppBundle/Security/FormLoginAuthenticator.php 5 | @@ -2,6 +2,7 @@ 6 | 7 | namespace AppBundle\Security; 8 | 9 | +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; 10 | use Symfony\Component\DependencyInjection\ContainerInterface; 11 | use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; 12 | use Symfony\Component\HttpFoundation\Request; 13 | @@ -39,6 +40,14 @@ class FormLoginAuthenticator extends AbstractFormLoginAuthenticator 14 | public function getUser($credentials, UserProviderInterface $userProvider) 15 | { 16 | $username = $credentials['username']; 17 | + 18 | + // a silly example of failing with a custom message 19 | + if ($username == 'rails_troll') { 20 | + throw new CustomUserMessageAuthenticationException( 21 | + 'Get outta here rails_troll - we don\'t like you!' 22 | + ); 23 | + } 24 | + 25 | $userRepo = $this->container 26 | ->get('doctrine') 27 | ->getManager() 28 | -------------------------------------------------------------------------------- /_tuts/install-install-guard.diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/_tuts/install-install-guard.diff -------------------------------------------------------------------------------- /_tuts/login-adding-login-processing-controller.diff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/_tuts/login-adding-login-processing-controller.diff -------------------------------------------------------------------------------- /_tuts/login-creating-login-form.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/Resources/views/security/login.html.twig b/app/Resources/views/security/login.html.twig 2 | new file mode 100644 3 | index 0000000..0d5b3a4 4 | --- /dev/null 5 | +++ b/app/Resources/views/security/login.html.twig 6 | @@ -0,0 +1,24 @@ 7 | +{% extends 'base.html.twig' %} 8 | + 9 | +{% block body %} 10 | +
11 | + {% if error %} 12 | +
13 | + {{ error.messageKey|trans(error.messageData) }} 14 | +
15 | + {% endif %} 16 | + 17 | +
18 | + 19 | + 20 | +
21 | + 22 | +
23 | + 24 | + 25 | +
26 | + 27 | +
28 | + 29 | +
30 | +{% endblock %} 31 | diff --git a/src/AppBundle/Controller/SecurityController.php b/src/AppBundle/Controller/SecurityController.php 32 | new file mode 100644 33 | index 0000000..a72d572 34 | --- /dev/null 35 | +++ b/src/AppBundle/Controller/SecurityController.php 36 | @@ -0,0 +1,32 @@ 37 | +get('security.authentication_utils'); 52 | + 53 | + return $this->render('security/login.html.twig', array( 54 | + // last username entered by the user (if any) 55 | + 'last_username' => $helper->getLastUsername(), 56 | + // last authentication error (if any) 57 | + 'error' => $helper->getLastAuthenticationError(), 58 | + )); 59 | + } 60 | + 61 | + /** 62 | + * @Route("/login_check", name="security_login_check") 63 | + */ 64 | + public function loginCheckAction() 65 | + { 66 | + // will never be executed 67 | + } 68 | +} 69 | -------------------------------------------------------------------------------- /_tuts/steps.json: -------------------------------------------------------------------------------- 1 | { 2 | "steps": [ 3 | { 4 | "id": "start", 5 | "name": "start", 6 | "description": null 7 | }, 8 | { 9 | "id": "userprovider-basic-security-yml-config", 10 | "name": "UserProvider: basic security.yml config", 11 | "description": null 12 | }, 13 | { 14 | "id": "login-creating-login-form", 15 | "name": "Login: creating login form", 16 | "description": null 17 | }, 18 | { 19 | "id": "login-adding-login-processing-controller", 20 | "name": "Login: adding login processing controller", 21 | "description": null 22 | }, 23 | { 24 | "id": "install-install-guard", 25 | "name": "Install: Install guard", 26 | "description": null 27 | }, 28 | { 29 | "id": "formlogin-create-empty-authenticator", 30 | "name": "FormLogin: Create empty authenticator", 31 | "description": null 32 | }, 33 | { 34 | "id": "formlogin-inject-container", 35 | "name": "FormLogin: Inject Container", 36 | "description": null 37 | }, 38 | { 39 | "id": "formlogin-filling-in-the-basics", 40 | "name": "FormLogin: Filling in the basics", 41 | "description": null 42 | }, 43 | { 44 | "id": "formlogin-register-as-a-service", 45 | "name": "FormLogin: register as a service", 46 | "description": null 47 | }, 48 | { 49 | "id": "formlogin-security-yml-setup", 50 | "name": "FormLogin: security.yml setup", 51 | "description": null 52 | }, 53 | { 54 | "id": "formlogin-add-logout", 55 | "name": "FormLogin: Add logout", 56 | "description": null 57 | }, 58 | { 59 | "id": "apitoken-creating-empty-apitokenauthenticator", 60 | "name": "ApiToken: Creating empty ApiTokenAuthenticator", 61 | "description": null 62 | }, 63 | { 64 | "id": "apitoken-custom-user-provider", 65 | "name": "ApiToken: Custom user provider", 66 | "description": null 67 | }, 68 | { 69 | "id": "apitoken-fill-in-authenticator", 70 | "name": "ApiToken: Fill in authenticator", 71 | "description": null 72 | }, 73 | { 74 | "id": "apitoken-configuration", 75 | "name": "ApiToken: Configuration", 76 | "description": null 77 | }, 78 | { 79 | "id": "apitoken-test-it", 80 | "name": "ApiToken: Test it", 81 | "description": null 82 | }, 83 | { 84 | "id": "formlogin-login-with-username-email", 85 | "name": "FormLogin: Login with username\/email", 86 | "description": null 87 | }, 88 | { 89 | "id": "formlogin-using-customauthenticationexception", 90 | "name": "FormLogin: Using CustomAuthenticationException", 91 | "description": null 92 | }, 93 | { 94 | "id": "formlogin-ajax-on-failure", 95 | "name": "FormLogin: AJAX on failure", 96 | "description": null 97 | }, 98 | { 99 | "id": "formlogin-ajax-on-success", 100 | "name": "FormLogin: AJAX on success", 101 | "description": null 102 | }, 103 | { 104 | "id": "formlogin-last-login-subscriber", 105 | "name": "FormLogin: Last login subscriber", 106 | "description": null 107 | }, 108 | { 109 | "id": "facebook-install-libs", 110 | "name": "Facebook: Install libs", 111 | "description": null 112 | }, 113 | { 114 | "id": "facebook-create-empty-authenticator", 115 | "name": "Facebook: Create empty authenticator", 116 | "description": null 117 | }, 118 | { 119 | "id": "facebook-full-implementation", 120 | "name": "Facebook: Full implementation", 121 | "description": null 122 | }, 123 | { 124 | "id": "facebook-add-abstract-class", 125 | "name": "Facebook: Add abstract class", 126 | "description": null 127 | }, 128 | { 129 | "id": "facebook-trait-experimentation", 130 | "name": "Facebook: Trait experimentation", 131 | "description": null 132 | }, 133 | { 134 | "id": "facebook-finish-registration", 135 | "name": "Facebook: Finish Registration", 136 | "description": null 137 | }, 138 | { 139 | "id": "facebook-use-state", 140 | "name": "Facebook: Use state", 141 | "description": null 142 | } 143 | ], 144 | "sha": "45f19ad66aed4b1666ef0743b5c7a1783bc0a882" 145 | } -------------------------------------------------------------------------------- /_tuts/userprovider-basic-security-yml-config.diff: -------------------------------------------------------------------------------- 1 | diff --git a/app/config/security.yml b/app/config/security.yml 2 | index a187595..9f38ef3 100644 3 | --- a/app/config/security.yml 4 | +++ b/app/config/security.yml 5 | @@ -1,8 +1,15 @@ 6 | security: 7 | 8 | + encoders: 9 | + # Our user class and the algorithm we'll use to encode passwords 10 | + # http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password 11 | + AppBundle\Entity\User: bcrypt 12 | + 13 | providers: 14 | - in_memory: 15 | - memory: ~ 16 | + # Simple example of loading users via Doctrine 17 | + # To load users from somewhere else: http://symfony.com/doc/current/cookbook/security/custom_provider.html 18 | + database_users: 19 | + entity: { class: AppBundle:User, property: username } 20 | 21 | firewalls: 22 | dev: 23 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /app/AppCache.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), array('dev', 'test'))) { 23 | $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); 24 | $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); 25 | $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); 26 | $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); 27 | $bundles[] = new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(); 28 | } 29 | 30 | return $bundles; 31 | } 32 | 33 | public function registerContainerConfiguration(LoaderInterface $loader) 34 | { 35 | $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Resources/views/base.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Welcome!{% endblock %} 6 | {% block stylesheets %}{% endblock %} 7 | 8 | 9 | 10 | {% block body %}{% endblock %} 11 | {% block javascripts %}{% endblock %} 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/Resources/views/default/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | 3 | {% block body %} 4 | Homepage. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /app/SymfonyRequirements.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | /* 13 | * Users of PHP 5.2 should be able to run the requirements checks. 14 | * This is why the file and all classes must be compatible with PHP 5.2+ 15 | * (e.g. not using namespaces and closures). 16 | * 17 | * ************** CAUTION ************** 18 | * 19 | * DO NOT EDIT THIS FILE as it will be overridden by Composer as part of 20 | * the installation/update process. The original file resides in the 21 | * SensioDistributionBundle. 22 | * 23 | * ************** CAUTION ************** 24 | */ 25 | 26 | /** 27 | * Represents a single PHP requirement, e.g. an installed extension. 28 | * It can be a mandatory requirement or an optional recommendation. 29 | * There is a special subclass, named PhpIniRequirement, to check a php.ini configuration. 30 | * 31 | * @author Tobias Schultze 32 | */ 33 | class Requirement 34 | { 35 | private $fulfilled; 36 | private $testMessage; 37 | private $helpText; 38 | private $helpHtml; 39 | private $optional; 40 | 41 | /** 42 | * Constructor that initializes the requirement. 43 | * 44 | * @param bool $fulfilled Whether the requirement is fulfilled 45 | * @param string $testMessage The message for testing the requirement 46 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 47 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 48 | * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement 49 | */ 50 | public function __construct($fulfilled, $testMessage, $helpHtml, $helpText = null, $optional = false) 51 | { 52 | $this->fulfilled = (bool) $fulfilled; 53 | $this->testMessage = (string) $testMessage; 54 | $this->helpHtml = (string) $helpHtml; 55 | $this->helpText = null === $helpText ? strip_tags($this->helpHtml) : (string) $helpText; 56 | $this->optional = (bool) $optional; 57 | } 58 | 59 | /** 60 | * Returns whether the requirement is fulfilled. 61 | * 62 | * @return bool true if fulfilled, otherwise false 63 | */ 64 | public function isFulfilled() 65 | { 66 | return $this->fulfilled; 67 | } 68 | 69 | /** 70 | * Returns the message for testing the requirement. 71 | * 72 | * @return string The test message 73 | */ 74 | public function getTestMessage() 75 | { 76 | return $this->testMessage; 77 | } 78 | 79 | /** 80 | * Returns the help text for resolving the problem. 81 | * 82 | * @return string The help text 83 | */ 84 | public function getHelpText() 85 | { 86 | return $this->helpText; 87 | } 88 | 89 | /** 90 | * Returns the help text formatted in HTML. 91 | * 92 | * @return string The HTML help 93 | */ 94 | public function getHelpHtml() 95 | { 96 | return $this->helpHtml; 97 | } 98 | 99 | /** 100 | * Returns whether this is only an optional recommendation and not a mandatory requirement. 101 | * 102 | * @return bool true if optional, false if mandatory 103 | */ 104 | public function isOptional() 105 | { 106 | return $this->optional; 107 | } 108 | } 109 | 110 | /** 111 | * Represents a PHP requirement in form of a php.ini configuration. 112 | * 113 | * @author Tobias Schultze 114 | */ 115 | class PhpIniRequirement extends Requirement 116 | { 117 | /** 118 | * Constructor that initializes the requirement. 119 | * 120 | * @param string $cfgName The configuration name used for ini_get() 121 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 122 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 123 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 124 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 125 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 126 | * @param string|null $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 127 | * @param string|null $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 128 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 129 | * @param bool $optional Whether this is only an optional recommendation not a mandatory requirement 130 | */ 131 | public function __construct($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null, $optional = false) 132 | { 133 | $cfgValue = ini_get($cfgName); 134 | 135 | if (is_callable($evaluation)) { 136 | if (null === $testMessage || null === $helpHtml) { 137 | throw new InvalidArgumentException('You must provide the parameters testMessage and helpHtml for a callback evaluation.'); 138 | } 139 | 140 | $fulfilled = call_user_func($evaluation, $cfgValue); 141 | } else { 142 | if (null === $testMessage) { 143 | $testMessage = sprintf('%s %s be %s in php.ini', 144 | $cfgName, 145 | $optional ? 'should' : 'must', 146 | $evaluation ? 'enabled' : 'disabled' 147 | ); 148 | } 149 | 150 | if (null === $helpHtml) { 151 | $helpHtml = sprintf('Set %s to %s in php.ini*.', 152 | $cfgName, 153 | $evaluation ? 'on' : 'off' 154 | ); 155 | } 156 | 157 | $fulfilled = $evaluation == $cfgValue; 158 | } 159 | 160 | parent::__construct($fulfilled || ($approveCfgAbsence && false === $cfgValue), $testMessage, $helpHtml, $helpText, $optional); 161 | } 162 | } 163 | 164 | /** 165 | * A RequirementCollection represents a set of Requirement instances. 166 | * 167 | * @author Tobias Schultze 168 | */ 169 | class RequirementCollection implements IteratorAggregate 170 | { 171 | private $requirements = array(); 172 | 173 | /** 174 | * Gets the current RequirementCollection as an Iterator. 175 | * 176 | * @return Traversable A Traversable interface 177 | */ 178 | public function getIterator() 179 | { 180 | return new ArrayIterator($this->requirements); 181 | } 182 | 183 | /** 184 | * Adds a Requirement. 185 | * 186 | * @param Requirement $requirement A Requirement instance 187 | */ 188 | public function add(Requirement $requirement) 189 | { 190 | $this->requirements[] = $requirement; 191 | } 192 | 193 | /** 194 | * Adds a mandatory requirement. 195 | * 196 | * @param bool $fulfilled Whether the requirement is fulfilled 197 | * @param string $testMessage The message for testing the requirement 198 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 199 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 200 | */ 201 | public function addRequirement($fulfilled, $testMessage, $helpHtml, $helpText = null) 202 | { 203 | $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, false)); 204 | } 205 | 206 | /** 207 | * Adds an optional recommendation. 208 | * 209 | * @param bool $fulfilled Whether the recommendation is fulfilled 210 | * @param string $testMessage The message for testing the recommendation 211 | * @param string $helpHtml The help text formatted in HTML for resolving the problem 212 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 213 | */ 214 | public function addRecommendation($fulfilled, $testMessage, $helpHtml, $helpText = null) 215 | { 216 | $this->add(new Requirement($fulfilled, $testMessage, $helpHtml, $helpText, true)); 217 | } 218 | 219 | /** 220 | * Adds a mandatory requirement in form of a php.ini configuration. 221 | * 222 | * @param string $cfgName The configuration name used for ini_get() 223 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 224 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 225 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 226 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 227 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 228 | * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 229 | * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 230 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 231 | */ 232 | public function addPhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) 233 | { 234 | $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, false)); 235 | } 236 | 237 | /** 238 | * Adds an optional recommendation in form of a php.ini configuration. 239 | * 240 | * @param string $cfgName The configuration name used for ini_get() 241 | * @param bool|callback $evaluation Either a boolean indicating whether the configuration should evaluate to true or false, 242 | * or a callback function receiving the configuration value as parameter to determine the fulfillment of the requirement 243 | * @param bool $approveCfgAbsence If true the Requirement will be fulfilled even if the configuration option does not exist, i.e. ini_get() returns false. 244 | * This is helpful for abandoned configs in later PHP versions or configs of an optional extension, like Suhosin. 245 | * Example: You require a config to be true but PHP later removes this config and defaults it to true internally. 246 | * @param string $testMessage The message for testing the requirement (when null and $evaluation is a boolean a default message is derived) 247 | * @param string $helpHtml The help text formatted in HTML for resolving the problem (when null and $evaluation is a boolean a default help is derived) 248 | * @param string|null $helpText The help text (when null, it will be inferred from $helpHtml, i.e. stripped from HTML tags) 249 | */ 250 | public function addPhpIniRecommendation($cfgName, $evaluation, $approveCfgAbsence = false, $testMessage = null, $helpHtml = null, $helpText = null) 251 | { 252 | $this->add(new PhpIniRequirement($cfgName, $evaluation, $approveCfgAbsence, $testMessage, $helpHtml, $helpText, true)); 253 | } 254 | 255 | /** 256 | * Adds a requirement collection to the current set of requirements. 257 | * 258 | * @param RequirementCollection $collection A RequirementCollection instance 259 | */ 260 | public function addCollection(RequirementCollection $collection) 261 | { 262 | $this->requirements = array_merge($this->requirements, $collection->all()); 263 | } 264 | 265 | /** 266 | * Returns both requirements and recommendations. 267 | * 268 | * @return array Array of Requirement instances 269 | */ 270 | public function all() 271 | { 272 | return $this->requirements; 273 | } 274 | 275 | /** 276 | * Returns all mandatory requirements. 277 | * 278 | * @return array Array of Requirement instances 279 | */ 280 | public function getRequirements() 281 | { 282 | $array = array(); 283 | foreach ($this->requirements as $req) { 284 | if (!$req->isOptional()) { 285 | $array[] = $req; 286 | } 287 | } 288 | 289 | return $array; 290 | } 291 | 292 | /** 293 | * Returns the mandatory requirements that were not met. 294 | * 295 | * @return array Array of Requirement instances 296 | */ 297 | public function getFailedRequirements() 298 | { 299 | $array = array(); 300 | foreach ($this->requirements as $req) { 301 | if (!$req->isFulfilled() && !$req->isOptional()) { 302 | $array[] = $req; 303 | } 304 | } 305 | 306 | return $array; 307 | } 308 | 309 | /** 310 | * Returns all optional recommendations. 311 | * 312 | * @return array Array of Requirement instances 313 | */ 314 | public function getRecommendations() 315 | { 316 | $array = array(); 317 | foreach ($this->requirements as $req) { 318 | if ($req->isOptional()) { 319 | $array[] = $req; 320 | } 321 | } 322 | 323 | return $array; 324 | } 325 | 326 | /** 327 | * Returns the recommendations that were not met. 328 | * 329 | * @return array Array of Requirement instances 330 | */ 331 | public function getFailedRecommendations() 332 | { 333 | $array = array(); 334 | foreach ($this->requirements as $req) { 335 | if (!$req->isFulfilled() && $req->isOptional()) { 336 | $array[] = $req; 337 | } 338 | } 339 | 340 | return $array; 341 | } 342 | 343 | /** 344 | * Returns whether a php.ini configuration is not correct. 345 | * 346 | * @return bool php.ini configuration problem? 347 | */ 348 | public function hasPhpIniConfigIssue() 349 | { 350 | foreach ($this->requirements as $req) { 351 | if (!$req->isFulfilled() && $req instanceof PhpIniRequirement) { 352 | return true; 353 | } 354 | } 355 | 356 | return false; 357 | } 358 | 359 | /** 360 | * Returns the PHP configuration file (php.ini) path. 361 | * 362 | * @return string|false php.ini file path 363 | */ 364 | public function getPhpIniConfigPath() 365 | { 366 | return get_cfg_var('cfg_file_path'); 367 | } 368 | } 369 | 370 | /** 371 | * This class specifies all requirements and optional recommendations that 372 | * are necessary to run the Symfony Standard Edition. 373 | * 374 | * @author Tobias Schultze 375 | * @author Fabien Potencier 376 | */ 377 | class SymfonyRequirements extends RequirementCollection 378 | { 379 | const REQUIRED_PHP_VERSION = '5.3.3'; 380 | 381 | /** 382 | * Constructor that initializes the requirements. 383 | */ 384 | public function __construct() 385 | { 386 | /* mandatory requirements follow */ 387 | 388 | $installedPhpVersion = phpversion(); 389 | 390 | $this->addRequirement( 391 | version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>='), 392 | sprintf('PHP version must be at least %s (%s installed)', self::REQUIRED_PHP_VERSION, $installedPhpVersion), 393 | sprintf('You are running PHP version "%s", but Symfony needs at least PHP "%s" to run. 394 | Before using Symfony, upgrade your PHP installation, preferably to the latest version.', 395 | $installedPhpVersion, self::REQUIRED_PHP_VERSION), 396 | sprintf('Install PHP %s or newer (installed version is %s)', self::REQUIRED_PHP_VERSION, $installedPhpVersion) 397 | ); 398 | 399 | $this->addRequirement( 400 | version_compare($installedPhpVersion, '5.3.16', '!='), 401 | 'PHP version must not be 5.3.16 as Symfony won\'t work properly with it', 402 | 'Install PHP 5.3.17 or newer (or downgrade to an earlier PHP version)' 403 | ); 404 | 405 | $this->addRequirement( 406 | is_dir(__DIR__.'/../vendor/composer'), 407 | 'Vendor libraries must be installed', 408 | 'Vendor libraries are missing. Install composer following instructions from http://getcomposer.org/. '. 409 | 'Then run "php composer.phar install" to install them.' 410 | ); 411 | 412 | $cacheDir = is_dir(__DIR__.'/../var/cache') ? __DIR__.'/../var/cache' : __DIR__.'/cache'; 413 | 414 | $this->addRequirement( 415 | is_writable($cacheDir), 416 | 'app/cache/ or var/cache/ directory must be writable', 417 | 'Change the permissions of either "app/cache/" or "var/cache/" directory so that the web server can write into it.' 418 | ); 419 | 420 | $logsDir = is_dir(__DIR__.'/../var/logs') ? __DIR__.'/../var/logs' : __DIR__.'/logs'; 421 | 422 | $this->addRequirement( 423 | is_writable($logsDir), 424 | 'app/logs/ or var/logs/ directory must be writable', 425 | 'Change the permissions of either "app/logs/" or "var/logs/" directory so that the web server can write into it.' 426 | ); 427 | 428 | $this->addPhpIniRequirement( 429 | 'date.timezone', true, false, 430 | 'date.timezone setting must be set', 431 | 'Set the "date.timezone" setting in php.ini* (like Europe/Paris).' 432 | ); 433 | 434 | if (version_compare($installedPhpVersion, self::REQUIRED_PHP_VERSION, '>=')) { 435 | $timezones = array(); 436 | foreach (DateTimeZone::listAbbreviations() as $abbreviations) { 437 | foreach ($abbreviations as $abbreviation) { 438 | $timezones[$abbreviation['timezone_id']] = true; 439 | } 440 | } 441 | 442 | $this->addRequirement( 443 | isset($timezones[@date_default_timezone_get()]), 444 | sprintf('Configured default timezone "%s" must be supported by your installation of PHP', @date_default_timezone_get()), 445 | 'Your default timezone is not supported by PHP. Check for typos in your php.ini file and have a look at the list of deprecated timezones at http://php.net/manual/en/timezones.others.php.' 446 | ); 447 | } 448 | 449 | $this->addRequirement( 450 | function_exists('json_encode'), 451 | 'json_encode() must be available', 452 | 'Install and enable the JSON extension.' 453 | ); 454 | 455 | $this->addRequirement( 456 | function_exists('session_start'), 457 | 'session_start() must be available', 458 | 'Install and enable the session extension.' 459 | ); 460 | 461 | $this->addRequirement( 462 | function_exists('ctype_alpha'), 463 | 'ctype_alpha() must be available', 464 | 'Install and enable the ctype extension.' 465 | ); 466 | 467 | $this->addRequirement( 468 | function_exists('token_get_all'), 469 | 'token_get_all() must be available', 470 | 'Install and enable the Tokenizer extension.' 471 | ); 472 | 473 | $this->addRequirement( 474 | function_exists('simplexml_import_dom'), 475 | 'simplexml_import_dom() must be available', 476 | 'Install and enable the SimpleXML extension.' 477 | ); 478 | 479 | if (function_exists('apc_store') && ini_get('apc.enabled')) { 480 | if (version_compare($installedPhpVersion, '5.4.0', '>=')) { 481 | $this->addRequirement( 482 | version_compare(phpversion('apc'), '3.1.13', '>='), 483 | 'APC version must be at least 3.1.13 when using PHP 5.4', 484 | 'Upgrade your APC extension (3.1.13+).' 485 | ); 486 | } else { 487 | $this->addRequirement( 488 | version_compare(phpversion('apc'), '3.0.17', '>='), 489 | 'APC version must be at least 3.0.17', 490 | 'Upgrade your APC extension (3.0.17+).' 491 | ); 492 | } 493 | } 494 | 495 | $this->addPhpIniRequirement('detect_unicode', false); 496 | 497 | if (extension_loaded('suhosin')) { 498 | $this->addPhpIniRequirement( 499 | 'suhosin.executor.include.whitelist', 500 | create_function('$cfgValue', 'return false !== stripos($cfgValue, "phar");'), 501 | false, 502 | 'suhosin.executor.include.whitelist must be configured correctly in php.ini', 503 | 'Add "phar" to suhosin.executor.include.whitelist in php.ini*.' 504 | ); 505 | } 506 | 507 | if (extension_loaded('xdebug')) { 508 | $this->addPhpIniRequirement( 509 | 'xdebug.show_exception_trace', false, true 510 | ); 511 | 512 | $this->addPhpIniRequirement( 513 | 'xdebug.scream', false, true 514 | ); 515 | 516 | $this->addPhpIniRecommendation( 517 | 'xdebug.max_nesting_level', 518 | create_function('$cfgValue', 'return $cfgValue > 100;'), 519 | true, 520 | 'xdebug.max_nesting_level should be above 100 in php.ini', 521 | 'Set "xdebug.max_nesting_level" to e.g. "250" in php.ini* to stop Xdebug\'s infinite recursion protection erroneously throwing a fatal error in your project.' 522 | ); 523 | } 524 | 525 | $pcreVersion = defined('PCRE_VERSION') ? (float) PCRE_VERSION : null; 526 | 527 | $this->addRequirement( 528 | null !== $pcreVersion, 529 | 'PCRE extension must be available', 530 | 'Install the PCRE extension (version 8.0+).' 531 | ); 532 | 533 | if (extension_loaded('mbstring')) { 534 | $this->addPhpIniRequirement( 535 | 'mbstring.func_overload', 536 | create_function('$cfgValue', 'return (int) $cfgValue === 0;'), 537 | true, 538 | 'string functions should not be overloaded', 539 | 'Set "mbstring.func_overload" to 0 in php.ini* to disable function overloading by the mbstring extension.' 540 | ); 541 | } 542 | 543 | /* optional recommendations follow */ 544 | 545 | if (file_exists(__DIR__.'/../vendor/composer')) { 546 | require_once __DIR__.'/../vendor/autoload.php'; 547 | 548 | try { 549 | $r = new \ReflectionClass('Sensio\Bundle\DistributionBundle\SensioDistributionBundle'); 550 | 551 | $contents = file_get_contents(dirname($r->getFileName()).'/Resources/skeleton/app/SymfonyRequirements.php'); 552 | } catch (\ReflectionException $e) { 553 | $contents = ''; 554 | } 555 | $this->addRecommendation( 556 | file_get_contents(__FILE__) === $contents, 557 | 'Requirements file should be up-to-date', 558 | 'Your requirements file is outdated. Run composer install and re-check your configuration.' 559 | ); 560 | } 561 | 562 | $this->addRecommendation( 563 | version_compare($installedPhpVersion, '5.3.4', '>='), 564 | 'You should use at least PHP 5.3.4 due to PHP bug #52083 in earlier versions', 565 | 'Your project might malfunction randomly due to PHP bug #52083 ("Notice: Trying to get property of non-object"). Install PHP 5.3.4 or newer.' 566 | ); 567 | 568 | $this->addRecommendation( 569 | version_compare($installedPhpVersion, '5.3.8', '>='), 570 | 'When using annotations you should have at least PHP 5.3.8 due to PHP bug #55156', 571 | 'Install PHP 5.3.8 or newer if your project uses annotations.' 572 | ); 573 | 574 | $this->addRecommendation( 575 | version_compare($installedPhpVersion, '5.4.0', '!='), 576 | 'You should not use PHP 5.4.0 due to the PHP bug #61453', 577 | 'Your project might not work properly due to the PHP bug #61453 ("Cannot dump definitions which have method calls"). Install PHP 5.4.1 or newer.' 578 | ); 579 | 580 | $this->addRecommendation( 581 | version_compare($installedPhpVersion, '5.4.11', '>='), 582 | 'When using the logout handler from the Symfony Security Component, you should have at least PHP 5.4.11 due to PHP bug #63379 (as a workaround, you can also set invalidate_session to false in the security logout handler configuration)', 583 | 'Install PHP 5.4.11 or newer if your project uses the logout handler from the Symfony Security Component.' 584 | ); 585 | 586 | $this->addRecommendation( 587 | (version_compare($installedPhpVersion, '5.3.18', '>=') && version_compare($installedPhpVersion, '5.4.0', '<')) 588 | || 589 | version_compare($installedPhpVersion, '5.4.8', '>='), 590 | 'You should use PHP 5.3.18+ or PHP 5.4.8+ to always get nice error messages for fatal errors in the development environment due to PHP bug #61767/#60909', 591 | 'Install PHP 5.3.18+ or PHP 5.4.8+ if you want nice error messages for all fatal errors in the development environment.' 592 | ); 593 | 594 | if (null !== $pcreVersion) { 595 | $this->addRecommendation( 596 | $pcreVersion >= 8.0, 597 | sprintf('PCRE extension should be at least version 8.0 (%s installed)', $pcreVersion), 598 | 'PCRE 8.0+ is preconfigured in PHP since 5.3.2 but you are using an outdated version of it. Symfony probably works anyway but it is recommended to upgrade your PCRE extension.' 599 | ); 600 | } 601 | 602 | $this->addRecommendation( 603 | class_exists('DomDocument'), 604 | 'PHP-DOM and PHP-XML modules should be installed', 605 | 'Install and enable the PHP-DOM and the PHP-XML modules.' 606 | ); 607 | 608 | $this->addRecommendation( 609 | function_exists('mb_strlen'), 610 | 'mb_strlen() should be available', 611 | 'Install and enable the mbstring extension.' 612 | ); 613 | 614 | $this->addRecommendation( 615 | function_exists('iconv'), 616 | 'iconv() should be available', 617 | 'Install and enable the iconv extension.' 618 | ); 619 | 620 | $this->addRecommendation( 621 | function_exists('utf8_decode'), 622 | 'utf8_decode() should be available', 623 | 'Install and enable the XML extension.' 624 | ); 625 | 626 | $this->addRecommendation( 627 | function_exists('filter_var'), 628 | 'filter_var() should be available', 629 | 'Install and enable the filter extension.' 630 | ); 631 | 632 | if (!defined('PHP_WINDOWS_VERSION_BUILD')) { 633 | $this->addRecommendation( 634 | function_exists('posix_isatty'), 635 | 'posix_isatty() should be available', 636 | 'Install and enable the php_posix extension (used to colorize the CLI output).' 637 | ); 638 | } 639 | 640 | $this->addRecommendation( 641 | class_exists('Locale'), 642 | 'intl extension should be available', 643 | 'Install and enable the intl extension (used for validators).' 644 | ); 645 | 646 | if (extension_loaded('intl')) { 647 | // in some WAMP server installations, new Collator() returns null 648 | $this->addRecommendation( 649 | null !== new Collator('fr_FR'), 650 | 'intl extension should be correctly configured', 651 | 'The intl extension does not behave properly. This problem is typical on PHP 5.3.X x64 WIN builds.' 652 | ); 653 | 654 | // check for compatible ICU versions (only done when you have the intl extension) 655 | if (defined('INTL_ICU_VERSION')) { 656 | $version = INTL_ICU_VERSION; 657 | } else { 658 | $reflector = new ReflectionExtension('intl'); 659 | 660 | ob_start(); 661 | $reflector->info(); 662 | $output = strip_tags(ob_get_clean()); 663 | 664 | preg_match('/^ICU version +(?:=> )?(.*)$/m', $output, $matches); 665 | $version = $matches[1]; 666 | } 667 | 668 | $this->addRecommendation( 669 | version_compare($version, '4.0', '>='), 670 | 'intl ICU version should be at least 4+', 671 | 'Upgrade your intl extension with a newer ICU version (4+).' 672 | ); 673 | 674 | $this->addPhpIniRecommendation( 675 | 'intl.error_level', 676 | create_function('$cfgValue', 'return (int) $cfgValue === 0;'), 677 | true, 678 | 'intl.error_level should be 0 in php.ini', 679 | 'Set "intl.error_level" to "0" in php.ini* to inhibit the messages when an error occurs in ICU functions.' 680 | ); 681 | } 682 | 683 | $accelerator = 684 | (extension_loaded('eaccelerator') && ini_get('eaccelerator.enable')) 685 | || 686 | (extension_loaded('apc') && ini_get('apc.enabled')) 687 | || 688 | (extension_loaded('Zend Optimizer+') && ini_get('zend_optimizerplus.enable')) 689 | || 690 | (extension_loaded('Zend OPcache') && ini_get('opcache.enable')) 691 | || 692 | (extension_loaded('xcache') && ini_get('xcache.cacher')) 693 | || 694 | (extension_loaded('wincache') && ini_get('wincache.ocenabled')) 695 | ; 696 | 697 | $this->addRecommendation( 698 | $accelerator, 699 | 'a PHP accelerator should be installed', 700 | 'Install and/or enable a PHP accelerator (highly recommended).' 701 | ); 702 | 703 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 704 | $this->addRecommendation( 705 | $this->getRealpathCacheSize() > 1000, 706 | 'realpath_cache_size should be above 1024 in php.ini', 707 | 'Set "realpath_cache_size" to e.g. "1024" in php.ini* to improve performance on windows.' 708 | ); 709 | } 710 | 711 | $this->addPhpIniRecommendation('short_open_tag', false); 712 | 713 | $this->addPhpIniRecommendation('magic_quotes_gpc', false, true); 714 | 715 | $this->addPhpIniRecommendation('register_globals', false, true); 716 | 717 | $this->addPhpIniRecommendation('session.auto_start', false); 718 | 719 | $this->addRecommendation( 720 | class_exists('PDO'), 721 | 'PDO should be installed', 722 | 'Install PDO (mandatory for Doctrine).' 723 | ); 724 | 725 | if (class_exists('PDO')) { 726 | $drivers = PDO::getAvailableDrivers(); 727 | $this->addRecommendation( 728 | count($drivers) > 0, 729 | sprintf('PDO should have some drivers installed (currently available: %s)', count($drivers) ? implode(', ', $drivers) : 'none'), 730 | 'Install PDO drivers (mandatory for Doctrine).' 731 | ); 732 | } 733 | } 734 | 735 | /** 736 | * Loads realpath_cache_size from php.ini and converts it to int. 737 | * 738 | * (e.g. 16k is converted to 16384 int) 739 | * 740 | * @return int 741 | */ 742 | protected function getRealpathCacheSize() 743 | { 744 | $size = ini_get('realpath_cache_size'); 745 | $size = trim($size); 746 | $unit = strtolower(substr($size, -1, 1)); 747 | switch ($unit) { 748 | case 'g': 749 | return $size * 1024 * 1024 * 1024; 750 | case 'm': 751 | return $size * 1024 * 1024; 752 | case 'k': 753 | return $size * 1024; 754 | default: 755 | return (int) $size; 756 | } 757 | } 758 | } 759 | -------------------------------------------------------------------------------- /app/autoload.php: -------------------------------------------------------------------------------- 1 | getPhpIniConfigPath(); 8 | 9 | echo_title('Symfony2 Requirements Checker'); 10 | 11 | echo '> PHP is using the following php.ini file:'.PHP_EOL; 12 | if ($iniPath) { 13 | echo_style('green', ' '.$iniPath); 14 | } else { 15 | echo_style('warning', ' WARNING: No configuration file (php.ini) used by PHP!'); 16 | } 17 | 18 | echo PHP_EOL.PHP_EOL; 19 | 20 | echo '> Checking Symfony requirements:'.PHP_EOL.' '; 21 | 22 | $messages = array(); 23 | foreach ($symfonyRequirements->getRequirements() as $req) { 24 | /** @var $req Requirement */ 25 | if ($helpText = get_error_message($req, $lineSize)) { 26 | echo_style('red', 'E'); 27 | $messages['error'][] = $helpText; 28 | } else { 29 | echo_style('green', '.'); 30 | } 31 | } 32 | 33 | $checkPassed = empty($messages['error']); 34 | 35 | foreach ($symfonyRequirements->getRecommendations() as $req) { 36 | if ($helpText = get_error_message($req, $lineSize)) { 37 | echo_style('yellow', 'W'); 38 | $messages['warning'][] = $helpText; 39 | } else { 40 | echo_style('green', '.'); 41 | } 42 | } 43 | 44 | if ($checkPassed) { 45 | echo_block('success', 'OK', 'Your system is ready to run Symfony2 projects', true); 46 | } else { 47 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony2 projects', true); 48 | 49 | echo_title('Fix the following mandatory requirements', 'red'); 50 | 51 | foreach ($messages['error'] as $helpText) { 52 | echo ' * '.$helpText.PHP_EOL; 53 | } 54 | } 55 | 56 | if (!empty($messages['warning'])) { 57 | echo_title('Optional recommendations to improve your setup', 'yellow'); 58 | 59 | foreach ($messages['warning'] as $helpText) { 60 | echo ' * '.$helpText.PHP_EOL; 61 | } 62 | } 63 | 64 | echo PHP_EOL; 65 | echo_style('title', 'Note'); 66 | echo ' The command console could use a different php.ini file'.PHP_EOL; 67 | echo_style('title', '~~~~'); 68 | echo ' than the one used with your web server. To be on the'.PHP_EOL; 69 | echo ' safe side, please check the requirements from your web'.PHP_EOL; 70 | echo ' server using the '; 71 | echo_style('yellow', 'web/config.php'); 72 | echo ' script.'.PHP_EOL; 73 | echo PHP_EOL; 74 | 75 | exit($checkPassed ? 0 : 1); 76 | 77 | function get_error_message(Requirement $requirement, $lineSize) 78 | { 79 | if ($requirement->isFulfilled()) { 80 | return; 81 | } 82 | 83 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL; 84 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL; 85 | 86 | return $errorMessage; 87 | } 88 | 89 | function echo_title($title, $style = null) 90 | { 91 | $style = $style ?: 'title'; 92 | 93 | echo PHP_EOL; 94 | echo_style($style, $title.PHP_EOL); 95 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL); 96 | echo PHP_EOL; 97 | } 98 | 99 | function echo_style($style, $message) 100 | { 101 | // ANSI color codes 102 | $styles = array( 103 | 'reset' => "\033[0m", 104 | 'red' => "\033[31m", 105 | 'green' => "\033[32m", 106 | 'yellow' => "\033[33m", 107 | 'error' => "\033[37;41m", 108 | 'success' => "\033[37;42m", 109 | 'title' => "\033[34m", 110 | ); 111 | $supports = has_color_support(); 112 | 113 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); 114 | } 115 | 116 | function echo_block($style, $title, $message) 117 | { 118 | $message = ' '.trim($message).' '; 119 | $width = strlen($message); 120 | 121 | echo PHP_EOL.PHP_EOL; 122 | 123 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 124 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT).PHP_EOL); 125 | echo_style($style, str_pad($message, $width, ' ', STR_PAD_RIGHT).PHP_EOL); 126 | echo_style($style, str_repeat(' ', $width).PHP_EOL); 127 | } 128 | 129 | function has_color_support() 130 | { 131 | static $support; 132 | 133 | if (null === $support) { 134 | if (DIRECTORY_SEPARATOR == '\\') { 135 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); 136 | } else { 137 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT); 138 | } 139 | } 140 | 141 | return $support; 142 | } 143 | -------------------------------------------------------------------------------- /app/config/config.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: parameters.yml } 3 | - { resource: security.yml } 4 | - { resource: services.yml } 5 | 6 | # Put parameters here that don't need to change on each machine where the app is deployed 7 | # http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 8 | parameters: 9 | locale: en 10 | 11 | framework: 12 | #esi: ~ 13 | #translator: { fallbacks: ["%locale%"] } 14 | secret: "%secret%" 15 | router: 16 | resource: "%kernel.root_dir%/config/routing.yml" 17 | strict_requirements: ~ 18 | form: ~ 19 | csrf_protection: ~ 20 | validation: { enable_annotations: true } 21 | #serializer: { enable_annotations: true } 22 | templating: 23 | engines: ['twig'] 24 | #assets_version: SomeVersionScheme 25 | default_locale: "%locale%" 26 | trusted_hosts: ~ 27 | trusted_proxies: ~ 28 | session: 29 | # handler_id set to null will use default session handler from php.ini 30 | handler_id: ~ 31 | fragments: ~ 32 | http_method_override: true 33 | 34 | # Twig Configuration 35 | twig: 36 | debug: "%kernel.debug%" 37 | strict_variables: "%kernel.debug%" 38 | 39 | # Assetic Configuration 40 | assetic: 41 | debug: "%kernel.debug%" 42 | use_controller: false 43 | bundles: [ ] 44 | #java: /usr/bin/java 45 | filters: 46 | cssrewrite: ~ 47 | #closure: 48 | # jar: "%kernel.root_dir%/Resources/java/compiler.jar" 49 | #yui_css: 50 | # jar: "%kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar" 51 | 52 | # Doctrine Configuration 53 | doctrine: 54 | dbal: 55 | driver: pdo_mysql 56 | host: "%database_host%" 57 | port: "%database_port%" 58 | dbname: "%database_name%" 59 | user: "%database_user%" 60 | password: "%database_password%" 61 | charset: UTF8 62 | # if using pdo_sqlite as your database driver: 63 | # 1. add the path in parameters.yml 64 | # e.g. database_path: "%kernel.root_dir%/data/data.db3" 65 | # 2. Uncomment database_path in parameters.yml.dist 66 | # 3. Uncomment next line: 67 | # path: "%database_path%" 68 | 69 | orm: 70 | auto_generate_proxy_classes: "%kernel.debug%" 71 | naming_strategy: doctrine.orm.naming_strategy.underscore 72 | auto_mapping: true 73 | 74 | # Swiftmailer Configuration 75 | swiftmailer: 76 | transport: "%mailer_transport%" 77 | host: "%mailer_host%" 78 | username: "%mailer_user%" 79 | password: "%mailer_password%" 80 | spool: { type: memory } 81 | -------------------------------------------------------------------------------- /app/config/config_dev.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config.yml } 3 | 4 | framework: 5 | router: 6 | resource: "%kernel.root_dir%/config/routing_dev.yml" 7 | strict_requirements: true 8 | profiler: { only_exceptions: false } 9 | 10 | web_profiler: 11 | toolbar: true 12 | intercept_redirects: false 13 | 14 | monolog: 15 | handlers: 16 | main: 17 | type: stream 18 | path: "%kernel.logs_dir%/%kernel.environment%.log" 19 | level: debug 20 | console: 21 | type: console 22 | bubble: false 23 | verbosity_levels: 24 | VERBOSITY_VERBOSE: INFO 25 | VERBOSITY_VERY_VERBOSE: DEBUG 26 | channels: ["!doctrine"] 27 | console_very_verbose: 28 | type: console 29 | bubble: false 30 | verbosity_levels: 31 | VERBOSITY_VERBOSE: NOTICE 32 | VERBOSITY_VERY_VERBOSE: NOTICE 33 | VERBOSITY_DEBUG: DEBUG 34 | channels: ["doctrine"] 35 | # uncomment to get logging in your browser 36 | # you may have to allow bigger header sizes in your Web server configuration 37 | #firephp: 38 | # type: firephp 39 | # level: info 40 | #chromephp: 41 | # type: chromephp 42 | # level: info 43 | 44 | assetic: 45 | use_controller: true 46 | 47 | #swiftmailer: 48 | # delivery_address: me@example.com 49 | -------------------------------------------------------------------------------- /app/config/config_prod.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config.yml } 3 | 4 | #framework: 5 | # validation: 6 | # cache: validator.mapping.cache.apc 7 | # serializer: 8 | # cache: serializer.mapping.cache.apc 9 | 10 | #doctrine: 11 | # orm: 12 | # metadata_cache_driver: apc 13 | # result_cache_driver: apc 14 | # query_cache_driver: apc 15 | 16 | monolog: 17 | handlers: 18 | main: 19 | type: fingers_crossed 20 | action_level: error 21 | handler: nested 22 | nested: 23 | type: stream 24 | path: "%kernel.logs_dir%/%kernel.environment%.log" 25 | level: debug 26 | console: 27 | type: console 28 | -------------------------------------------------------------------------------- /app/config/config_test.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: config_dev.yml } 3 | 4 | framework: 5 | test: ~ 6 | session: 7 | storage_id: session.storage.mock_file 8 | profiler: 9 | collect: false 10 | 11 | web_profiler: 12 | toolbar: false 13 | intercept_redirects: false 14 | 15 | swiftmailer: 16 | disable_delivery: true 17 | -------------------------------------------------------------------------------- /app/config/parameters.yml: -------------------------------------------------------------------------------- 1 | # This file is auto-generated during the composer install 2 | parameters: 3 | database_host: 127.0.0.1 4 | database_port: null 5 | database_name: guard_tutorial 6 | database_user: root 7 | database_password: null 8 | mailer_transport: smtp 9 | mailer_host: 127.0.0.1 10 | mailer_user: null 11 | mailer_password: null 12 | secret: a3250d79a622c2f1d034943d9ff4397d500552dd 13 | -------------------------------------------------------------------------------- /app/config/parameters.yml.dist: -------------------------------------------------------------------------------- 1 | # This file is a "template" of what your parameters.yml file should look like 2 | # Set parameters here that may be different on each deployment target of the app, e.g. development, staging, production. 3 | # http://symfony.com/doc/current/best_practices/configuration.html#infrastructure-related-configuration 4 | parameters: 5 | database_host: 127.0.0.1 6 | database_port: ~ 7 | database_name: symfony 8 | database_user: root 9 | database_password: ~ 10 | # You should uncomment this if you want use pdo_sqlite 11 | # database_path: "%kernel.root_dir%/data.db3" 12 | 13 | mailer_transport: smtp 14 | mailer_host: 127.0.0.1 15 | mailer_user: ~ 16 | mailer_password: ~ 17 | 18 | # A secret key that's used to generate certain security-related tokens 19 | secret: ThisTokenIsNotSoSecretChangeIt 20 | -------------------------------------------------------------------------------- /app/config/routing.yml: -------------------------------------------------------------------------------- 1 | app: 2 | resource: "@AppBundle/Controller/" 3 | type: annotation 4 | -------------------------------------------------------------------------------- /app/config/routing_dev.yml: -------------------------------------------------------------------------------- 1 | _wdt: 2 | resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" 3 | prefix: /_wdt 4 | 5 | _profiler: 6 | resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" 7 | prefix: /_profiler 8 | 9 | _configurator: 10 | resource: "@SensioDistributionBundle/Resources/config/routing/webconfigurator.xml" 11 | prefix: /_configurator 12 | 13 | _errors: 14 | resource: "@TwigBundle/Resources/config/routing/errors.xml" 15 | prefix: /_error 16 | 17 | _main: 18 | resource: routing.yml 19 | -------------------------------------------------------------------------------- /app/config/security.yml: -------------------------------------------------------------------------------- 1 | security: 2 | 3 | providers: 4 | in_memory: 5 | memory: ~ 6 | 7 | firewalls: 8 | dev: 9 | pattern: ^/(_(profiler|wdt|error)|css|images|js)/ 10 | security: false 11 | 12 | main: 13 | anonymous: ~ 14 | -------------------------------------------------------------------------------- /app/config/services.yml: -------------------------------------------------------------------------------- 1 | # Learn more about services, parameters and containers at 2 | # http://symfony.com/doc/current/book/service_container.html 3 | parameters: 4 | # parameter_name: value 5 | 6 | services: 7 | # service_name: 8 | # class: AppBundle\Directory\ClassName 9 | # arguments: ["@another_service_name", "plain_value", "%parameter_name%"] 10 | -------------------------------------------------------------------------------- /app/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); 19 | $debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod'; 20 | 21 | if ($debug) { 22 | Debug::enable(); 23 | } 24 | 25 | $kernel = new AppKernel($env, $debug); 26 | $application = new Application($kernel); 27 | $application->run($input); 28 | -------------------------------------------------------------------------------- /app/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/app/logs/.gitkeep -------------------------------------------------------------------------------- /app/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | ../src/*/*Bundle/Tests 13 | ../src/*/Bundle/*Bundle/Tests 14 | ../src/*Bundle/Tests 15 | 16 | 17 | 18 | 23 | 24 | 25 | 26 | ../src 27 | 28 | ../src/*Bundle/Resources 29 | ../src/*Bundle/Tests 30 | ../src/*/*Bundle/Resources 31 | ../src/*/*Bundle/Tests 32 | ../src/*/Bundle/*Bundle/Resources 33 | ../src/*/Bundle/*Bundle/Tests 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weaverryan/guard.tutorial", 3 | "license": "proprietary", 4 | "type": "project", 5 | "autoload": { 6 | "psr-4": { 7 | "": "src/", 8 | "SymfonyStandard\\": "app/SymfonyStandard/" 9 | } 10 | }, 11 | "require": { 12 | "php": ">=5.3.9", 13 | "symfony/symfony": "2.8.*", 14 | "doctrine/orm": "~2.2,>=2.2.3,<2.5", 15 | "doctrine/dbal": "<2.5", 16 | "doctrine/doctrine-bundle": "~1.4", 17 | "symfony/assetic-bundle": "~2.3", 18 | "symfony/swiftmailer-bundle": "~2.3", 19 | "symfony/monolog-bundle": "~2.4", 20 | "sensio/distribution-bundle": "~4.0", 21 | "sensio/framework-extra-bundle": "~3.0,>=3.0.2", 22 | "incenteev/composer-parameter-handler": "~2.0", 23 | "doctrine/doctrine-fixtures-bundle": "^2.2" 24 | }, 25 | "require-dev": { 26 | "sensio/generator-bundle": "~2.3" 27 | }, 28 | "scripts": { 29 | "post-root-package-install": [ 30 | "SymfonyStandard\\Composer::hookRootPackageInstall" 31 | ], 32 | "post-install-cmd": [ 33 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 34 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 35 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 36 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 37 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 38 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles", 39 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 40 | ], 41 | "post-update-cmd": [ 42 | "Incenteev\\ParameterHandler\\ScriptHandler::buildParameters", 43 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::buildBootstrap", 44 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::clearCache", 45 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installAssets", 46 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::installRequirementsFile", 47 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::removeSymfonyStandardFiles", 48 | "Sensio\\Bundle\\DistributionBundle\\Composer\\ScriptHandler::prepareDeploymentTarget" 49 | ] 50 | }, 51 | "config": { 52 | "bin-dir": "bin" 53 | }, 54 | "extra": { 55 | "symfony-app-dir": "app", 56 | "symfony-web-dir": "web", 57 | "symfony-assets-install": "relative", 58 | "incenteev-parameters": { 59 | "file": "app/config/parameters.yml" 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /knpu/api-token.md: -------------------------------------------------------------------------------- 1 | # How to Authenticate via an API Token 2 | 3 | Suppose you want to have an API where users authenticate by sending an `X-TOKEN` 4 | header. With Guard, this is one of the *easiest* things you can setup. 5 | 6 | For this example, we have a `User` entity that as an `$apiToken` property that's 7 | auto-generated for every user when they register: 8 | 9 | [[[ code('b59c9fbd98') ]]] 10 | 11 | But your setup can look however you want: an independent `ApiToken` entity that relates 12 | to your `User`, no `User` entity at all, or api tokens that are validated in some 13 | other way entirely. 14 | 15 | ## Installing Guard 16 | 17 | Read the short [Installation](install) chapter to make sure you've got the 18 | bundle installed and enabled. 19 | 20 | ## Creating an Authenticator 21 | 22 | In Guard, the whole authentication process - reading the `X-TOKEN` header value, 23 | validating it, returning an error response, etc - is handled in a single class called 24 | an "Authenticator". Your authenticator can be as crazy as you want, as long as it 25 | implements [KnpU\Guard\GuardAuthenticatorInterface](https://github.com/knpuniversity/KnpUGuard/blob/master/src/GuardAuthenticatorInterface.php). 26 | 27 | Most of the time, you can extend a convenience class called `AbstractGuardAuthenticator`. 28 | Create a new `ApiTokenAuthenticator` class, make it extend this class, and add all 29 | the necessary methods: 30 | 31 | [[[ code('30cdaf1984') ]]] 32 | 33 | Your mission: fill in each method. We'll get to that in a second. 34 | 35 | But to fill on those methods, we'll need to query the database. Let's pass the Doctrine 36 | `EntityManager` into our authenticator: 37 | 38 | [[[ code('928d547573') ]]] 39 | 40 | ## Registering your Authenticator 41 | 42 | Before filling in the methods, let's tell Symfony about our fancy new authenticator. 43 | First, register it as a service: 44 | 45 | [[[ code('4ea155b7ce') ]]] 46 | 47 | Next, update your `security.yml` file to use the new service: 48 | 49 | [[[ code('4bf4efdda0') ]]] 50 | 51 | Your firewall (called `main` here) can look however you want, as long as it has a 52 | `knpu_guard` section under it with an `authenticators` key that includes the service 53 | name that you setup a second ago (`app.api_token_authenticator` in my example). 54 | 55 | ***TIP 56 | The other authenticator - `app.form_login_authenticator` - is for my login form. 57 | If you don't need to *also* allow users to login via a form, then you can remove 58 | this. The `entry_point` option is only needed if you have multiple authenticators. 59 | See [How can I use Multiple Authenticators?](multiple-authenticators). 60 | *** 61 | 62 | 63 | 64 | ## Filling in the Authenticator Methods 65 | 66 | Your authenticator is now being used by Symfony. So let's fill in each method: 67 | 68 | ### getCredentials() 69 | 70 | [[[ code('53dcfe88ec') ]]] 71 | 72 | The `getCredentials()` method is called on **every single request** and its job is 73 | to fetch the API token and return it. 74 | 75 | Well, that's pretty simple. From here, there are 3 possibilities: 76 | 77 | # | Conditions | Result | Next Step 78 | ----- | ------------------------------------------- | -------------------------- | ---------- 79 | A) | Return non-null value | Authentication continues | [getUser()](#getUser) 80 | B) | Return null + endpoint requires auth | Auth skipped, 401 response | [start()](#start) 81 | C) | Return null+ endpoint does not require auth | Auth skipped, user is anon | nothing 82 | 83 | **A)** The `X-TOKEN` header exists, so this returns a non-null value. In this case, 84 | [getUser()](#getUser) is called next. 85 | 86 | **B)** The `X-TOKEN` header is missing, so this returns `null`. But, your application 87 | *does* require authentication (e.g. via `access_control` or an `isGranted()` call). 88 | In this case, see [start()](#start). 89 | 90 | **C)** The `X-TOKEN` header is missing, so this returns `null`. But the user is accessing 91 | an endpoint that does *not* require authentication. In this case, the request continues 92 | anonymously - no other methods are called on the authenticator. 93 | 94 | 95 | 96 | ### getUser() 97 | 98 | If `getCredentials()` returns a non-null value, then `getUser()` is called next. 99 | Its job is simple: return a the user (an object implementing `UserInterface`): 100 | 101 | [[[ code('47f86a1c25') ]]] 102 | 103 | The `$credentials` argument is whatever you returned from `getCredentials()`, and 104 | the `$userProvider` is whatever you've configured in security.yml under the 105 | [providers](#security-providers) key. 106 | 107 | You can choose to use your provider, or you can do something else entirely to load 108 | the user. In this case, we're doing a simple query on the `User` entity to see which 109 | User (if any) has this `apiToken` value. 110 | 111 | From here, there are 2 possibilities: 112 | 113 | # | Conditions | Result | Next Step 114 | ----- | ---------------------------------------------- | ------------------------- | ---------- 115 | A) | Return a User | Authentication continues | [checkCredentials()](#checkCredentials) 116 | B) | Return null or throw AuthenticationException | Authentication fails | [onAuthenticationFailure()](#onAuthenticationFailure) 117 | 118 | A) If you successfully return a `User` object, then, [checkCredentials()](#checkCredentials) 119 | is called next. 120 | 121 | B) If you return `null` or throw any `Symfony\Component\Security\Core\Exception\AuthenticationException`, 122 | authentication will fail and [onAuthenticationFailure()](#onAuthenticationFailure) 123 | is called next. 124 | 125 | 126 | 127 | ### checkCredentials() 128 | 129 | If you return a user from `getUser()`, then `checkCredentials()` is called next. 130 | Here, you can do any additional checks for the validity of the token - or anything 131 | else you can think of. In this example, we're doing nothing: 132 | 133 | [[[ code('9f38945da4') ]]] 134 | 135 | Like before, `$credentials` is whatever you returned from `getCredentials()`. And 136 | now, the `$user` argument is what you just returned from `getUser()`. 137 | 138 | From here, there are 2 possibilities: 139 | 140 | # | Conditions | Result | Next Step 141 | ----- | ---------------------------------------------------------- | ------------------------- | ------------- 142 | A) | do anything *except* throwing an `AuthenticationException` | Authentication successful | [onAuthenticationSuccess()](#onAuthenticationSuccess) 143 | B) | Throw any type of `AuthenticationException` | Authentication fails | [onAuthenticationFailure()](#onAuthenticationFailure) 144 | 145 | A) If you *don't* throw an exception, congrats! You're authenticated! In this case, 146 | [onAuthenticationSuccess()](#onAuthenticationSuccess) is called next. 147 | 148 | B) If you perform extra checks and throw any `Symfony\Component\Security\Core\Exception\AuthenticationException`, 149 | authentication will fail and [onAuthenticationFailure()](#onAuthenticationFailure) 150 | is called next. 151 | 152 | 153 | 154 | ### onAuthenticationSuccess 155 | 156 | Your user is authenticated! Amazing! At this point, in an API, you usually want to 157 | simply let the request continue like normal: 158 | 159 | [[[ code('1f5d61727e') ]]] 160 | 161 | If you return `null` from this method: the request continues to process through Symfony 162 | like normal (only now, the request is authenticated). 163 | 164 | Alternatively, you could return a `Response` object here. If you did, that `Response` 165 | would be returned to the client directly, without executing the controller for this 166 | request. For an API, that's probably not what you want. 167 | 168 | 169 | 170 | ### onAuthenticationFailure 171 | 172 | If you return `null` from `getUser()` or throw any `AuthenticationException` from 173 | `getUser()` or `checkCredentials()`, then you'll end up here. Your job is to create 174 | a `Response` that should be sent back to the user to tell them what went wrong: 175 | 176 | [[[ code('e8b49612cd') ]]] 177 | 178 | In this case, we'll return a 403 Forbidden JSON response with a message about what 179 | went wrong. The `$exception` argument is the actual `AuthenticationException` that 180 | was thrown. It has a `getMessageKey()` method that contains a safe message about 181 | the authentication problem. 182 | 183 | This Response will be sent back to the client - the controller will never be executed 184 | for this request. 185 | 186 | 187 | 188 | ### start() 189 | 190 | This method is called if an anomymous user accesses en endpoint that requires authentication. 191 | For our example, this would happen if the `X-TOKEN` header is empty (and so `getCredentials()`) 192 | returns `null`. Our job here is to return a Response that instructs the user that 193 | they need to re-send the request with authentication information (i.e. the `X-TOKEN` 194 | header): 195 | 196 | [[[ code('f1899c871e') ]]] 197 | 198 | In this case, we'll return a 401 Unauthorized JSON response. 199 | 200 | ### supportsRememberMe 201 | 202 | This method is required for all authenticators. If `true`, then the authenticator 203 | will work together with the `remember_me` functionality on your firewall, if you 204 | have it configured. Obviously, for an API, we don't need remember me functionality: 205 | 206 | [[[ code('ead9d11584') ]]] 207 | 208 | ## Testing your Endpoint 209 | 210 | Yes! API token authentication is all setup. Let's test it with a simple script. 211 | 212 | First, install [Guzzle](http://guzzle.readthedocs.org/en/latest/overview.html#installation): 213 | 214 | ```bash 215 | composer require guzzlehttp/guzzle:~6.0 216 | ``` 217 | 218 | Next, create a little "play" file at the root of your project that will make a request 219 | to our app. This assume your web server is running on `localhost:8000`: 220 | 221 | [[[ code('6ddb4b8b1b') ]]] 222 | 223 | In our app, the `anna_admin` user in the database has an `apiToken` of `ABCD1234`. 224 | In other words, this *should* work. Try it out from the command line: 225 | 226 | ```bash 227 | php testAuth.php 228 | ``` 229 | 230 | If you see a 200 status code with response of "It works!"... well, um... it works! 231 | The `/secure` controller requires authentication. Now try changing the token value 232 | (or removing it entirely) to see our error responses. 233 | -------------------------------------------------------------------------------- /knpu/error-messages.md: -------------------------------------------------------------------------------- 1 | # Customizing Authentication Failure Messages 2 | 3 | Authentication can fail for a lot of reasons: an invalid username, bad password, 4 | locked account, etc, etc. And whether we're building a login form or an API, you 5 | need to give your users the *best* possible error message so they know how to fix 6 | things. If your error message is "Authentication error" when they type in a bad password, 7 | you're doing it wrong. 8 | 9 | ## How and Where to Fail Authentication 10 | 11 | Authentication can fail inside your authenticator in any of these 3 functions: 12 | 13 | * `getCredentials()` 14 | * `getUser()` 15 | * `checkCredentials()` 16 | 17 | Causing an authentication failure is easy: simply throw *any* instance of 18 | Symfony's `Symfony\Component\Security\Core\Exception\AuthenticationException`. In 19 | fact, if you return `null` from `getUser()`, Guard automatically throws a 20 | [UsernameNotFoundException](https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/Security/Core/Exception/UsernameNotFoundException.php), 21 | which extends `AuthenticationException`. 22 | 23 | ## Controlling the Message with CustomAuthenticationException 24 | 25 | Any class that extends `AuthenticationException` has a hardcoded message that it 26 | causes. Here are some examples: 27 | 28 | Class | Message 29 | ------ | --------------------- 30 | [UsernameNotFoundException](https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/Security/Core/Exception/UsernameNotFoundException.php) | `Username could not be found.` 31 | [BadCredentialsException](https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/Security/Core/Exception/BadCredentialsException.php) | `Invalid credentials.` 32 | [AccountExpiredException](https://github.com/symfony/symfony/blob/2.8/src/Symfony/Component/Security/Core/Exception/AccountExpiredException.php) | `Account has expired.` 33 | 34 | Unfortunately, you *cannot* change these messages dynamically. In normal Symfony, 35 | you either need to translate these message or create a *new* exception class that 36 | extends `AuthenticationException` and customize your message there. 37 | 38 | But wait! Guard comes with a class to help: [CustomAuthenticationException](https://github.com/knpuniversity/KnpUGuard/blob/master/src/Exception/CustomAuthenticationException.php). 39 | Use it inside any of the 3 methods above to customize your error message: 40 | 41 | [[[ code('90c0f3d190') ]]] 42 | 43 | ## Using the Message in onAuthenticationFailure 44 | 45 | Whenever any type of `AuthenticationException` is thrown in the process, the 46 | `onAuthenticationFailure()` method is called on your authenticator. Its second argument - 47 | `$exception` - will be this exception. Use its `getMessageKey()` to fetch the 48 | correct message: 49 | 50 | [[[ code('9d53b88aa6') ]]] 51 | 52 | ***TIP 53 | if you're using the `AbstractFormLoginAuthenticator` base class, the 54 | `onAuthenticationFailure()` method is taken care of for you, but you can override 55 | it if you need to. 56 | *** 57 | 58 | Of course, you can really use whatever logic you want in here to return a nice message 59 | to the user. 60 | 61 | Have fun and give friendly errors! 62 | -------------------------------------------------------------------------------- /knpu/failure-handling.md: -------------------------------------------------------------------------------- 1 | # Customizing Failure Handling 2 | 3 | 4 | Authentication can fail inside your authenticator in any of these 3 functions: 5 | 6 | * `getCredentials()` 7 | * `getUser()` 8 | * `checkCredentials()` 9 | 10 | The [Customizing Authentication Failure Messages](error-messages) tutorial tells 11 | you *how* you can fail authentication and how to customize the error message when 12 | that happens. 13 | 14 | But if you need more control, use the `onAuthenticationFailure()` method. 15 | 16 | ## onAuthenticationFailure() 17 | 18 | Every authenticator has a `onAuthenticationFailure()` method. This is called whenever 19 | authentication fails, and it has one job: create a `Response` that should be sent 20 | back to the user. This could be a redirect back to the login page or a 403 JSON 21 | response. 22 | 23 | If you extend certain authenticators - like `AbstractFormLoginAuthenticator` - then 24 | this method is filled in for you automatically. But you can feel free to override 25 | it and customize. 26 | 27 | ## Sending back JSON for AJAX 28 | 29 | Suppose your [login form](login-form) uses AJAX. Instead of redirecting to `/login` 30 | on a failure, you probably want it to return some sort of JSON response. Just 31 | override `onAuthenticationFailure()`: 32 | 33 | [[[ code('1d0f823294') ]]] 34 | 35 | That's it! If you fail authentication via AJAX, you'll receive a JSON response instead 36 | of the redirect. 37 | -------------------------------------------------------------------------------- /knpu/fos-user-bundle.md: -------------------------------------------------------------------------------- 1 | # FOSUserBundle 2 | 3 | Todo - come back later! 4 | -------------------------------------------------------------------------------- /knpu/guard-auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/knpu/guard-auth.png -------------------------------------------------------------------------------- /knpu/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Installation is easy! So let's get it behind us! 4 | 5 | ## 1) Install the Library via Composer 6 | 7 | [Download Composer](https://getcomposer.org/download/) and then run this command 8 | from inside your project. 9 | 10 | ```bash 11 | composer require knpuniversity/guard-bundle:~0.1@dev 12 | ``` 13 | 14 | Use `php composer.phar require knpuniversity/guard-bundle:~0.1@dev` if you don't 15 | have Composer installed globally. 16 | 17 | ## 2) Enable the Bundle 18 | 19 | Find your `app/AppKernel.php` file and enable the bundle: 20 | 21 | [[[ code('5fe4c20ab2') ]]] 22 | 23 | ## 3) Build your first authenticator! 24 | 25 | You're ready to build your authentication system! 26 | 27 | **A)** Learn the fundamentals by [building a login form](login-form) 28 | 29 | **B)** Create an [API token authentication system](api-token) 30 | -------------------------------------------------------------------------------- /knpu/login-form-csrf.md: -------------------------------------------------------------------------------- 1 | # Login Form: Adding a CSRF Token 2 | 3 | Come back soon - still in progress! 4 | -------------------------------------------------------------------------------- /knpu/login-form-customize-user.md: -------------------------------------------------------------------------------- 1 | # How to Login with a username *or* email (or crazier) 2 | 3 | Whenever you login, you identify yourself. For a form, this might be with a username 4 | or email. With an [API](api-token), the token often serves both to identify *who* 5 | you are and serve as a sort of "password". 6 | 7 | With Guard, you can use any crazy combination of methods to figure out *who* is 8 | trying to authenticate. The only rule is that your [getUser](login-form#getUser) 9 | function returns *some* object that implements [UserInterface](http://symfony.com/doc/current/cookbook/security/entity_provider.html#what-s-this-userinterface). 10 | 11 | Let's look at an example of *how* you could customize this: 12 | 13 | ## Logging in with a username *or* email 14 | 15 | In the [Form Login](login-form) chapter, we built a login form that queries for 16 | a user from the database using the `username` property: 17 | 18 | [[[ code('ce30843cf7') ]]] 19 | 20 | But what if we wanted to let the user enter his username *or* email? First, create 21 | a method inside your `UserRepository` for this query: 22 | 23 | [[[ code('7a3076c940') ]]] 24 | 25 | Now, in `getUser()`, simply call this method to return your `User` object: 26 | 27 | [[[ code('63ad655faf') ]]] 28 | 29 | This works because we're injecting the entire service container. But, you could just 30 | as easily inject *only* the entity manager to clean things up. 31 | 32 | Now, wasn't that easy? Have some other weird requirement for how a user is loaded? 33 | Do whatever you want inside of `getUser()`. 34 | 35 | ***TIP 36 | Why not use the `$userProvider` argument? The `$userProvider` that's passed to us 37 | here is what we have configured in `security.yml` under the [providers](login-form#security-providers) 38 | key. In this project, this object gives us a `loadUserByUsername` method that queries 39 | for the `User` by the username. We *could* customize the user provider and make 40 | it do what we want. Or, we could simply fetch our repository directly and query 41 | for what we need. That seems much easier, and I've yet to see a downside. 42 | *** 43 | -------------------------------------------------------------------------------- /knpu/login-form.md: -------------------------------------------------------------------------------- 1 | # How to Create a Login Form 2 | 3 | So you want a login form? That's simple. And along the way, you'll learn all the 4 | steps that happen during authentication, and how you can customize what happens 5 | at each one. 6 | 7 | You still have to do some work, but you're going to be really happy with the result. 8 | 9 | ***TIP 10 | Click **Download** to get the starting or finished code of this tutorial. 11 | *** 12 | 13 | 14 | 15 | ## Create the Login Form 16 | 17 | Don't think about security yet! Instead, start by creating a Symfony controller with 18 | two action methods: one for rendering the login form and another that'll handle the 19 | login submit: 20 | 21 | [[[ code('586d873109') ]]] 22 | 23 | So far, this is just a lovely, but boring set of actions. The only interesting 24 | parts are the `last_username` and `error` variables. Where are those coming from? 25 | You'll see. Also, `loginCheckAction()` doesn't do anything - and it never will. Another 26 | layer will handle the login submit. 27 | 28 | 29 | 30 | Next, create the login template: 31 | 32 | [[[ code('771878ae65') ]]] 33 | 34 | This form submits to the `/login_check` URL and the field names are `_username` and 35 | `_password`. Remember these - they'll be important in a minute (see [getCredentials()](#getCredentials)). 36 | 37 | ## Installing Guard 38 | 39 | Read the short [Installation](install) chapter to make sure you've got the 40 | bundle installed and enabled. 41 | 42 | ## Creating an Authenticator 43 | 44 | With Guard, the whole authentication process - fetching the username/password POST 45 | values, validating the password, redirecting after success, etc - is handled in a 46 | single class called an "Authenticator". Your authenticator can be as crazy as you 47 | want, as long as it implements [KnpU\Guard\GuardAuthenticatorInterface](https://github.com/knpuniversity/KnpUGuard/blob/master/src/GuardAuthenticatorInterface.php). 48 | 49 | For login forms, life is easier, thanks to a convenience class called `AbstractFormLoginAuthenticator`. 50 | Create a new `FormLoginAuthenticator` class, make it extend this class, and add all 51 | the missing methods (from the interface and abstract class): 52 | 53 | [[[ code('1334f477a3') ]]] 54 | 55 | Your mission: fill in each method. We'll get to that in a second. 56 | 57 | To fill in those methods, we're going to need some services. To keep this tutorial 58 | simple, let's pass the entire container into our authenticator: 59 | 60 | [[[ code('7a83cbc309') ]]] 61 | 62 | ***TIP 63 | For seasoned-Symfony devs, you can of course inject *only* the services you need. 64 | *** 65 | 66 | ## Registering your Authenticator 67 | 68 | Before filling in the methods, let's tell Symfony about our fancy new authenticator. 69 | First, register it as a service: 70 | 71 | [[[ code('a83921bba1') ]]] 72 | 73 | Next, update your `security.yml` file to use the new service: 74 | 75 | [[[ code('b5384b49a0') ]]] 76 | 77 | Your firewall (called `main` here) can look however you want, as long as it has a 78 | `knpu_guard` section under it with an `authenticators` key that includes the service 79 | name that you setup a second ago (`app.form_login_authenticator` in my example). 80 | 81 | 82 | 83 | I've also setup my "user provider" to load my users from the database: 84 | 85 | [[[ code('0f206d4cda') ]]] 86 | 87 | In a minute, you'll see where that's used. 88 | 89 | ## Filling in the Authenticator Methods 90 | 91 | Your authenticator is now being used by Symfony. So let's fill in each method: 92 | 93 | 94 | 95 | ### getCredentials() 96 | 97 | [[[ code('2bc735ebfe') ]]] 98 | 99 | The `getCredentials()` method is called on **every single request** and its job is 100 | either to fetch the username/password from the request and return them. 101 | 102 | So, from here, there are 2 possibilities: 103 | 104 | # | Conditions | Result | Next Step 105 | ------ | --------------------- | ------------------------- | ---------- 106 | A) | Return non-null value | Authentication continues | [getUser()](#getUser) 107 | B) | Return null | Authentication is skipped | Nothing! But if the user is anonymous and tries to access a secure page, [getLoginUrl()](#getLoginUrl) will be called 108 | 109 | **A)** If the URL is `/login_check` (that's the URL that our login form submits to), 110 | then we fetch the `_username` and `_password` post parameters (these were our 111 | [form field names](#create-login-form-template)) and return them. Whatever you return 112 | here will be passed to a few other methods later. In this case - since we returned 113 | a non-null value from `getCredentials()` - the [getUser()](#getUser) method is called 114 | next. 115 | 116 | **B)** If the URL is *not* `/login_check`, we return `null`. In this case, the request 117 | continues anonymously - no other methods are called on the authenticator. If the 118 | page the user is accessing requires login, they'll be redirected to the login form: 119 | see [getLoginUrl()](#getLoginUrl). 120 | 121 | ***TIP 122 | We also set a `Security::LAST_USERNAME` key into the session. This is optional, but 123 | it lets you pre-fill the login form with this value (see the 124 | [SecurityController::loginAction](#SecurityController-loginAction) from earlier). 125 | *** 126 | 127 | 128 | 129 | ### getUser() 130 | 131 | If `getCredentials()` returns a non-null value, then `getUser()` is called next. 132 | Its job is simple: return a user (an object implementing [UserInterface](http://symfony.com/doc/current/cookbook/security/entity_provider.html#what-s-this-userinterface)): 133 | 134 | [[[ code('ce30843cf7') ]]] 135 | 136 | The `$credentials` argument is whatever you returned from `getCredentials()` and 137 | the `$userProvider` is whatever you've configured in security.yml under the 138 | [providers](#security-providers) key. My provider queries the database and returns 139 | the `User` entity. 140 | 141 | There are 2 paths from there: 142 | 143 | # | Conditions | Result | Next Step 144 | ----- | ------------------------------------------------- | ------------------------- | ------------- 145 | A) | Return a User object | Authentication continues | [checkCredentials()](#checkCredentials) 146 | B) | Return null or throw an `AuthenticationException` | Authentication fails | Redirect to [getLoginUrl()](#getLoginUrl) 147 | 148 | **A)** If you return some `User` object (using whatever method you want) - then 149 | you'll continue on to [checkCredentials()](#checkCredentials). 150 | 151 | **B** If you return `null` or throw any `Symfony\Component\Security\Core\Exception\AuthenticationException`, 152 | authentication will fail and the user will be redirected back to the login page: 153 | see [getLoginUrl()](#getLoginUrl). 154 | 155 | 156 | 157 | ### checkCredentials() 158 | 159 | If you return a user from `getUser()`, then `checkCredentials()` is called next. 160 | Your job is simple: check if the username/password combination is valid. If it isn't, 161 | throw a `BadCredentialsException` (or any `AuthenticationException`): 162 | 163 | [[[ code('95ef4c0238') ]]] 164 | 165 | Like before, `$credentials` is whatever you returned from `getCredentials()`. And 166 | now, the `$user` argument is what you just returned from `getUser()`. To check the 167 | user, you can use the `security.password_encoder`, which automatically hashes the 168 | plain password based on your `security.yml` configuration. 169 | 170 | Want to do some other custom checks beyond the password? Go crazy! Based on what 171 | you do, there are 2 paths: 172 | 173 | # | Conditions | Result | Next Step 174 | ----- | ---------------------------------------------------------- | ------------------------- | ------------- 175 | A) | do anything *except* throwing an `AuthenticationException` | Authentication successful | Redirect the user (may involve [getDefaultSuccessRedirectUrl()](#getDefaultSuccessRedirectUrl)) 176 | B) | Throw any type of `AuthenticationException` | Authentication fails | Redirect to [getLoginUrl()](#getLoginUrl) 177 | 178 | If you *don't* throw an exception, congratulations! You're user is now authenticated, 179 | and will be redirected somewhere... 180 | 181 | 182 | 183 | ### getDefaultSuccessRedirectUrl() 184 | 185 | Your user is now authenticated. Woot! But, where should we redirect them? The `AbstractFormLoginAuthenticator` 186 | class takes care of *most* of this automatically. If the user originally tried to 187 | access a protected page (e.g. `/admin`) but was redirected to the login page, then 188 | they'll now be redirected back to that URL (so, `/admin`). 189 | 190 | But what if the user went to `/login` directly? In that case, you'll need to decide 191 | where they should go. How about the homepage? 192 | 193 | [[[ code('845a83722f') ]]] 194 | 195 | This fetches the `router` service and redirects to a `homepage` route (change this 196 | to a real route in your application). But note: this method is *only* called if there 197 | isn't some previous page that user should be redirected to. 198 | 199 | 200 | 201 | ### getLoginUrl() 202 | 203 | If authentication fails in `getUser()` or `checkCredentials()`, the user will be 204 | redirected back to the login page. In this method, you just need to tell Symfony 205 | where your login page lives: 206 | 207 | [[[ code('362a033205') ]]] 208 | 209 | In our case, the login page route name is `security_login`. 210 | 211 | ## Customize! 212 | 213 | Try it out! You should be able to login, see login errors, and control most of the 214 | process. So what else can we customize? 215 | 216 | * [How can I login by username *or* email (or any other weird way)?](login-form-customize-user) 217 | * [How can I customize the error messages?](login-form-error-messages) 218 | * [How can I control/hook into what happens when login fails?](login-form-failure-handling) 219 | * [How can I control/hook into what happens on login success?](login-form-success-handling) 220 | * [How can I add a CSRF token?](login-form-csrf) 221 | -------------------------------------------------------------------------------- /knpu/manually-authenticating.md: -------------------------------------------------------------------------------- 1 | # How can I Manually Authenticate a User? 2 | 3 | In progress - check back later! 4 | -------------------------------------------------------------------------------- /knpu/metadata.yml: -------------------------------------------------------------------------------- 1 | title: KnpUGuard: Symfony Authentication with a Smile 2 | summary: | 3 | Symfony's authentication is definitely powerful, but if you 4 | need to do something complex, you might pull your hair out. 5 | 6 | Introducing KnpUGuard authentication, which puts all the complexities of any 7 | authentication scheme right at your finger-tips. The code you right will be 8 | easy to understand and customizing things to any whim will be simple: 9 | 10 | * Authentication via an API token 11 | * "Social login" / OAuth authentication 12 | * Creating a traditional login form 13 | * Customizing success/failure behavior 14 | * Customizing error messages 15 | * Loading users in complex ways 16 | * ... probably anything else 17 | 18 | If all goes well, Guard will become a core part of Symfony. If you like it, let 19 | people know it worked for you at [symfony/symfony#14673](https://github.com/symfony/symfony/pull/14673). 20 | And if it doesn't cover a use-case, [open an issue](https://github.com/knpuniversity/KnpUGuard) 21 | and let me know! 22 | 23 | illustration: guard-auth.png 24 | hasVideoDownload: 0 25 | 26 | chapters: 27 | install: 28 | is_finished: true 29 | login-form: 30 | is_finished: true 31 | api-token: 32 | is_finished: true 33 | social-login: 34 | is_finished: false 35 | login-form-customize-user: 36 | is_finished: true 37 | error-messages: 38 | is_finished: true 39 | failure-handling: 40 | is_finished: true 41 | success-handling: 42 | is_finished: true 43 | login-form-csrf: 44 | is_finished: false 45 | multiple-authenticators: 46 | is_finished: false 47 | silex: 48 | is_finished: false 49 | fos-user-bundle: 50 | is_finished: false 51 | manually-authenticating: 52 | is_finished: false 53 | -------------------------------------------------------------------------------- /knpu/multiple-authenticators.md: -------------------------------------------------------------------------------- 1 | # How can I use Multiple Authenticators? 2 | 3 | Todo - come back later! 4 | -------------------------------------------------------------------------------- /knpu/notes.md: -------------------------------------------------------------------------------- 1 | - check links on deploy 2 | 3 | 4 | - store last username 5 | - csrf protection 6 | - custom messages for no user 7 | - logout 8 | - remember me 9 | - direct authentication 10 | 11 | - should I be explaining a bit more about authentication? 12 | - how weird is the setup stuff? 13 | 14 | 15 | - controller to redirect to Facebook 16 | - redirect back and create/fetch user 17 | - finish registration 18 | - store access token in session 19 | - after registration, redirect back to auth endpoint? Or 20 | authenticate directly? 21 | 22 | OAUTH PLUGIN 23 | - ultimately I need a to deliver back the OAuth "User" 24 | - have a way to hook in other OAuth "drivers" 25 | - ability to just configure your client_id and client_secret stuff 26 | - user hook points: 27 | A) what to do with the Facebook User 28 | - return a User object (maybe you create one, we don't care) 29 | - stash in the session and redirect elsewhere 30 | B) ??? That's really all you need 31 | -------------------------------------------------------------------------------- /knpu/silex.md: -------------------------------------------------------------------------------- 1 | # How can I use this in Silex? 2 | 3 | Todo! Check back... -------------------------------------------------------------------------------- /knpu/social-login.md: -------------------------------------------------------------------------------- 1 | # Social Login with Facebook 2 | 3 | Everybody wants their site to have a "Login with Facebook", "Login with GitHub" or 4 | "Login with InstaFaceTweet". Let's give the people what they want! 5 | 6 | Setting this up will take some coding, but the result will be easy to understand 7 | and simple to extend. Let's do it! 8 | 9 | ***TIP 10 | Watch our [OAuth2 in 8 Steps](http://knpuniversity.com/screencast/oauth) tutorial 11 | first to get handle on how OAuth works. 12 | *** 13 | 14 | ## The Flow 15 | 16 | Social authentication uses OAuth - usually the 17 | [authorization code grant type](http://knpuniversity.com/screencast/oauth/authorization-code). 18 | That just means that we have a flow that looks like this: 19 | 20 | TODO - IMAGE HERE 21 | 22 | 1. Your user clicks on a link to "Login with Facebook". This takes them to 23 | a Symfony controller on your site (e.g. `/connect/facebook`) 24 | 25 | 2. That controller redirects to Facebook, where they grant your application access 26 | 27 | 3. Facebook redirects back to your site (e.g. `/connect/facebook-check`) with 28 | a `?code=` query parameter 29 | 30 | 4. We make an API request back to Facebook to exchange this code for an access token. 31 | Then, immediately, we use this access token to make another API request to Facebook 32 | to fetch the user's information - like their email address. If we find an existing 33 | user, we can log the user in. If not, we might choose to create a User in the 34 | database, or have the user complete a "registration" form. 35 | 36 | ## Installing Guard 37 | 38 | Read the short [Installation](install) chapter to make sure you've got the 39 | bundle installed and enabled. 40 | 41 | ## Creating your Facebook Application 42 | 43 | To follow along, you'll need to create a Facebook Application at https://developers.facebook.com/. 44 | You can name it anything, but for the "Site URL", make sure it uses whatever domain 45 | you're using. In my case, I'm using `http://localhost:8000/`. I'll probably create 46 | a different application for my production site. 47 | 48 | When you're done, this will give you "App ID" and "App Secret". Keep those handy! 49 | 50 | ## Installing the OAuth Client Libraries 51 | 52 | To help with the OAuth heavy-lifting, we'll use a nice [oauth2-client](TOODOOOOOOOO) 53 | library, and its [oauth2-facebook](https://github.com/thephpleague/oauth2-facebook) 54 | helper library. Get these installed: 55 | 56 | ```bash 57 | composer require league/oauth2-client:~1.0@dev league/oauth2-facebook 58 | ``` 59 | 60 | ## Setting up the Facebook Provider Service 61 | 62 | The oauth2-facebook library lets us create a nice `Facebook` object that makes doing 63 | OAuth with Facebook a breeze. We'll need this object in several places, so let's register 64 | it as a service: 65 | 66 | [[[ code('2299b2f677') ]]] 67 | 68 | This references two new parameters - `facebook_app_id` and `facebook_app_secret`. 69 | Add these to your `parameters.yml` (and `parameters.yml.dist`) file with your Facebook 70 | application's "App ID" and "App Secret" values: 71 | 72 | [[[ code('01b69ed14c') ]]] 73 | 74 | ***TIP 75 | If you haven't seen the odd `"@=service('router')..."` expression syntax before, 76 | we have a blog post on it: 77 | [Symfony Service Expressions: Do things you thought Impossible](http://knpuniversity.com/blog/service-expressions). 78 | *** 79 | 80 | ## Creating the FacebookConnectController 81 | 82 | Don't think about security yet! Instead, look at the flow above. We'll need a controller 83 | with *two* actions: one that simply redirects to Facebook for authorization (`/connect/facebook`) 84 | and another that handles what happens when Facebook redirects back to us (`/connect/facebook-check`): 85 | 86 | [[[ code('dcd753998b') ]]] 87 | 88 | The first URL - `/connect/facebook` - uses the Facebook provider service from the 89 | oauth2-client library that we just setup. Its job is simple: redirect to Facebook 90 | to start the authorization process. In a second, we'll add a "Login with Facebook" 91 | link that will point here. 92 | 93 | The second URL - `/connect/facebook-check` - will be the URL that Facebook will 94 | redirect back to after. But notice it doesn't do anything - and it never will. Another 95 | layer (the authenticator) will intercept things and handle all the logic. 96 | 97 | For good measure, let's create a "Login with Facebook" on our normal login page: 98 | 99 | [[[ code('797ea77de9') ]]] 100 | 101 | ## Creating an Authenticator 102 | 103 | With Guard, the whole authentication process - fetching the access token, getting 104 | the user information, redirecting after success, etc - is handled in a 105 | single class called an "Authenticator". Your authenticator can be as crazy as you 106 | want, as long as it implements [KnpU\Guard\GuardAuthenticatorInterface](https://github.com/knpuniversity/KnpUGuard/blob/master/src/GuardAuthenticatorInterface.php). 107 | 108 | Most of the time, you can extend a convenience class called `AbstractGuardAuthenticator`. 109 | Create a new `FacebookAuthenticator` class, make it extend this class, and add all 110 | the necessary methods: 111 | 112 | [[[ code('8fd00c3f3c') ]]] 113 | 114 | Your mission: fill in each method. We'll get to that in a second. 115 | 116 | To fill in those methods, we're going to need some services. To keep this tutorial 117 | simple, let's pass the entire container into our authenticator: 118 | 119 | [[[ code('46b27bcaab') ]]] 120 | 121 | ***TIP 122 | For seasoned-Symfony devs, you can of course inject *only* the services you need. 123 | *** 124 | 125 | ## Registering your Authenticator 126 | 127 | Before filling in the methods, let's tell Symfony about our fancy new authenticator. 128 | First, register it as a service: 129 | 130 | [[[ code('478a1211c0') ]]] 131 | 132 | Next, update your `security.yml` file to use the new service: 133 | 134 | [[[ code('7372399109') ]]] 135 | 136 | Your firewall (called `main` here) can look however you want, as long as it has a 137 | `knpu_guard` section under it with an `authenticators` key that includes the service 138 | name that you setup a second ago (`app.facebook_authenticator` in my example). 139 | 140 | ## Filling in the Authenticator Methods 141 | 142 | Congratulations! Your authenticator is now being used by Symfony. Here's the flow 143 | so far: 144 | 145 | 1) The user clicks "Login with Facebook"; 146 | 2) Our `connectFacebookAction` redirects the user to Facebook; 147 | 3) After authorizing our app, Facebook redirects back to `/connect/facebook-connect`; 148 | 4) The `getCredentials()` method on `FacebookAuthenticator` is called and we start 149 | working our magic! 150 | 151 | 152 | 153 | ### getCredentials() 154 | 155 | [[[ code('4cfa91bec3') ]]] 156 | 157 | If the user approves our application, Facebook will redirect back to `/connect/facebook-connect` 158 | with a `?code=ABC123` query parameter. That's called the "authorization code". 159 | 160 | The `getCredentials()` method is called on **every single request** and its job is 161 | simple: grab this "authorization code" and return it. 162 | 163 | Inside `getCredentials()`, here are 2 possible paths: 164 | 165 | # | Conditions | Result | Next Step 166 | ------ | --------------------- | ------------------------- | ---------- 167 | A) | Return non-null value | Authentication continues | [getUser()](#getUser) 168 | B) | Throw an exception | Authentication fails | [onAuthenticationFailure()](#onAuthenticationFailure) 169 | C) | Return null | Authentication is skipped | Nothing! 170 | 171 | 172 | **A)** If the URL is `/connect/facebook-connect`, then we fetch the `code` query 173 | parameter that Facebook is sending us and return it. This will be passed to a few 174 | other methods later. In this case - since we returned a non-null value from `getCredentials()` - 175 | the [getUser()](#getUser) method is called next. 176 | 177 | **B)** If the URL is `/connect/facebook-connect` but there is no `code` query parameter, 178 | something went wrong! This probably means the user didn't authorize our app. To 179 | fail authentication, you can throw any `AuthenticationException`. The 180 | [CustomAuthenticationException](error-messages) is just a cool way to control the 181 | message the user sees. 182 | 183 | **C)** If the URL is *not* `/connect/facebook-connect`, we return `null`. In this 184 | case, the request continues anonymously - no other methods are called on the authenticator. 185 | 186 | 187 | 188 | ### getUser() 189 | 190 | If `getCredentials()` returns a non-null value, then `getUser()` is called next. 191 | Its job is simple: return a user (an object implementing [UserInterface](http://symfony.com/doc/current/cookbook/security/entity_provider.html#what-s-this-userinterface)). 192 | 193 | But to do that, there are several steps. Ultimately, there are two possible results: 194 | 195 | # | Conditions | Result | Next Step 196 | ----- | ------------------------------------------------- | ------------------------- | ------------- 197 | A) | Return a User object | Authentication continues | [checkCredentials()](#checkCredentials) 198 | B) | Return null or throw an `AuthenticationException` | Authentication fails | Redirect to [getLoginUrl()](#getLoginUrl) 199 | 200 | #### getUser() Part 1: Get the access token 201 | 202 | The `$authorizationCode` argument is whatever you returned from `getCredentials()`. 203 | Our first job is to talk to the Facebook API and exchange this for an "access token". 204 | Fortunately, with the oauth2-client library, this is easy: 205 | 206 | [[[ code('03af090698') ]]] 207 | 208 | If this fails for some reason, we throw an AuthenticationException (specifically 209 | a `CustomAuthenticationException` to control the message). 210 | 211 | #### getUser() Part 2: Get Facebook User Information 212 | 213 | Now that we have a valid access token, we can make reqeusts to the Facebook API 214 | on behalf of the user. The most important thing we need is information about the 215 | user - like what is their email address? 216 | 217 | To get that, use the `getResourceOwner()` method: 218 | 219 | [[[ code('7bfe403f0d') ]]] 220 | 221 | This returns a `FacebookUser` from the `oauth2-facebook` library (if you connect to 222 | something else like GitHub, it will have a different object). We can use that to 223 | get the user's email address. 224 | 225 | #### getUser() Part 3: Fetching/Creating the User 226 | 227 | Great! We now know some information about the user, including their email address. 228 | 229 | 230 | 231 | 232 | **A)** If you return some `User` object (using whatever method you want) - then 233 | you'll continue on to [checkCredentials()](#checkCredentials). 234 | 235 | **B** If you return `null` or throw any `Symfony\Component\Security\Core\Exception\AuthenticationException`, 236 | authentication will fail and the user will be redirected back to the login page: 237 | see [getLoginUrl()](#getLoginUrl). 238 | 239 | 240 | 241 | ### checkCredentials() 242 | 243 | If you return a user from `getUser()`, then `checkCredentials()` is called next. 244 | Your job is simple: check if the username/password combination is valid. If it isn't, 245 | throw a `BadCredentialsException` (or any `AuthenticationException`): 246 | 247 | [[[ code('95ef4c0238') ]]] 248 | 249 | Like before, `$credentials` is whatever you returned from `getCredentials()`. And 250 | now, the `$user` argument is what you just returned from `getUser()`. To check the 251 | user, you can use the `security.password_encoder`, which automatically hashes the 252 | plain password based on your `security.yml` configuration. 253 | 254 | Want to do some other custom checks beyond the password? Go crazy! Based on what 255 | you do, there are 2 paths: 256 | 257 | # | Conditions | Result | Next Step 258 | ----- | ---------------------------------------------------------- | ------------------------- | ------------- 259 | A) | do anything *except* throwing an `AuthenticationException` | Authentication successful | Redirect the user (may involve [getDefaultSuccessRedirectUrl()](#getDefaultSuccessRedirectUrl)) 260 | B) | Throw any type of `AuthenticationException` | Authentication fails | Redirect to [getLoginUrl()](#getLoginUrl) 261 | 262 | If you *don't* throw an exception, congratulations! You're user is now authenticated, 263 | and will be redirected somewhere... 264 | 265 | 266 | 267 | ### getDefaultSuccessRedirectUrl() 268 | 269 | Your user is now authenticated. Woot! But, where should we redirect them? The `AbstractFormLoginAuthenticator` 270 | class takes care of *most* of this automatically. If the user originally tried to 271 | access a protected page (e.g. `/admin`) but was redirected to the login page, then 272 | they'll now be redirected back to that URL (so, `/admin`). 273 | 274 | But what if the user went to `/login` directly? In that case, you'll need to decide 275 | where they should go. How about the homepage? 276 | 277 | [[[ code('845a83722f') ]]] 278 | 279 | This fetches the `router` service and redirects to a `homepage` route (change this 280 | to a real route in your application). But note: this method is *only* called if there 281 | isn't some previous page that user should be redirected to. 282 | 283 | 284 | 285 | ### getLoginUrl() 286 | 287 | If authentication fails in `getUser()` or `checkCredentials()`, the user will be 288 | redirected back to the login page. In this method, you just need to tell Symfony 289 | where your login page lives: 290 | 291 | [[[ code('362a033205') ]]] 292 | 293 | In our case, the login page route name is `security_login`. 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | ## Installing Guard 313 | 314 | Read the short [Installation](install) chapter to make sure you've got the 315 | bundle installed and enabled. 316 | 317 | ## Creating an Authenticator 318 | 319 | With Guard, the whole authentication process - fetching the username/password POST 320 | values, validating the password, redirecting after success, etc - is handled in a 321 | single class called an "Authenticator". Your authenticator can be as crazy as you 322 | want, as long as it implements [KnpU\Guard\GuardAuthenticatorInterface](https://github.com/knpuniversity/KnpUGuard/blob/master/src/GuardAuthenticatorInterface.php). 323 | 324 | For login forms, life is easier, thanks to a convenience class called `AbstractFormLoginAuthenticator`. 325 | Create a new `FormLoginAuthenticator` class, make it extend this class, and add all 326 | the missing methods (from the interface and abstract class): 327 | 328 | [[[ code('1334f477a3') ]]] 329 | 330 | Your mission: fill in each method. We'll get to that in a second. 331 | 332 | To fill in those methods, we're going to need some services. To keep this tutorial 333 | simple, let's pass the entire container into our authenticator: 334 | 335 | [[[ code('7a83cbc309') ]]] 336 | 337 | ***TIP 338 | For seasoned-Symfony devs, you can of course inject *only* the services you need. 339 | *** 340 | 341 | ## Registering your Authenticator 342 | 343 | Before filling in the methods, let's tell Symfony about our fancy new authenticator. 344 | First, register it as a service: 345 | 346 | [[[ code('a83921bba1') ]]] 347 | 348 | Next, update your `security.yml` file to use the new service: 349 | 350 | [[[ code('b5384b49a0') ]]] 351 | 352 | Your firewall (called `main` here) can look however you want, as long as it has a 353 | `knpu_guard` section under it with an `authenticators` key that includes the service 354 | name that you setup a second ago (`app.form_login_authenticator` in my example). 355 | 356 | 357 | 358 | I've also setup my "user provider" to load my users from the database: 359 | 360 | [[[ code('0f206d4cda') ]]] 361 | 362 | In a minute, you'll see where that's used. 363 | 364 | ## Filling in the Authenticator Methods 365 | 366 | Your authenticator is now being used by Symfony. So let's fill in each method: 367 | 368 | 369 | 370 | ### getCredentials() 371 | 372 | [[[ code('2bc735ebfe') ]]] 373 | 374 | The `getCredentials()` method is called on **every single request** and its job is 375 | either to fetch the username/password from the request and return them. 376 | 377 | So, from here, there are 2 possibilities: 378 | 379 | # | Conditions | Result | Next Step 380 | ------ | --------------------- | ------------------------- | ---------- 381 | A) | Return non-null value | Authentication continues | [getUser()](#getUser) 382 | B) | Return null | Authentication is skipped | Nothing! But if the user is anonymous and tries to access a secure page, [getLoginUrl()](#getLoginUrl) will be called 383 | 384 | **A)** If the URL is `/login_check` (that's the URL that our login form submits to), 385 | then we fetch the `_username` and `_password` post parameters (these were our 386 | [form field names](#create-login-form-template)) and return them. Whatever you return 387 | here will be passed to a few other methods later. In this case - since we returned 388 | a non-null value from `getCredentials()` - the [getUser()](#getUser) method is called 389 | next. 390 | 391 | **B)** If the URL is *not* `/login_check`, we return `null`. In this case, the request 392 | continues anonymously - no other methods are called on the authenticator. If the 393 | page the user is accessing requires login, they'll be redirected to the login form: 394 | see [getLoginUrl()](#getLoginUrl). 395 | 396 | ***TIP 397 | We also set a `Security::LAST_USERNAME` key into the session. This is optional, but 398 | it lets you pre-fill the login form with this value (see the 399 | [SecurityController::loginAction](#SecurityController-loginAction) from earlier). 400 | *** 401 | 402 | 403 | 404 | ### getUser() 405 | 406 | If `getCredentials()` returns a non-null value, then `getUser()` is called next. 407 | Its job is simple: return a user (an object implementing [UserInterface](http://symfony.com/doc/current/cookbook/security/entity_provider.html#what-s-this-userinterface)): 408 | 409 | [[[ code('ce30843cf7') ]]] 410 | 411 | The `$credentials` argument is whatever you returned from `getCredentials()` and 412 | the `$userProvider` is whatever you've configured in security.yml under the 413 | [providers](#security-providers) key. My provider queries the database and returns 414 | the `User` entity. 415 | 416 | There are 2 paths from there: 417 | 418 | # | Conditions | Result | Next Step 419 | ----- | ------------------------------------------------- | ------------------------- | ------------- 420 | A) | Return a User object | Authentication continues | [checkCredentials()](#checkCredentials) 421 | B) | Return null or throw an `AuthenticationException` | Authentication fails | Redirect to [getLoginUrl()](#getLoginUrl) 422 | 423 | **A)** If you return some `User` object (using whatever method you want) - then 424 | you'll continue on to [checkCredentials()](#checkCredentials). 425 | 426 | **B** If you return `null` or throw any `Symfony\Component\Security\Core\Exception\AuthenticationException`, 427 | authentication will fail and the user will be redirected back to the login page: 428 | see [getLoginUrl()](#getLoginUrl). 429 | 430 | 431 | 432 | ### checkCredentials() 433 | 434 | If you return a user from `getUser()`, then `checkCredentials()` is called next. 435 | Your job is simple: check if the username/password combination is valid. If it isn't, 436 | throw a `BadCredentialsException` (or any `AuthenticationException`): 437 | 438 | [[[ code('95ef4c0238') ]]] 439 | 440 | Like before, `$credentials` is whatever you returned from `getCredentials()`. And 441 | now, the `$user` argument is what you just returned from `getUser()`. To check the 442 | user, you can use the `security.password_encoder`, which automatically hashes the 443 | plain password based on your `security.yml` configuration. 444 | 445 | Want to do some other custom checks beyond the password? Go crazy! Based on what 446 | you do, there are 2 paths: 447 | 448 | # | Conditions | Result | Next Step 449 | ----- | ---------------------------------------------------------- | ------------------------- | ------------- 450 | A) | do anything *except* throwing an `AuthenticationException` | Authentication successful | Redirect the user (may involve [getDefaultSuccessRedirectUrl()](#getDefaultSuccessRedirectUrl)) 451 | B) | Throw any type of `AuthenticationException` | Authentication fails | Redirect to [getLoginUrl()](#getLoginUrl) 452 | 453 | If you *don't* throw an exception, congratulations! You're user is now authenticated, 454 | and will be redirected somewhere... 455 | 456 | 457 | 458 | ### getDefaultSuccessRedirectUrl() 459 | 460 | Your user is now authenticated. Woot! But, where should we redirect them? The `AbstractFormLoginAuthenticator` 461 | class takes care of *most* of this automatically. If the user originally tried to 462 | access a protected page (e.g. `/admin`) but was redirected to the login page, then 463 | they'll now be redirected back to that URL (so, `/admin`). 464 | 465 | But what if the user went to `/login` directly? In that case, you'll need to decide 466 | where they should go. How about the homepage? 467 | 468 | [[[ code('845a83722f') ]]] 469 | 470 | This fetches the `router` service and redirects to a `homepage` route (change this 471 | to a real route in your application). But note: this method is *only* called if there 472 | isn't some previous page that user should be redirected to. 473 | 474 | 475 | 476 | ### getLoginUrl() 477 | 478 | If authentication fails in `getUser()` or `checkCredentials()`, the user will be 479 | redirected back to the login page. In this method, you just need to tell Symfony 480 | where your login page lives: 481 | 482 | [[[ code('362a033205') ]]] 483 | 484 | In our case, the login page route name is `security_login`. 485 | 486 | ## Customize! 487 | 488 | Try it out! You should be able to login, see login errors, and control most of the 489 | process. So what else can we customize? 490 | 491 | * [How can I login by username *or* email (or any other weird way)?](login-form-customize-user) 492 | * [How can I customize the error messages?](login-form-error-messages) 493 | * [How can I control/hook into what happens when login fails?](login-form-failure-handling) 494 | * [How can I control/hook into what happens on login success?](login-form-success-handling) 495 | * [How can I add a CSRF token?](login-form-csrf) 496 | 497 | -------------------------------------------------------------------------------- /knpu/success-handling.md: -------------------------------------------------------------------------------- 1 | # Customizing Success Handling 2 | 3 | So, your authentication is working. Yes! Now, what if you need to hook into what 4 | happens next? For example, maybe you need to redirect to a special page, or return 5 | JSON instead of a redirect in some cases. Or perhaps you want to store the last login 6 | time of your user. All that is possible and easy. 7 | 8 | ## onAuthenticationSuccess() 9 | 10 | Every authenticator has a `onAuthenticationSuccess()` method. This is called whenever 11 | authentication is completed, and it has one job: create a `Response` that should 12 | be sent back to the user. This could be a redirect back to the last page the user 13 | visited or return `null` and let the request continue (see [API token](api-token)). 14 | 15 | If you extend certain authenticators - like `AbstractFormLoginAuthenticator` - then 16 | this method is filled in for you automatically. But you can feel free to override it 17 | and customize. 18 | 19 | ## Sending back JSON for AJAX 20 | 21 | Suppose your [login form](login-form) uses AJAX. Instead of redirecting after success, 22 | you probably want it to return some sort of JSON response. Just override `onAuthenticationSuccess()`: 23 | 24 | [[[ code('0041f970c4') ]]] 25 | 26 | That's it! If you login via AJAX, you'll receive a JSON response instead of the 27 | redirect. 28 | 29 | ## Performing an Action on Login 30 | 31 | Suppose you want to store the "last login" time for the user in the database. You 32 | *could* override `onAuthenticationSuccess()`, update the User and save. 33 | 34 | But, there's a better way: Symfony security system dispatches a `security.interactive_login` 35 | event that you can hook into. Why is this better? Because this will be called whenever 36 | a user logs in, whether it is via this authenticator, another authenticator or some 37 | non-Guard system. 38 | 39 | ***TIP 40 | Everything in this section works in normal Symfony, even without Guard! 41 | *** 42 | 43 | First, make sure you have a column on your user: 44 | 45 | [[[ code('65a4b3aa56') ]]] 46 | 47 | Next, create an event subscriber. This will be called whenever a user logs in. It's 48 | job is simple: update this `lastLoginTime` property and save the User: 49 | 50 | [[[ code('273fcd52b5') ]]] 51 | 52 | ***TIP 53 | Not familiar with listeners or susbcribers? Check out 54 | [Interrupt Symfony with an Event Subscriber](http://knpuniversity.com/screencast/symfony-journey/event-subscriber) 55 | *** 56 | 57 | Now, just register this as a service and tag it so that Symfony know about the subscriber: 58 | 59 | [[[ code('31b10f28a4') ]]] 60 | 61 | That's all you need. Next time you login, the User's `lastLoginTime` will automatically 62 | be updated in the database. 63 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/AppBundle/AppBundle.php: -------------------------------------------------------------------------------- 1 | render('default/index.html.twig'); 17 | } 18 | 19 | /** 20 | * @Route("/secure") 21 | */ 22 | public function secureAction() 23 | { 24 | $this->denyAccessUnlessGranted('ROLE_USER'); 25 | 26 | return new Response('It works!'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AppBundle/DataFixtures/ORM/LoadUserData.php: -------------------------------------------------------------------------------- 1 | container = $container; 24 | } 25 | 26 | /** 27 | * {@inheritDoc} 28 | */ 29 | public function load(ObjectManager $manager) 30 | { 31 | $annaAdmin = new User(); 32 | $annaAdmin->setUsername('anna_admin'); 33 | $annaAdmin->setEmail('anna_admin@example.com'); 34 | $encoded = $this->container->get('security.password_encoder') 35 | ->encodePassword($annaAdmin, 'kitten'); 36 | $annaAdmin->setPassword($encoded); 37 | 38 | $manager->persist($annaAdmin); 39 | $manager->flush(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AppBundle/Entity/User.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Javier Eguiluz 19 | */ 20 | class User implements UserInterface 21 | { 22 | /** 23 | * @ORM\Id 24 | * @ORM\GeneratedValue 25 | * @ORM\Column(type="integer") 26 | */ 27 | private $id; 28 | 29 | /** 30 | * @ORM\Column(type="string", unique=true) 31 | */ 32 | private $username; 33 | 34 | /** 35 | * @ORM\Column(type="string", unique=true) 36 | */ 37 | private $email; 38 | 39 | /** 40 | * @ORM\Column(type="string") 41 | */ 42 | private $password; 43 | 44 | /** 45 | * @ORM\Column(type="json_array") 46 | */ 47 | private $roles = array(); 48 | 49 | /** 50 | * @ORM\Column(type="string", unique=true, nullable=true) 51 | */ 52 | private $apiToken; 53 | 54 | /** 55 | * @ORM\Column(type="datetime", nullable=true) 56 | */ 57 | private $lastLoginTime; 58 | 59 | public function __construct() 60 | { 61 | $this->apiToken = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36); 62 | } 63 | 64 | public function getId() 65 | { 66 | return $this->id; 67 | } 68 | 69 | public function getUsername() 70 | { 71 | return $this->username; 72 | } 73 | public function setUsername($username) 74 | { 75 | $this->username = $username; 76 | } 77 | 78 | public function getEmail() 79 | { 80 | return $this->email; 81 | } 82 | public function setEmail($email) 83 | { 84 | $this->email = $email; 85 | } 86 | 87 | public function getPassword() 88 | { 89 | return $this->password; 90 | } 91 | public function setPassword($password) 92 | { 93 | $this->password = $password; 94 | } 95 | 96 | /** 97 | * Returns the roles or permissions granted to the user for security. 98 | */ 99 | public function getRoles() 100 | { 101 | $roles = $this->roles; 102 | 103 | // guarantees that a user always has at least one role for security 104 | if (empty($roles)) { 105 | $roles[] = 'ROLE_USER'; 106 | } 107 | 108 | return array_unique($roles); 109 | } 110 | 111 | public function setRoles($roles) 112 | { 113 | $this->roles = $roles; 114 | } 115 | 116 | /** 117 | * Returns the salt that was originally used to encode the password. 118 | */ 119 | public function getSalt() 120 | { 121 | // See "Do you need to use a Salt?" at http://symfony.com/doc/current/cookbook/security/entity_provider.html 122 | // we're using bcrypt in security.yml to encode the password, so 123 | // the salt value is built-in and you don't have to generate one 124 | 125 | return; 126 | } 127 | 128 | /** 129 | * Removes sensitive data from the user. 130 | */ 131 | public function eraseCredentials() 132 | { 133 | // if you had a plainPassword property, you'd nullify it here 134 | // $this->plainPassword = null; 135 | } 136 | 137 | /** 138 | * @param string $apiToken 139 | */ 140 | public function setApiToken($apiToken) 141 | { 142 | $this->apiToken = $apiToken; 143 | } 144 | 145 | public function setLastLoginTime(\DateTime $lastLoginTime) 146 | { 147 | $this->lastLoginTime = $lastLoginTime; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/AppBundle/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace AppBundle\Repository; 13 | 14 | use Doctrine\ORM\EntityRepository; 15 | 16 | /** 17 | * This custom Doctrine repository is empty because so far we don't need any custom 18 | * method to query for application user information. But it's always a good practice 19 | * to define a custom repository that will be used when the application grows. 20 | * See http://symfony.com/doc/current/book/doctrine.html#custom-repository-classes 21 | * 22 | * @author Ryan Weaver 23 | * @author Javier Eguiluz 24 | */ 25 | class UserRepository extends EntityRepository 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /src/AppBundle/Tests/Controller/DefaultControllerTest.php: -------------------------------------------------------------------------------- 1 | request('GET', '/app/example'); 14 | 15 | $this->assertEquals(200, $client->getResponse()->getStatusCode()); 16 | $this->assertTrue($crawler->filter('html:contains("Homepage")')->count() > 0); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /web/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex app.php 7 | 8 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/app" should not resolve 9 | # to the front controller "/app.php" but be rewritten to "/app.php/app". 10 | 11 | Options -MultiViews 12 | 13 | 14 | 15 | RewriteEngine On 16 | 17 | # Determine the RewriteBase automatically and set it as environment variable. 18 | # If you are using Apache aliases to do mass virtual hosting or installed the 19 | # project in a subdirectory, the base path will be prepended to allow proper 20 | # resolution of the app.php file and to redirect to the correct URI. It will 21 | # work in environments without path prefix as well, providing a safe, one-size 22 | # fits all solution. But as you do not need it in this case, you can comment 23 | # the following 2 lines to eliminate the overhead. 24 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 25 | RewriteRule ^(.*) - [E=BASE:%1] 26 | 27 | # Sets the HTTP_AUTHORIZATION header removed by apache 28 | RewriteCond %{HTTP:Authorization} . 29 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 30 | 31 | # Redirect to URI without front controller to prevent duplicate content 32 | # (with and without `/app.php`). Only do this redirect on the initial 33 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 34 | # endless redirect loop (request -> rewrite to front controller -> 35 | # redirect -> request -> ...). 36 | # So in case you get a "too many redirects" error or you always get redirected 37 | # to the start page because your Apache does not expose the REDIRECT_STATUS 38 | # environment variable, you have 2 choices: 39 | # - disable this feature by commenting the following 2 lines or 40 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 41 | # following RewriteCond (best solution) 42 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 43 | RewriteRule ^app\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L] 44 | 45 | # If the requested filename exists, simply serve it. 46 | # We only want to let Apache serve files and not directories. 47 | RewriteCond %{REQUEST_FILENAME} -f 48 | RewriteRule .? - [L] 49 | 50 | # Rewrite all other queries to the front controller. 51 | RewriteRule .? %{ENV:BASE}/app.php [L] 52 | 53 | 54 | 55 | 56 | # When mod_rewrite is not available, we instruct a temporary redirect of 57 | # the start page to the front controller explicitly so that the website 58 | # and the generated links can still be used. 59 | RedirectMatch 302 ^/$ /app.php/ 60 | # RedirectTemp cannot be used instead 61 | 62 | 63 | -------------------------------------------------------------------------------- /web/app.php: -------------------------------------------------------------------------------- 1 | unregister(); 15 | $apcLoader->register(true); 16 | */ 17 | 18 | require_once __DIR__.'/../app/AppKernel.php'; 19 | //require_once __DIR__.'/../app/AppCache.php'; 20 | 21 | $kernel = new AppKernel('prod', false); 22 | $kernel->loadClassCache(); 23 | //$kernel = new AppCache($kernel); 24 | 25 | // When using the HttpCache, you need to call the method in your front controller instead of relying on the configuration parameter 26 | //Request::enableHttpMethodParameterOverride(); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /web/app_dev.php: -------------------------------------------------------------------------------- 1 | loadClassCache(); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /web/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/web/apple-touch-icon.png -------------------------------------------------------------------------------- /web/config.php: -------------------------------------------------------------------------------- 1 | getFailedRequirements(); 20 | $minorProblems = $symfonyRequirements->getFailedRecommendations(); 21 | 22 | ?> 23 | 24 | 25 | 26 | 27 | 28 | Symfony Configuration 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | 39 | 40 | 60 |
61 | 62 |
63 |
64 |
65 |

Welcome!

66 |

Welcome to your new Symfony project.

67 |

68 | This script will guide you through the basic configuration of your project. 69 | You can also do the same by editing the ‘app/config/parameters.yml’ file directly. 70 |

71 | 72 | 73 |

Major problems

74 |

Major problems have been detected and must be fixed before continuing:

75 |
    76 | 77 |
  1. getHelpHtml() ?>
  2. 78 | 79 |
80 | 81 | 82 | 83 |

Recommendations

84 |

85 | Additionally, toTo enhance your Symfony experience, 86 | it’s recommended that you fix the following: 87 |

88 |
    89 | 90 |
  1. getHelpHtml() ?>
  2. 91 | 92 |
93 | 94 | 95 | hasPhpIniConfigIssue()): ?> 96 |

* 97 | getPhpIniConfigPath()): ?> 98 | Changes to the php.ini file must be done in "getPhpIniConfigPath() ?>". 99 | 100 | To change settings, create a "php.ini". 101 | 102 |

103 | 104 | 105 | 106 |

Your configuration looks good to run Symfony.

107 | 108 | 109 | 118 |
119 |
120 |
121 |
Symfony Standard Edition
122 |
123 | 124 | 125 | -------------------------------------------------------------------------------- /web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knpuniversity/guard-tutorial/729558e31283af68d17b0be06e88221ca2260779/web/favicon.ico -------------------------------------------------------------------------------- /web/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | User-agent: * 5 | --------------------------------------------------------------------------------