개요
Socialite나 외부 Oauth를 통해 해당 회원 정보를 가져오는데 성공 하였지만, 내부적인 API을 사용하기 위해서는 내부적으로 구축한 인증 절차도 통과 해 줘야 한다.
하지만, 내부적으로 구축한 인증 절차를 통과하기 위해서는 내부 인증에 사용되는 email / password 값을 입력하는게 보통이지만 외부 oauth를 통과하기 위해 이미 외부 계정과 비밀번호를 입력한 유저에게 한번 더 email / password 값을 입력해야지만 로그인을 시켜주는 것은 절차상 너무 복잡한 절차이며 회원들이 이탈할 확률이 높아진다.
계정 통합을 위하여 가입시에 email / password 값을 추가적으로 받는 경우들은 많지만 매 로그인 시마다 해당 절차를 밟게 하는 것은 옳은 판단은 아닐 것이다.
다양한 편법으로 구현된 인증 절차들도 많은데, (외부 로그인 절차를 진행하면서 온 token 값이나 id 값들을 임시 비밀번호로 저장하여 인증 절차를 우회 하는 등) 이런 경우에는 계정 통합도 어렵고 (비밀번호를 sns 별로 관리 해 줘야 함) 개발 하면서도 정상적인 방법은 아닌걸 인지 하면서도 개발을 진행 했을 것이다.
특히 Laravel에서 제공되는 Passport로 해당 과정에서 기존 인증들을 패싱하는 것은 상당히 고된 일이며 기존 구축된 내부 인증 검증을 하는 middleware도 문제 없이 통과 되어야 하기 때문에 다양한 고민들이 있을 것이다.
설계
추가적인 grant type을 만들어서 인증 하는 방법으로 처리하는게 가장 적합하다라는 생각이 들었다.
api middleware를 통과하기 위해서는 대부분
- Authorization Code Grant
- Password Grant
방식들을 채택 하였을 텐데, 해당 방식들을 사용할 경우 내부 인증에 필요한 email / password 값을 유저에게 한번 더 받아야 한다는 맹점이 존재한다.
따라서, SocialGrant
라는 인증 방식을 만들어, 기존 social 인증시 발급된 토큰값이 현재 social에서 받은 토큰값과 일치한다면, 내부 api middleware를 통과시키기 위한 토큰값을 발급해 주는 형태로 설계하였다.
추가적으로 생성한 파일 파일은 아래와 같다.
SocialGrnat.php
그리고 아래와 같은 파일들을 수정하는 방식으로 진행 했다.
User.php
(유저 모델)AuthServiceProvider.php
구현
먼저 User
모델에 UserEntityInterface
를 implements
해 준다.
물론 추가적인 인증 모델을 구축하고 config 쪽에 provider로 넣어 주는 방식도 좋다.
<?php
namespace App\Models;
// .. 기존 import
use League\OAuth2\Server\Entities\UserEntityInterface;
class User extends Authenticatable implements UserEntityInterface
{
use HasApiTokens, HasFactory, Notifiable;
// .. 기존 소스 코드
public function getIdentifier()
{
// TODO: Implement getIdentifier() method.
return $this->id;
}
}
SocialGrant.php
파일은 다음과 같다.
<?php
namespace App\Core\Auth;
use App\Models\User;
use DateInterval;
use Illuminate\Support\Facades\Log;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\RequestEvent;
use RuntimeException;
use Laravel\Socialite\Facades\Socialite;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
class SocialGrant extends AbstractGrant
{
public function __construct(
RefreshTokenRepositoryInterface $refreshTokenRepository
) {
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new DateInterval('P1M');
}
/**
* @throws OAuthServerException
*/
protected function validateUser(ServerRequestInterface $request) {
$accessToken = $this->getRequestParameter('access_token', $request);
$socialType = $this->getRequestParameter('social_type', $request);
$username = $this->getRequestParameter('username', $request);
if(!$accessToken) {
throw OAuthServerException::invalidRequest('access_token');
}
if(!$socialType) {
throw OAuthServerException::invalidRequest('social_type');
}
if (is_null($username)) {
throw OAuthServerException::invalidRequest('username');
}
$socialUserInfo = Socialite::driver($socialType)->stateless()->user();
// TODO :: 추후 소셜인증별 access_token값이 어떻게 반환 되는지 확인 해야 함
// gitlab일땐 일단 access_token 반환값이 token 으로 떨어짐
if($accessToken !== $socialUserInfo->token) {
throw OAuthServerException::invalidRequest('in correct social access token');
}
$user = $this->getUserEntityBySocialAccessToken($username);
if ($user instanceof UserEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
}
return $user;
}
private function getUserEntityBySocialAccessToken($username)
{
$provider = config('auth.guards.api.provider');
if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
throw new RuntimeException('Unable to determine authentication model from configuration.');
}
$user = (new $model)->where('email', $username)->first();
if (is_null($user)) {
return;
}
return $user;
// return new User($user->getAuthIdentifier());
}
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
): ResponseTypeInterface
{
// TODO: Implement respondToAccessTokenRequest() method.
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$user = $this->validateUser($request, $client);
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
return $responseType;
}
// grant type 지정
public function getIdentifier(): string
{
// TODO: Implement getIdentifier() method.
return 'social';
}
}
getIdentifier
에 return 한 social
이 grant_type이 된다.
만약 User Model
을 커스텀 하지 않고 따로 인증 모델을 구축 하였다면, getUserEntityBySocialAccessToken
코드 내부를 보게 되면 config provider 쪽에서 어떤 모델을 사용 할 것인지 체크하는 부분이 존재하는데,config/auth.php
내부의 providers
에 해당 내용을 추가해 주면 된다.
마찬가지로, 추가한 provider
를 사용하는 guards
를 추가 지정해 줘야 한다.
하지만, 기존 guards
의 api
부분을 이용 할 것이기 때문에 User Model
을 커스텀 하여서 작성 하였다.
validateUser
부분을 확인하면, 현재 Social 에서 인증된 AccessToken값을 기준으로 validateUser
인지 확인 한다.
그 다음 Providers/AuthServiceProvider.php
에 다음과 같이 코드를 추가한다.
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use App\Core\Auth\SocialGrant;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Passport;
use Laravel\Passport\Bridge\RefreshTokenRepository;
use League\OAuth2\Server\AuthorizationServer;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
//
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
$this->registerPolicies();
app(AuthorizationServer::class)->enableGrantType(
$this->makeSocialGrant(), Passport::tokensExpireIn()
);
}
protected function makeSocialGrant() {
$grant = new SocialGrant(
$this->app->make(RefreshTokenRepository::class)
);
$grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());
return $grant;
}
}
인증 Request는 다음과 같이 처리했다.
// ...중략
$socialType = UserSocialProviderType::where('type_name',$request->type)
->where('use_provider', true)
->first();
if(!$socialType) {
return response()->caps('THIS SERVICE NOT USE '. $request->type.' LOGIN');
}
// SNS 로그인
$socialUserInfo = Socialite::driver($request->type)->stateless()->user();
$user = User::where('email', $socialUserInfo->email)->first();
// 회원정보가 없으면 일단 등록 처리
if(!$user) {
$user = new User([
'name' => $socialUserInfo->name,
'email' => $socialUserInfo->email
]);
$user->save();
}
// 계정 연동 처리
SocialProvider::updateOrCreate([
'user_id'=>$user->id,
'user_social_provider_type_id'=>$socialType->id,
'provider_id'=>$socialUserInfo->id,
], [
'extend_social_info'=>json_encode($socialUserInfo)
]);
return [
'grant_type'=>'social',
'client_id'=>env('CLIENT_ID'),
'client_secret'=>env('CLIENT_SECRET'),
'username'=>$user->email,
'access_token'=>$socialUserInfo->token,
'social_type'=>$request->type,
'scope'=>'*'
];
// ... 중략
// Request 부분
// $authData가 위에서 return한 값
$request = Request::create('/oauth/token', 'POST', $authData);
try {
$response = app()->handle($request);
$authData = json_decode($response->content(), true);
if($response->getStatusCode() === 200) {
return response($authData);
} else {
return response($authData, ResponseAlias::HTTP_UNAUTHORIZED);
}
} catch (Exception $e) {
return response($e, ResponseAlias::HTTP_INTERNAL_SERVER_ERROR);
}
위 방식을 탐색하고 고민하다가 구현 해 보았는데 일단 정상적으로 잘 동작함을 확인 하였다.
국내에는 해당 관련된 문서가 존재하지 않고 해외 문서를 참조하였다.
다른 분들에게 도움이 되었으면 하고, 수정 사항이 필요하다면 꼭 누군가 알려주셨으면.. 한다.
아래는 작성시 참고한 문서
https://arifulislam-ron.medium.com/create-custom-grant-token-in-laravel-passport-1ff0cc255dc5
'PHP > PHP' 카테고리의 다른 글
[Laravel] Laravel Dockerfile + gitrunner 또는 gitaction 에서의 배포 (0) | 2023.08.04 |
---|---|
[Laravel] API 서버로 활용 할 때, 기본 laravel 오류 메시지 안뜨게 처리 (0) | 2023.06.05 |
[Laravel] Schedule + job(queue) (0) | 2023.05.12 |
[Laravel] Task Scheduling (1) | 2023.05.11 |
[Laravel] XSS Protect Middleware 구축 ( + CSRF 보호에 대한 주저리..) (0) | 2023.04.27 |