Zend Expressive com OAuth 2 e JWT

Quem sou eu?

Zend Expressive

  • Middlewares
  • PSR-7
  • PSR-15

API

if (!$this->checkAuthentication()) {
  throw new InvalidCredentialsException('Usuário inválido.');
}

if (!$this->checkAuthorization()) {
  throw new UnauthorizedException('Você não possui permissão.');
}

return new MyResponse([]);

Middlewares

Representação de Middlewares

PSR-7

  • Psr\Http\Message\ServerRequestInterface
  • Psr\Http\Message\ResponseInterface
  • Psr\Http\Message\MessageInterface
  • Psr\Http\Message\RequestInterface
  • Psr\Http\Message\StreamInterface
  • Psr\Http\Message\UriInterface
  • Psr\Http\Message\UploadedFileInterface

PSR-15

use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Interop\Http\ServerMiddleware\DelegateInterface as Delegate;

class MyMiddleware implements MiddlewareInterface {
  public function process(Request $request, Delegate $delegate) {
    // Processa a requisição e retorna a resposta
    // Ou chama o próximo middleware capaz de responder
    return $delegate->process($request);
  }
}

OAuth 2

  • Protocolo de autorização
    • Para aplicações terceiras
    • Para o próprio cliente

Papéis

  • Resource Owner: Usuário
  • Client: Aplicação
  • Authorization Server: API
  • Resource Server: API *

Tokens

  • Access Token
  • Refresh Token

Grant Types

  • Client credentials
  • Authorization code
  • Implicit
  • Resource owner credentials
Grant types

Referências

JWT

  • JSON Web Tokens
  • Padrão para representar claims
  • Dividido em header, payload e signature
  • Assinado com HMAC ou RSA

Claims

  • Audience: aud
  • Subject: sub
  • Identifier: jti
  • Issuer: iss
  • Issued At: iat
  • Not Before: nbf
  • Expiration: exp

Referências

Exemplo

              eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiIxMjM0NTYiLCJhdWQiOiJQSFAgQ29uZmVyZW5jZSAyMDE3Iiwic3ViIjoiRXhlbXBsbyBKV1QiLCJ1c2VyIjp7ImlkIjoxLCJuYW1lIjoidmNhbXBpdGVsbGkifSwic2NvcGVzIjpbInNwZWFrZXIiXX0.8OZ6wkqtqfG9YjI5OGNN4Isyr5kdeZzhYIqHCC9u4cA
            
{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "jti": "123456",
  "aud": "PHP Conference 2017",
  "sub": "Exemplo JWT",
  "user": {
  "id": 1,
  "name": "vcampitelli"
  },
  "scopes": [
  "speaker"
  ]
}
HMACSHA256(
  base64Url(header) + "." +  
  base64Url(payload),
  "kJf;N%Z$K1oQ>dPN|LB?Vno$"
)

Na Prática

Instalação

composer create-project zendframework/zend-expressive-skeleton
composer require league/oauth2-server
Screenshot da instalação do zend-expressive-skeleton

Classes

  • ConfigProvider
  • ResourceServerMiddleware
  • TokenAction
  • AuthorizationServer
  • AccessTokenEntity
  • AccessTokenRepository
  • ClientEntity
  • ClientRepository
  • ScopeEntity
  • ScopeRepository

ConfigProvider

public function getRoutes() {
  return [
    [
      'name'            => 'oauth.token',
      'path'            => '/token',
      'middleware'      => TokenAction::class,
      'allowed_methods' => ['POST']
    ]
  ];
}

ConfigProvider

public function getDependencies() {
  return [
    'factories'  => [
      TokenAction::class              => TokenActionFactory::class,
      ClientRepository::class         => RepositoryFactory::class,
      ScopeRepository::class          => RepositoryFactory::class,
      AccessTokenRepository::class    => RepositoryFactory::class,
      ResourceServerMiddleware::class => ResourceServerFactory::class,
      AuthorizationServer::class      => AuthorizationServerFactory::class
    ]
  ];
}

ResourceServerMiddleware

