Autenticazione delle api con jwt e symfony (Italian)
8 de Feb de 2016•0 recomendaciones•819 vistas
Descargar para leer sin conexión
Denunciar
Tecnología
A quick overview of the JWT token format and how it can be used to authenticate REST APIs even without implementing the full Openid Connect stack. The examples show also a TDD approach
3. Marco Albarelli
Freelance
Full stack developer web e mobile
Sysadmin
PHP, javascript, Android, Go, java
https://www.adhocmobile.it
https://github.com/marcoalbarelli
6. Un mondo fatto
di API
Ci sono più microservizi usati da
client di ogni genere:
JS, mobile, server, cli
Forniti da server di ogni genere:
nodejs, php, .Net, Java, ESB
Potenzialmente identità multiple
Condividere fra tutti la sessione
serverside diventa impraticabile
7. Un mondo fatto
di API
Un nuovo standard
Per permettere a sistemi diversi di
interagire serviva un nuovo
standard:
OpenID Connect
google lo usa in produzione https:
//developers.google.
com/identity/protocols/OpenIDConn
ect
8. Un mondo fatto
di API
Un nuovo standard
Obiettivo:
Passare da
Cookie: PHPSESSID
a
Authorization: Bearer mqZSaG...
9. Un mondo fatto
di API
Un nuovo standard:
OpenID Connect
Si basa su Oauth2.0
Usa dei token particolari: JWT
Interoperabile
Molto più semplice di SAML
11. JWT, cos’è
Una convenzione: RFC 7519
Una stringa: 3 blocchi di testo
Base64 encoded uniti da un punto
Supporta JOSE: Json Object Signing
and Encryption
Composto di header, corpo e firma
Supporta “claims”
21. La ricetta
Installiamo symfony come da
manuale
Aggiungiamo al composer.json:
"friendsofsymfony/user-bundle":
"2.0.x-dev",
"firebase/php-jwt": "^3.0"
composer update
22. La ricetta
Testiamo due scenari:
Richiesta con token non valido e ci
aspettiamo un codice 401
Richiesta con token valido e ci
aspettiamo un codice 200
Cosa stiamo per fare
23. Scriviamo il primo test:
public function testApiEndpointsAreInaccessibleWithAnInvalidJWTAuthorizationHeader($method,$route,$params){
$this->setupMocksWithoutExpectations();
$router = $this->container->get('router');
$uri = $router->generate($route,$params);
$this->client->setServerParameter('HTTP_Authorization',
'Bearer'.$this->createInvalidJWT($this->container->getParameter('secret')));
$this->client->request($method,$uri,$params);
$this->assertEquals(401,$this->client->getResponse()->getStatusCode());
}
Testiamo che la chiamata ottenga risposta (401)
Il token non è valido
24. Scriviamo il primo test:
public function testApiEndpointsAreInaccessibleWithAnInvalidJWTAuthorizationHeader($method,$route,$params){
$this->setupMocksWithoutExpectations();
$router = $this->container->get('router');
$uri = $router->generate($route,$params);
$this->client->setServerParameter('HTTP_Authorization',
'Bearer'.$this->createInvalidJWT($this->container->getParameter('secret')));
$this->client->request($method,$uri,$params);
$this->assertEquals(401,$this->client->getResponse()->getStatusCode());
}
Testiamo che la chiamata ottenga risposta (401)
Il token non è valido
$this->client->setServerParameter(
'HTTP_Authorization',
'Bearer'.$this->createInvalidJWT(
$this->container->getParameter('secret')
));
26. JWT Test doubles
public function createValidJWT($key,$role = 'ROLE_USER',$apiKey =
null)
{
$now = new DateTime('now');
$role = 'ROLE_USER';
if($apiKey == null){
$apiKey = md5(rand(0,10));
}
$token = array(
"iss" => "http://example.org",
"aud" => "http://example.com",
"iat" => $now->getTimestamp(),
"nbf" => $now->sub(new
DateInterval('P1D'))->getTimestamp(),
"role" => $role,
Constants::JWT_APIKEY_PARAMETER_NAME => $apiKey
);
return JWT::encode($token,$key);
}
public function createInvalidJWT($key,$role = 'ROLE_USER')
{
$now = new DateTime('now');
$role = 'ROLE_USER';
//Missing apikey and valid since tomorrow
$token = array(
"iss" => "http://example.org",
"aud" => "http://example.com",
"iat" => $now->getTimestamp(),
"nbf" => $now->add(new
DateInterval('P1D'))->getTimestamp(),
"role" => $role
);
return JWT::encode($token,$key);
}
"nbf" => $now->add(
new DateInterval('P1D'))->getTimestamp()
);
27. Scriviamo il primo test:
CONTROLLER
/**
* @Route("/hello/{name}", name="api_hello")
*/
public function indexAction($name)
{
return new Response(json_encode(array('hello'=>$name)));
}
Come si vede per usare un autenticatore custom
non dobbiamo modificare il controller
28. Scriviamo il primo test:
Lanciamo i test adesso e siamo in
profondo rosso: dobbiamo
configurare un bel po’ di cose:
app/config/security.yml
security:
...
firewalls:
...
api_area:
pattern: ^/api/
provider: api_chain_provider
stateless: true
entry_point: marcoalbarelli.api_user_auth_entrypoint
anonymous: ~
simple_preauth:
authenticator: marcoalbarelli.api_user_authenticator
access_control:
- { path: ^/api/status, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Entry point
Authenticator vero e proprio
Due cose principali:
29. Scriviamo il primo test:
Lanciamo i test adesso e siamo in
profondo rosso: dobbiamo
configurare un bel po’ di cose:
app/config/security.yml
security:
...
firewalls:
...
api_area:
pattern: ^/api/
provider: api_chain_provider
stateless: true
entry_point: marcoalbarelli.api_user_auth_entrypoint
anonymous: ~
simple_preauth:
authenticator: marcoalbarelli.api_user_authenticator
access_control:
- { path: ^/api/status, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Entry point
Authenticator vero e proprio
Due cose principali:
authenticator: marcoalbarelli.
api_user_authenticator
...
marcoalbarelli.api_user_authenticator:
class: MarcoalbarelliAPIBundleServiceAPIUserAuthenticator
arguments:
- @marcoalbarelli.api_user_provider
- @marcoalbarelli.jwt_checker
30. Entry point:
public function testAuthEntrypointGives401ErrorForMissingJWT(){
$authException = new AuthenticationException("missing JWT");
$request = new Request();
$service = $this->container->get('marcoalbarelli.
api_user_auth_entrypoint');
$response = $service->start($request,$authException);
$this->assertTrue($response instanceof Response);
$this->assertEquals('application/json',$response->headers->get
('Content-Type'));
$this->assertEquals('OpenID realm="api_area"',$response-
>headers->get('WWW-Authenticate'));
}
/**
* Starts the authentication scheme.
*
* @param Request $request The request that resulted in an
AuthenticationException
* @param AuthenticationException $authException The exception that
started the authentication process
*
* @return Response
*/
public function start(Request $request, AuthenticationException
$authException = null)
{
$content = array('success'=>false);
$response = new Response(json_encode($content),401);
$response->headers->set('Content-Type','application/json');
$response->headers->set('WWW-Authenticate','OpenID realm="
api_area"'); //TODO: retrieve the firewall name dynamically
return $response;
}
31. SimplePreAuthenticatorInterface:
/**
* @expectedException Exception
*/
public function testAuthenticatorThrowsExceptionIfRequestIsInvalid(){
$jwt = $this->createInvalidJWT($this->container->getParameter
('secret'));
$request = new Request();
$request->headers->add(array('Authorization'=> Constants::
JWT_BEARER_PREFIX .$jwt));
$service = $this->container->get('marcoalbarelli.
api_user_authenticator');
$service->createToken($request,'pippo');
}
public function createToken(Request $request, $providerKey)
{
$authorizationHeader = $request->headers->get('Authorization');
…
$encodedJWT = $authorizationHeader[1];
try {
$jwt = $this->jwtCheckerService->
decodeToken($encodedJWT);
} catch (Exception $exception){
throw new AuthenticationException
($exception->
getMessage());
}
$user = $this->userProvider->findUserByAPIKey($jwt->$apiKeyName);
...
$token = new PreAuthenticatedToken($user,$encodedJWT,$providerKey);
return $token;
}
32. SimplePreAuthenticatorInterface:
/**
* @expectedException Exception
*/
public function testAuthenticatorThrowsExceptionIfRequestIsInvalid(){
$jwt = $this->createInvalidJWT($this->container->getParameter
('secret'));
$request = new Request();
$request->headers->add(array('Authorization'=> Constants::
JWT_BEARER_PREFIX .$jwt));
$service = $this->container->get('marcoalbarelli.
api_user_authenticator');
$service->createToken($request,'pippo');
}
public function createToken(Request $request, $providerKey)
{
$authorizationHeader = $request->headers->get('Authorization');
…
$encodedJWT = $authorizationHeader[1];
try {
$jwt = $this->jwtCheckerService->
decodeToken($encodedJWT);
} catch (Exception $exception){
throw new AuthenticationException
($exception->
getMessage());
}
$user = $this->userProvider->findUserByAPIKey($jwt->$apiKeyName);
...
$token = new PreAuthenticatedToken($user,$encodedJWT,$providerKey);
return $token;
}
Il fulcro di tutto:
$jwt = $this->jwtCheckerService->
decodeToken($encodedJWT);
33. JWT Checker
/**
* @expectedException Exception
*/
public function testServiceThrowsExceptionForInvalidJWTToken(){
$key = $key = $this->container->getParameter('secret');
$token = $this->createInvalidJWT($key);
$service = $this->container->get('marcoalbarelli.jwt_checker');
$service->decodeToken($token);
}
//TODO: creare un dataprovider che copra
esplicitamente tutti i casi di invalidità
/**
* @var string $secret The secret for this deployment (from parameters.
yml)
*/
private $secret;
/**
* @var array $algs The algs for JWT signing (from parameters.yml)
*/
private $algs;
public function __construct($secret, $algs)
{
$this->secret = $secret;
$this->algs = $algs;
}
public function decodeToken($token)
{
return JWT::decode($token, $this->secret,
$this->algs);
}
34. JWT Checker
/**
* @expectedException Exception
*/
public function testServiceThrowsExceptionForInvalidJWTToken(){
$key = $key = $this->container->getParameter('secret');
$token = $this->createInvalidJWT($key);
$service = $this->container->get('marcoalbarelli.jwt_checker');
$service->decodeToken($token);
}
//TODO: creare un dataprovider che copra
esplicitamente tutti i casi di invalidità
/**
* @var string $secret The secret for this deployment (from parameters.
yml)
*/
private $secret;
/**
* @var array $algs The algs for JWT signing (from parameters.yml)
*/
private $algs;
public function __construct($secret, $algs)
{
$this->secret = $secret;
$this->algs = $algs;
}
public function decodeToken($token)
{
return JWT::decode($token, $this->secret,
$this->algs);
}
La prima implementazione:
solo correttezza formale
return JWT::decode($token, $this-
>secret, $this->algs);
35. Scriviamo il secondo test:
public function testApiEndpointsAreAccessibleWithAValidJWTAuthorizationHeader($method,$route,$params){
$this->setupMocks();
$router = $this->container->get('router');
$uri = $router->generate($route,$params);
$this->client->setServerParameter('HTTP_Authorization',
'Bearer '.$this->createValidJWT($this->container->getParameter('secret')));
$this->client->request($method,$uri,$params);
$this->assertEquals(200,$this->client->getResponse()->getStatusCode());
}
Testiamo che la chiamata ottenga risposta
Praticamente identico al precedente, tranne che per il token (stavolta valido)
36. SimplePreAuthenticatorInterface:
public function
testAuthenticatorCreatesValidTokenIfRequestIsValidAnUserIsPresent(){
$jwt = $this->createValidJWT($this->container->getParameter
('secret'));
$request = new Request();
$request->headers->add(array('Authorization'=> Constants::
JWT_BEARER_PREFIX .$jwt));
$this->container->set('marcoalbarelli.api_user_provider',$this-
>getMockedUserProvider());
$service = $this->container->get('marcoalbarelli.
api_user_authenticator');
$preauthenticatedToken = $service->createToken($request,'pippo');
$this->assertNotNull($preauthenticatedToken);
$this->assertEquals($preauthenticatedToken->getCredentials(),
$jwt);
}
public function createToken(Request $request, $providerKey)
{
….
//Tutto ok
$token = new PreAuthenticatedToken($user,
$encodedJWT,$providerKey);
return $token;
}
37. Il cuore dell’autenticazione
try {
$jwt = $this->jwtCheckerService->decodeToken($encodedJWT);
} catch (Exception $exception){
throw new AuthenticationException($exception->getMessage());
}
if( !isset($jwt->$apiKeyName)){
throw new BadCredentialsException('Invalid JWT');
}
$user = $this->userProvider->findUserByAPIKey($jwt->$apiKeyName);
if($user == null){
throw new UsernameNotFoundException("Invalid User");
}
…
$token = new PreAuthenticatedToken($user,$encodedJWT,$providerKey);
return $token;
Qui possiamo aggiungere una miriade
di controlli sia sul nostro sistema che
su altri (grazie all’interoperabilità
offerta da JWT)
39. Prossimi passi
Implementazione di tutto il flusso
OpenID Connect
Implementazione dei token JWT
cifrati
Implementazione di un AP con
symfony (soon on a github near you)
41. Conclusioni
Symfony + JWT
Autenticazione API
Abbiamo visto rapidamente come
creare un autenticatore per delle API
che non si appoggia ai cookie di
sessione
Abbiamo visto come sia semplice
farlo con approccio TDD, essenziale
in ambito di sicurezza
Abbiamo iniziato ad usare un nuovo
standard che ci renderà più facile
integrarci con OpenID Connect