Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Sign in
Toggle navigation
S
Silex
Project overview
Project overview
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Commits
Open sidebar
common
Silex
Commits
4b5ccc9a
Commit
4b5ccc9a
authored
Dec 17, 2015
by
Jérôme Tamarelle
Committed by
Fabien Potencier
Apr 29, 2016
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add support for the Guard component to the SecurityServiceProvider
parent
32bed6d4
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
400 additions
and
7 deletions
+400
-7
doc/cookbook/guard_authentication.rst
doc/cookbook/guard_authentication.rst
+182
-0
doc/providers/security.rst
doc/providers/security.rst
+7
-0
src/Silex/Provider/SecurityServiceProvider.php
src/Silex/Provider/SecurityServiceProvider.php
+71
-7
tests/Silex/Tests/Provider/SecurityServiceProviderTest.php
tests/Silex/Tests/Provider/SecurityServiceProviderTest.php
+61
-0
tests/Silex/Tests/Provider/SecurityServiceProviderTest/TokenAuthenticator.php
...ovider/SecurityServiceProviderTest/TokenAuthenticator.php
+79
-0
No files found.
doc/cookbook/guard_authentication.rst
0 → 100644
View file @
4b5ccc9a
How to Create a Custom Authentication System with Guard
=======================================================
Whether you need to build a traditional login form, an API token
authentication system or you need to integrate with some proprietary
single-sign-on system, the Guard component can make it easy... and fun!
In this example, you'll build an API token authentication system and
learn how to work with Guard.
Step 1) Create the Authenticator Class
--------------------------------------
Suppose you have an API where your clients will send an X-AUTH-TOKEN
header on each request. This token is composed of the username followed
by a password, separated by a colon (e.g. ``X-AUTH-TOKEN: coolguy:awesomepassword``).
Your job is to read this, find theassociated user (if any) and check
the password.
To create a custom authentication system, just create a class and make
it implement GuardAuthenticatorInterface. Or, extend the simpler
AbstractGuardAuthenticator. This requires you to implement six methods:
.. code-block:: php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
public function getCredentials(Request $request)
{
// Checks if the credential header is provided
if (!$token = $request->headers->get('X-AUTH-TOKEN')) {
return;
}
// Parse the header or ignore it if the format is incorrect.
if (false === strpos(':', $token)) {
return;
}
list($username, $secret) = explode(':', $token, 2);
return array(
'username' => $username,
'secret' => $secret,
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUsername($credentials['username']);
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// return true to cause authentication success
$encoder = $this->encoderFactory->getEncoder($user);
return $encoder->isPasswordValid(
$user->getPassword(),
$credentials['secret'],
$user->getSalt()
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data, 403);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required',
);
return new JsonResponse($data, 401);
}
public function supportsRememberMe()
{
return false;
}
}
Step 2) Configure the Authenticator
-----------------------------------
To finish this, register the class as a service:
.. code-block:: php
$app['app.token_authenticator'] = function ($app) {
return new App\Security\TokenAuthenticator($app['security.encoder_factory']);
};
Finally, configure your `security.firewalls` key to use this authenticator:
.. code-block:: php
$app['security.firewalls'] => array(
'main' => array(
'guard' => array(
'authenticators' => array(
'app.token_authenticator'
),
// Using more than 1 authenticator, you must specify
// which one is used as entry point.
// 'entry_point' => 'app.token_authenticator',
),
// configure where your users come from. Hardcode them, or load them from somewhere
// http://silex.sensiolabs.org/doc/providers/security.html#defining-a-custom-user-provider
'users' => array(
'victoria' => array('ROLE_USER', 'randomsecret'),
),
// 'anonymous' => true
),
);
.. note::
You can use many authenticators, they are executed by the order
they are configured.
You did it! You now have a fully-working API token authentication
system. If your homepage required ROLE_USER, then you could test it
under different conditions:
.. code-block:: bash
# test with no token
curl http://localhost:8000/
# {"message":"Authentication Required"}
# test with a bad token
curl -H "X-AUTH-TOKEN: alan" http://localhost:8000/
# {"message":"Username could not be found."}
# test with a working token
curl -H "X-AUTH-TOKEN: victoria:ransomsecret" http://localhost:8000/
# the homepage controller is executed: the page loads normally
For more details read the Symfony cookbook entry on
`How to Create aCustom Authentication System with Guard <http://symfony.com/doc/current/cookbook/security/guard-authentication.html>`_.
doc/providers/security.rst
View file @
4b5ccc9a
...
...
@@ -618,6 +618,13 @@ argument of your authentication factory (see above).
This example uses the authentication provider classes as described in the
Symfony `cookbook`_.
.. note::
Since Symfony 2.8, the Guard component simplify the creation of custom
authentication providers.
:doc:`How to Create a Custom Authentication System with Guard <cookbook/guard_authentication>`
Stateless Authentication
~~~~~~~~~~~~~~~~~~~~~~~~
...
...
src/Silex/Provider/SecurityServiceProvider.php
View file @
4b5ccc9a
...
...
@@ -58,6 +58,9 @@ use Symfony\Component\Security\Http\Logout\SessionLogoutHandler;
use
Symfony\Component\Security\Http\Logout\DefaultLogoutSuccessHandler
;
use
Symfony\Component\Security\Http\AccessMap
;
use
Symfony\Component\Security\Http\HttpUtils
;
use
Symfony\Component\Security\Guard\GuardAuthenticatorHandler
;
use
Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener
;
use
Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider
;
/**
* Symfony Security component Provider.
...
...
@@ -148,12 +151,14 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
};
// generate the build-in authentication factories
foreach
(
array
(
'logout'
,
'pre_auth'
,
'form'
,
'http'
,
'remember_me'
,
'anonymous'
)
as
$type
)
{
foreach
(
array
(
'logout'
,
'pre_auth'
,
'
guard'
,
'
form'
,
'http'
,
'remember_me'
,
'anonymous'
)
as
$type
)
{
$entryPoint
=
null
;
if
(
'http'
===
$type
)
{
$entryPoint
=
'http'
;
}
elseif
(
'form'
===
$type
)
{
$entryPoint
=
'form'
;
}
elseif
(
'guard'
===
$type
)
{
$entryPoint
=
'guard'
;
}
$app
[
'security.authentication_listener.factory.'
.
$type
]
=
$app
->
protect
(
function
(
$name
,
$options
)
use
(
$type
,
$app
,
$entryPoint
)
{
...
...
@@ -165,9 +170,14 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
$app
[
'security.authentication_listener.'
.
$name
.
'.'
.
$type
]
=
$app
[
'security.authentication_listener.'
.
$type
.
'._proto'
](
$name
,
$options
);
}
$provider
=
'anonymous'
===
$type
?
'anonymous'
:
'dao'
;
$provider
=
'dao'
;
if
(
'anonymous'
===
$type
)
{
$provider
=
'anonymous'
;
}
elseif
(
'guard'
===
$type
)
{
$provider
=
'guard'
;
}
if
(
!
isset
(
$app
[
'security.authentication_provider.'
.
$name
.
'.'
.
$provider
]))
{
$app
[
'security.authentication_provider.'
.
$name
.
'.'
.
$provider
]
=
$app
[
'security.authentication_provider.'
.
$provider
.
'._proto'
](
$name
);
$app
[
'security.authentication_provider.'
.
$name
.
'.'
.
$provider
]
=
$app
[
'security.authentication_provider.'
.
$provider
.
'._proto'
](
$name
,
$options
);
}
return
array
(
...
...
@@ -180,7 +190,7 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
}
$app
[
'security.firewall_map'
]
=
function
(
$app
)
{
$positions
=
array
(
'logout'
,
'pre_auth'
,
'form'
,
'http'
,
'remember_me'
,
'anonymous'
);
$positions
=
array
(
'logout'
,
'pre_auth'
,
'
guard'
,
'
form'
,
'http'
,
'remember_me'
,
'anonymous'
);
$providers
=
array
();
$configs
=
array
();
foreach
(
$app
[
'security.firewalls'
]
as
$name
=>
$firewall
)
{
...
...
@@ -285,7 +295,7 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
$listener
=
$app
[
$listenerId
];
if
(
isset
(
$app
[
'security.remember_me.service.'
.
$name
]))
{
if
(
$listener
instanceof
AbstractAuthenticationListener
)
{
if
(
$listener
instanceof
AbstractAuthenticationListener
||
$listener
instanceof
GuardAuthenticationListener
)
{
$listener
->
setRememberMeServices
(
$app
[
'security.remember_me.service.'
.
$name
]);
}
if
(
$listener
instanceof
LogoutListener
)
{
...
...
@@ -420,6 +430,27 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
};
});
$app
[
'security.authentication_listener.guard._proto'
]
=
$app
->
protect
(
function
(
$providerKey
,
$options
)
use
(
$app
,
$that
)
{
return
function
()
use
(
$app
,
$providerKey
,
$options
,
$that
)
{
if
(
!
isset
(
$app
[
'security.authentication.guard_handler'
]))
{
$app
[
'security.authentication.guard_handler'
]
=
new
GuardAuthenticatorHandler
(
$app
[
'security.token_storage'
],
$app
[
'dispatcher'
]);
}
$authenticators
=
array
();
foreach
(
$options
[
'authenticators'
]
as
$authenticatorId
)
{
$authenticators
[]
=
$app
[
$authenticatorId
];
}
return
new
GuardAuthenticationListener
(
$app
[
'security.authentication.guard_handler'
],
$app
[
'security.authentication_manager'
],
$providerKey
,
$authenticators
,
$app
[
'logger'
]
);
};
});
$app
[
'security.authentication_listener.form._proto'
]
=
$app
->
protect
(
function
(
$name
,
$options
)
use
(
$app
,
$that
)
{
return
function
()
use
(
$app
,
$name
,
$options
,
$that
)
{
$that
->
addFakeRoute
(
...
...
@@ -545,7 +576,24 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
};
});
$app
[
'security.authentication_provider.dao._proto'
]
=
$app
->
protect
(
function
(
$name
)
use
(
$app
)
{
$app
[
'security.entry_point.guard._proto'
]
=
$app
->
protect
(
function
(
$name
,
array
$options
)
use
(
$app
)
{
if
(
isset
(
$options
[
'entry_point'
]))
{
// if it's configured explicitly, use it!
return
$app
[
$options
[
'entry_point'
]];
}
$authenticatorIds
=
$options
[
'authenticators'
];
if
(
count
(
$authenticatorIds
)
==
1
)
{
// if there is only one authenticator, use that as the entry point
return
$app
[
reset
(
$authenticatorIds
)];
}
// we have multiple entry points - we must ask them to configure one
throw
new
\LogicException
(
sprintf
(
'Because you have multiple guard configurators, you need to set the "guard.entry_point" key to one of you configurators (%s)'
,
implode
(
', '
,
$authenticatorIds
)
));
});
$app
[
'security.authentication_provider.dao._proto'
]
=
$app
->
protect
(
function
(
$name
,
$options
)
use
(
$app
)
{
return
function
()
use
(
$app
,
$name
)
{
return
new
DaoAuthenticationProvider
(
$app
[
'security.user_provider.'
.
$name
],
...
...
@@ -557,7 +605,23 @@ class SecurityServiceProvider implements ServiceProviderInterface, EventListener
};
});
$app
[
'security.authentication_provider.anonymous._proto'
]
=
$app
->
protect
(
function
(
$name
)
use
(
$app
)
{
$app
[
'security.authentication_provider.guard._proto'
]
=
$app
->
protect
(
function
(
$name
,
$options
)
use
(
$app
)
{
return
function
()
use
(
$app
,
$name
,
$options
)
{
$authenticators
=
array
();
foreach
(
$options
[
'authenticators'
]
as
$authenticatorId
)
{
$authenticators
[]
=
$app
[
$authenticatorId
];
}
return
new
GuardAuthenticationProvider
(
$authenticators
,
$app
[
'security.user_provider.'
.
$name
],
$name
,
$app
[
'security.user_checker'
]
);
};
});
$app
[
'security.authentication_provider.anonymous._proto'
]
=
$app
->
protect
(
function
(
$name
,
$options
)
use
(
$app
)
{
return
function
()
use
(
$app
,
$name
)
{
return
new
AnonymousAuthenticationProvider
(
$name
);
};
...
...
tests/Silex/Tests/Provider/SecurityServiceProviderTest.php
View file @
4b5ccc9a
...
...
@@ -120,6 +120,33 @@ class SecurityServiceProviderTest extends WebTestCase
$this
->
assertEquals
(
'admin'
,
$client
->
getResponse
()
->
getContent
());
}
public
function
testGuardAuthentication
()
{
if
(
!
class_exists
(
'Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator'
))
{
$this
->
markTestSkipped
(
'The guard component require Symfony 2.8+'
);
}
$app
=
$this
->
createApplication
(
'guard'
);
$client
=
new
Client
(
$app
);
$client
->
request
(
'get'
,
'/'
);
$this
->
assertEquals
(
401
,
$client
->
getResponse
()
->
getStatusCode
(),
'The entry point is configured'
);
$this
->
assertEquals
(
'{"message":"Authentication Required"}'
,
$client
->
getResponse
()
->
getContent
());
$client
->
request
(
'get'
,
'/'
,
array
(),
array
(),
array
(
'HTTP_X_AUTH_TOKEN'
=>
'lili:not the secret'
));
$this
->
assertEquals
(
403
,
$client
->
getResponse
()
->
getStatusCode
(),
'User not found'
);
$this
->
assertEquals
(
'{"message":"Username could not be found."}'
,
$client
->
getResponse
()
->
getContent
());
$client
->
request
(
'get'
,
'/'
,
array
(),
array
(),
array
(
'HTTP_X_AUTH_TOKEN'
=>
'victoria:not the secret'
));
$this
->
assertEquals
(
403
,
$client
->
getResponse
()
->
getStatusCode
(),
'Invalid credentials'
);
$this
->
assertEquals
(
'{"message":"Invalid credentials."}'
,
$client
->
getResponse
()
->
getContent
());
$client
->
request
(
'get'
,
'/'
,
array
(),
array
(),
array
(
'HTTP_X_AUTH_TOKEN'
=>
'victoria:victoriasecret'
));
$this
->
assertEquals
(
'victoria'
,
$client
->
getResponse
()
->
getContent
());
}
public
function
testUserPasswordValidatorIsRegistered
()
{
$app
=
new
Application
();
...
...
@@ -356,4 +383,38 @@ class SecurityServiceProviderTest extends WebTestCase
return
$app
;
}
private
function
addGuardAuthentication
(
$app
)
{
$app
[
'app.authenticator.token'
]
=
function
(
$app
)
{
return
new
SecurityServiceProviderTest\TokenAuthenticator
(
$app
);
};
$app
->
register
(
new
SecurityServiceProvider
(),
array
(
'security.firewalls'
=>
array
(
'guard'
=>
array
(
'pattern'
=>
'^.*$'
,
'form'
=>
true
,
'guard'
=>
array
(
'authenticators'
=>
array
(
'app.authenticator.token'
,
),
),
'users'
=>
array
(
'victoria'
=>
array
(
'ROLE_USER'
,
'victoriasecret'
),
),
),
),
));
$app
->
get
(
'/'
,
function
()
use
(
$app
)
{
$user
=
$app
[
'security.token_storage'
]
->
getToken
()
->
getUser
();
$content
=
is_object
(
$user
)
?
$user
->
getUsername
()
:
'ANONYMOUS'
;
return
$content
;
})
->
bind
(
'homepage'
);
return
$app
;
}
}
tests/Silex/Tests/Provider/SecurityServiceProviderTest/TokenAuthenticator.php
0 → 100644
View file @
4b5ccc9a
<?php
/*
* This file is part of the Silex framework.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace
Silex\Tests\Provider\SecurityServiceProviderTest
;
use
Symfony\Component\HttpFoundation\Request
;
use
Symfony\Component\HttpFoundation\JsonResponse
;
use
Symfony\Component\Security\Core\User\UserInterface
;
use
Symfony\Component\Security\Core\User\UserProviderInterface
;
use
Symfony\Component\Security\Guard\AbstractGuardAuthenticator
;
use
Symfony\Component\Security\Core\Authentication\Token\TokenInterface
;
use
Symfony\Component\Security\Core\Exception\AuthenticationException
;
/**
* This class is used to test "guard" authentication with the SecurityServiceProvider.
*/
class
TokenAuthenticator
extends
AbstractGuardAuthenticator
{
public
function
getCredentials
(
Request
$request
)
{
if
(
!
$token
=
$request
->
headers
->
get
(
'X-AUTH-TOKEN'
))
{
return
;
}
list
(
$username
,
$secret
)
=
explode
(
':'
,
$token
);
return
array
(
'username'
=>
$username
,
'secret'
=>
$secret
,
);
}
public
function
getUser
(
$credentials
,
UserProviderInterface
$userProvider
)
{
return
$userProvider
->
loadUserByUsername
(
$credentials
[
'username'
]);
}
public
function
checkCredentials
(
$credentials
,
UserInterface
$user
)
{
// This is not a safe way of validating a password.
return
$user
->
getPassword
()
===
$credentials
[
'secret'
];
}
public
function
onAuthenticationSuccess
(
Request
$request
,
TokenInterface
$token
,
$providerKey
)
{
return
;
}
public
function
onAuthenticationFailure
(
Request
$request
,
AuthenticationException
$exception
)
{
$data
=
array
(
'message'
=>
strtr
(
$exception
->
getMessageKey
(),
$exception
->
getMessageData
()),
);
return
new
JsonResponse
(
$data
,
403
);
}
public
function
start
(
Request
$request
,
AuthenticationException
$authException
=
null
)
{
$data
=
array
(
'message'
=>
'Authentication Required'
,
);
return
new
JsonResponse
(
$data
,
401
);
}
public
function
supportsRememberMe
()
{
return
false
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment