ridibooks/oauth2
, (*1)
์๊ฐ
- OAuth2 ํด๋ผ์ด์ธํธ์ ๋ฆฌ์์ค ์๋ฒ๋ฅผ ๊ตฌ์ถํ๊ธฐ ์ํ PHP ๋ผ์ด๋ธ๋ฌ๋ฆฌ์
๋๋ค.
- Ridi ์คํ์ผ ๊ฐ์ด๋(๋ด๋ถ ์๋น์ค๊ฐ์ SSO)์ ๋ฐ๋ผ ์์ฑ ๋์์ต๋๋ค.
- JWK Caching ๋ฅผ ์ ํ์ ์ผ๋ก ์ง์ํฉ๋๋ค. psr-6์ ๊ตฌํ์ฒด๋ฅผ JwtTokenValidator์ ์ฃผ์
ํ๋ฉด ์บ์ฑ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
Requirements
-
PHP 7.2 or higher
-
php7.2-gmp web-token decryption ๋ชจ๋์ ์ํด์๋ php7.2-gmp ๋ฅผ os ๋ด์ ์ค์นํด์ค์ผ ํฉ๋๋ค.
๋ฐ๋ผ์ ์ด ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํด๋ผ์ด์ธํธ๋ค์ OS ํน์ ๋์ปค ์ด๋ฏธ์ง ๋ด์ ๊ผญ ์ค์นํด์ฃผ์๊ธธ ๋ฐ๋๋๋ค. ์ฐธ๊ณ PR
-
silex/silex v1.3.x (optional)
-
symfony/symfony v4.x.x (optional)
-
guzzlehttp/guzzle (optional)
Installation
composer require ridibooks/oauth2
Usage
JwtTokenValidator Without Caching
use Ridibooks\OAuth2\Authorization\Validator\JwtTokenValidator;
$access_token = '...';
try {
$jwk_url = $this->configs['jwk_url'];
$validator = new JwtTokenValidator($jwk_url);
$validator->validateToken($access_token);
} catch (AuthorizationException $e) {
// handle exception
}
JwtTokenValidator With Caching
use Ridibooks\OAuth2\Authorization\Validator\JwtTokenValidator;
$access_token = '...';
try {
$jwk_url = $this->configs['jwk_url'];
$cache_item_pool = new FilesystemAdapter(); // [psr-6](https://www.php-fig.org/psr/psr-6/) Implementation Adaptor
$validator = new JwtTokenValidator($jwk_url, $cache_item_pool);
$validator->validateToken($access_token);
} catch (AuthorizationException $e) {
// handle exception
}
ScopeChecker
$required = ['write', 'read'];
if (ScopeChecker::every($required, $granted)) {
// pass
}
Granter
$client_info = new ClientInfo('client_id', 'client_secret', ['scope'], 'redirect_uri');
$auth_server_info = new AuthorizationServerInfo('authorization_url', 'token_url');
$granter = new Granter($client_info, $auth_server_info);
$authorization_url = $granter->authorize();
// Redirect to `$authorization_url`
Usage: with Silex Provider
OAuth2ServiceProvider๋ฅผ Silex ์ ํ๋ฆฌ์ผ์ด์
์ ๋ฑ๋ก(register)ํด ์ฌ์ฉํ๋ค., (*2)
Services
-
OAuth2ProviderKeyConstant::GRANTER
-
authorize(string $state, string $redirect_uri = null, array $scope = null): string: /authorize๋ฅผ ์ํ URL์ ๋ฐํ
-
OAuth2ProviderKeyConstant::AUTHORIZER
-
autorize(Request $request): JwtToken: access_token ์ ํจ์ฑ ๊ฒ์ฌ ํ JwtToken ๊ฐ์ฒด๋ฅผ ๋ฐํ
-
OAuth2ProviderKeyConstant::MIDDLEWARE
-
authorize(OAuth2ExceptionHandlerInterface $exception_handler = null, UserProviderInterface $user_provider = null, array $required_scopes = []): ๋ฏธ๋ค์จ์ด๋ฅผ ๋ฐํ
Example: OAuth2ProviderKeyConstant::MIDDLEWARE Service
use Ridibooks\OAuth2\Silex\Constant\OAuth2ProviderKeyConstant as KeyConstant;
use Ridibooks\OAuth2\Silex\Handler\LoginRequiredExceptionHandler;
use Ridibooks\OAuth2\Silex\Provider\OAuth2ServiceProvider;
use Ridibooks\OAuth2\Authorization\Validator\JwtTokenValidator;
use Example\UserProvder;
// `OAuth2ServiceProvider` ๋ฑ๋ก
$app->register(new OAuth2ServiceProvider(), [
KeyConstant::CLIENT_ID => 'example-client-id',
KeyConstant::CLIENT_SECRET => 'example-client-secret',
KeyConstant::JWT_VALIDATOR => new JwtTokenValidator($jwk_url)
]);
// ๋ฏธ๋ค์จ์ด ๋ฑ๋ก
$app->get('/auth-required', [$this, 'authRequiredApi'])
->before($app[KeyConstant::MIDDLEWARE]->authorize(new LoginRequiredExceptionHandler(), new UserProvider());
public function authRequiredApi(Application $app)
{
// ์ฌ์ฉ์ ์ถ์ถ
$user = $app[KeyConstant::USER];
...
}
Example: OAuth2ProviderKeyConstant::AUTHORIZER Service
use Ridibooks\OAuth2\Authorization\Authorizer;
use Ridibooks\OAuth2\Authorization\Exception\AuthorizationException;
use Ridibooks\OAuth2\Silex\Constant\OAuth2ProviderKeyConstant;
use Ridibooks\OAuth2\Silex\Provider\OAuth2ServiceProvider;
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
use Ridibooks\OAuth2\Authorization\Validator\JwtTokenValidator;
...
// `OAuth2ServiceProvider` ๋ฑ๋ก
$app->register(new OAuth2ServiceProvider(), [
KeyConstant::CLIENT_ID => 'example-client-id',
KeyConstant::CLIENT_SECRET => 'example-client-secret',
KeyConstant::JWT_VALIDATOR => new JwtTokenValidator($jwk_url)
]);
...
$app->get('/', function (Application $app, Request $request) {
/** @var Authorizer $authorizer */
$authorizer = $app[OAuth2ProviderKeyConstant::AUTHORIZER];
try {
$token = $authorizer->authorize($request);
return $token->getSubject();
} catch (AuthorizationException $e) {
// handle authorization error ...
}
});
Usage: with Symfony Bundle
Services
-
Granter()
OAuth2ServiceProvider::getGranter()
-
Granter::authorize(string $state, string $redirect_uri = null, array $scope = null): string: /authorize๋ฅผ ์ํ URL์ ๋ฐํ
-
Authorizer()
OAuth2ServiceProvider::getAuthorizer()
-
Authorizer::autorize(Request $request): JwtToken: access_token ์ ํจ์ฑ ๊ฒ์ฌ ํ JwtToken ๊ฐ์ฒด๋ฅผ ๋ฐํ
-
OAuth2Middleware
OAuth2ServiceProvider::getMiddleware()
-
OAuth2ServiceProvider ์์ฑ ์, Symfony Event Subscriber๋ก ๋ฑ๋ก
Example: OAuth2Middleware Service
Configuration
- ์ค์ ์์๋
tests/Symfony์์ ์ดํด๋ณผ ์ ์์ต๋๋ค.
1. OAuth2ServiceProviderBundle ๋ฑ๋ก
# example: <project_root>/config/bundles.php
return [
...,
Ridibooks\OAuth2\Symfony\OAuth2ServiceProviderBundle::class => ['all' => true]
];
2. Parameter ๋ฐ Service ์ค์
- '%env(VARIABLE)%'์ ์ด์ฉํด environment variable์ ์ด์ฉํ ์ ์์ต๋๋ค.
- Required
- client_id
- client_secret
- authorize_url
- token_url
- jwk_url
- user_info_url
- token_cookie_domain
- default_exception_handler
- optional
- client_default_scope
- client_default_redirect_uri
- default_user_provider
- cache_item_pool # psr-6์ ๊ตฌํ์ฒด๋ฅผ ์ฃผ์
ํ๋ฉด Jwk ์์ฒญ ์ ์บ์ฑ ๊ธฐ๋ฅ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
# example: <project_root>/config/packages/o_auth2_service_provider.yml
o_auth2_service_provider:
client_id: '%env(CLIENT_ID)%'
client_secret: '%env(CLIENT_SECRET)%'
authorize_url: https://account.dev.ridi.io/ridi/authorize/
token_url: https://account.dev.ridi.io/oauth2/token/
jwk_url: https://account.dev.ridi.io/oauth2/keys/public
user_info_url: https://account.dev.ridi.io/accounts/me/
token_cookie_domain: .ridi.io
default_exception_handler: Ridibooks\OAuth2\Example\DefaultExceptionHandler
cache_item_pool: Symfony\Component\Cache\Adapter\FilesystemAdapter
# example: <project_root>/config/services.yml
services:
Ridibooks\OAuth2\Example\ExampleController:
class: Ridibooks\OAuth2\Example\ExampleController
autowire: true
autoconfigure: true
public: false
arguments:
- '@oauth2_service_provider'
3. Controller ์ค์
namespace Ridibooks\OAuth2\Example;
use Ridibooks\OAuth2\Symfony\Annotation\OAuth2;
use Ridibooks\OAuth2\Symfony\Provider\OAuth2ServiceProvider;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class ExampleController extends Controller
{
/** @var OAuth2ServiceProvider */
private $oauth2_service_provider;
/**
* @param OAuth2ServiceProvider $oauth2_service_provider
*/
public function __construct(OAuth2ServiceProvider $oauth2_service_provider)
{
$this->oauth2_service_provider = $oauth2_service_provider;
}
/**
* @Route("/oauth2", methods={"GET"})
* @OAuth2()
*
* @param Request $request
* @return Response
*/
public function normal(Request $request): Response
{
$user = $this->oauth2_service_provider->getMiddleware()->getUser();
return new JsonResponse([
'u_idx' => $user->getUidx(),
'u_id' => $user->getUid()
]);
}
}
OAuth2 Exception Handler ์ค์
- Exception Handler๋ OAuth2 ๊ณผ์ ์ค, ์ค๋ฅ ๋ฐ์ ์ Exception ์ํฉ์ ์ฒ๋ฆฌํ๋ ์ญํ ์ ๋ด๋นํฉ๋๋ค.
- Application Controller์์
default_exception_handler ํ๋ผ๋ฏธํฐ๋ก ์ง์ ํ Exception Handler๊ฐ ์๋ ๋ณ๋์ Exception Handler๋ฅผ ์ด์ฉํ๋ ค๋ ๊ฒฝ์ฐ, ์๋ ์ ์ฐจ๋ฅผ ๋ฐ๋ฆ
๋๋ค.
-
Ridibooks\OAuth2\Symfony\Handler\OAuth2ExceptionHandlerInterface๋ฅผ implementํ Exception Handler๋ฅผ ์์ฑํฉ๋๋ค.
- example:
Ridibooks\Test\OAuth2\Symfony\TestExceptionHandler
- Application Controller์
@OAuth2 Annotation์์ exception_handler ์์ฑ์ ์ง์ ํฉ๋๋ค.
- example:
@OAuth2(exception_handler="Ridibooks\Test\OAuth2\Symfony\TestExceptionHandler")
Custom User Provider ์ค์
- User Provider๋ ์ธ์ฆ ์ดํ, User ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ญํ ์ ๋ด๋นํฉ๋๋ค.
-
default_user_provider ํ๋ผ๋ฏธํฐ๋ฅผ ์ง์ ํ์ง ์์ ๊ฒฝ์ฐ, ๊ธฐ๋ณธ์ ์ผ๋ก Ridibooks\OAuth2\Symfony\Provider\DefaultUserProvider๋ฅผ ์ด์ฉํฉ๋๋ค.
- Application Controller์์
default_user_provider ํ๋ผ๋ฏธํฐ๋ก ์ง์ ํ User Provider๊ฐ ์๋ ๋ณ๋์ User Provider๋ฅผ ์ด์ฉํ๋ ค๋ ๊ฒฝ์ฐ, ์๋ ์ ์ฐจ๋ฅผ ๋ฐ๋ฆ
๋๋ค.
-
Ridibooks\OAuth2\Symfony\Provider\UserProviderInterface๋ฅผ implementํ User Provider๋ฅผ ์์ฑํฉ๋๋ค.
- Application Controller์
@OAuth2 Annotation์์ user_provider ์์ฑ์ ์ง์ ํฉ๋๋ค.
namespace Ridibooks\OAuth2\Example;
use Ridibooks\OAuth2\Authorization\Token\JwtToken;
use Ridibooks\OAuth2\Symfony\Provider\OAuth2ServiceProvider;
use Ridibooks\OAuth2\Symfony\Provider\UserProviderInterface;
use Symfony\Component\HttpFoundation\Request;
class CustomUserProvider implement UserProviderInterface
{
/**
* @param JwtToken $token
* @param Request $request
* @param OAuth2ServiceProvider $oauth2_service_provider
* @return User
*/
public function getUser(JwtToken $token, Request $request, OAuth2ServiceProvider $oauth2_service_provider): User
{
...
}
}
namespace Ridibooks\OAuth2\Example;
use Ridibooks\OAuth2\Symfony\Annotation\OAuth2;
use Ridibooks\OAuth2\Symfony\Provider\OAuth2ServiceProvider;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class ExampleController extends Controller
{
/**
* @Route("/oauth2", methods={"GET"})
* @OAuth2(user_provider="Ridibooks\OAuth2\Example\CustomUserProvider")
*
* @param Request $request
* @return Response
*/
public function normal(Request $request): Response
{
...
}
}
Cache Item Pool ์ค์
- Cache Item Pool ์ Jwk ๋ฅผ ์บ์ฑํ๋ ์ญํ ์ ๋ด๋นํฉ๋๋ค.
์ ์ํ ์
-
Jwk Multi signatures ๋ฅผ ์ง์ํ์ง ์์ต๋๋ค. ์ค์ง ์ฒซ ๋ฒ์งธ ์ธ๋ฑ์ค์ ์๊ทธ๋์ณ๋ฅผ ๊ฐ์ ธ์์ decode ํฉ๋๋ค.
- Jwk Cache File ์ TTL(Time To Live)๋ 5๋ถ ์
๋๋ค.