public function process(
  ServerRequestInterface $request,
  DelegateInterface $delegate
) {
  $request = $this->resourceServer->validateAuthenticatedRequest($request);

  $clientId = $request->getAttribute('oauth_client_id');
  if ((empty($clientId)) || (!$this->isClientAuthorized($clientId, $request))) {
    throw OAuthServerException::accessDenied('Unauthorized client');
  }

  return $delegate->process($request);
}

TokenAction

public function process(
  ServerRequestInterface $request,
  DelegateInterface $delegate
) {
  return $this->authorizationServer->handleTokenRequest($request);
}

AuthorizationServer

public function handleTokenRequest(ServerRequestInterface $request) {
  try {
    $response = new \Zend\Diactoros\Response\JsonResponse([]);
    return $this->authorizationServer
      ->respondToAccessTokenRequest($request, $response);
  } catch (\League\OAuth2\Server\Exception\OAuthServerException $e) {
    return $e->generateHttpResponse($response);
  }
}

AuthorizationServerFactory

public function __invoke(ContainerInterface $container) {
  $config = $container->get('config')['oauth'];
  $server = new \League\OAuth2\Server\AuthorizationServer(
    $container->get(ClientRepository::class),
    $container->get(AccessTokenRepository::class),
    $container->get(ScopeRepository::class),
    $config['private_key'],
    $config['encryption_key']
  );
  $server->enableGrantType(
    new \League\OAuth2\Server\Grant\ClientCredentialsGrant(),
    new \DateInterval('PT1H') // 1 hora para expiração
  );
  return $server;
}

ScopeRepository

public function finalizeScopes(
  array $scopes,
  $grantType,
  ClientEntityInterface $clientEntity,
  $userIdentifier = null
) {
  // Valida os scopes que o cliente está pedindo,
  // adicionando novos e removendo os inválidos
  return $scopes;
}

AccessTokenEntity

public function convertToJWT(\League\OAuth2\Server\CryptKey $key) {
  return (new \Lcobucci\JWT\Builder())
    ->setAudience('PHP Conference 2017')
    ->setSubject('OAuth 2 com JWT')
    ->set('scopes', $this->getScopes())
    ->setIssuedAt(time())
    ->setExpiration($this->getExpiryDateTime())
    ->sign(
      new \Lcobucci\JWT\Signer\Rsa\Sha256(),
      (new \Lcobucci\JWT\Signer\Key())->getPrivateKey(
        $key->getKeyPath(),
        $key->getPassphrase()
      )
    )->getToken();
}

Exemplo

curl -X POST -d client_id=api -d client_secret=mysecret \
 -d scope="user_list user_create" https://api.local/token
{
  "token_type": "Bearer",
  "expires_in": 3600,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjA1ODI3NTJkOTNiMTI4ZTYifQ.eyJhdWQiOiIxIiwic3ViIjoiIiwianRpIjoiMDU4Mjc1MmQ5M2IxMjhlNiIsInNjb3BlcyI6W10sImlhdCI6MTUwNzQwNTI0NCwiZXhwIjoxNTA3NDA4ODQ0fQ.P7V0TBzBOiTXqUk48wMFMhEPYjvT3EXOMqztRHXGqAmZJm7Uhd7jRejwwE-YBPu4lOcRhDsxoYbM5b_VVc1BRgGf824WpWdW1Mg5FALHTlGJqLvVmHYbZqPahBNei4_BXJmtZ7e7Vp9IkjY3qTR4W9h4BjieI7P0TLTIS0S3Q1c"
}
curl -X GET \
 -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6IjA1ODI3NTJkOTNiMTI4ZTYifQ.eyJhdWQiOiIxIiwic3ViIjoiIiwianRpIjoiMDU4Mjc1MmQ5M2IxMjhlNiIsInNjb3BlcyI6W10sImlhdCI6MTUwNzQwNTI0NCwiZXhwIjoxNTA3NDA4ODQ0fQ.P7V0TBzBOiTXqUk48wMFMhEPYjvT3EXOMqztRHXGqAmZJm7Uhd7jRejwwE-YBPu4lOcRhDsxoYbM5b_VVc1BRgGf824WpWdW1Mg5FALHTlGJqLvVmHYbZqPahBNei4_BXJmtZ7e7Vp9IkjY3qTR4W9h4BjieI7P0TLTIS0S3Q1c" \
 https://api.local/users
{ ... }