- POST /api/v1/internal/exchange-token 추가 - HMAC-SHA256 서명 기반 서버간 인증 - InternalTokenService: 서명 검증 및 Sanctum 토큰 발급 - ExchangeTokenRequest: 요청 검증 (user_id, tenant_id, exp, signature) - ApiKeyMiddleware: 내부 통신 경로 화이트리스트 추가 - i18n 메시지 추가 (error.internal.*, message.internal.*) 환경변수 필요: INTERNAL_EXCHANGE_SECRET (MNG와 동일)
165 lines
5.0 KiB
PHP
165 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Members\User;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
/**
|
|
* 내부 서버간 토큰 교환 서비스
|
|
*
|
|
* MNG 서버에서 HMAC 서명된 페이로드를 검증하고
|
|
* API 사용을 위한 Sanctum 토큰을 발급합니다.
|
|
*/
|
|
class InternalTokenService
|
|
{
|
|
/**
|
|
* 토큰 유효 시간 (초) - 기본 1시간
|
|
*/
|
|
private const TOKEN_EXPIRES_IN = 3600;
|
|
|
|
/**
|
|
* 서명 유효 시간 (초) - 기본 5분
|
|
*/
|
|
private const SIGNATURE_VALID_DURATION = 300;
|
|
|
|
/**
|
|
* 서명된 페이로드 검증
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $tenantId 테넌트 ID
|
|
* @param int $exp 만료 타임스탬프
|
|
* @param string $signature HMAC 서명
|
|
* @return array{valid: bool, error?: string}
|
|
*/
|
|
public function verifySignature(int $userId, int $tenantId, int $exp, string $signature): array
|
|
{
|
|
$sharedSecret = config('services.internal.exchange_secret');
|
|
|
|
if (empty($sharedSecret)) {
|
|
Log::error('[InternalTokenService] exchange_secret not configured');
|
|
|
|
return ['valid' => false, 'error' => __('error.internal.secret_not_configured')];
|
|
}
|
|
|
|
// 만료 시간 검증 (현재 시간 기준 SIGNATURE_VALID_DURATION 초 이내)
|
|
$now = time();
|
|
if ($exp < $now) {
|
|
Log::warning('[InternalTokenService] Signature expired', [
|
|
'exp' => $exp,
|
|
'now' => $now,
|
|
'diff' => $now - $exp,
|
|
]);
|
|
|
|
return ['valid' => false, 'error' => __('error.internal.signature_expired')];
|
|
}
|
|
|
|
if ($exp > $now + self::SIGNATURE_VALID_DURATION) {
|
|
Log::warning('[InternalTokenService] Signature exp too far in future', [
|
|
'exp' => $exp,
|
|
'now' => $now,
|
|
'diff' => $exp - $now,
|
|
]);
|
|
|
|
return ['valid' => false, 'error' => __('error.internal.invalid_exp')];
|
|
}
|
|
|
|
// HMAC 서명 검증
|
|
$payload = "{$userId}:{$tenantId}:{$exp}";
|
|
$expectedSignature = hash_hmac('sha256', $payload, $sharedSecret);
|
|
|
|
if (! hash_equals($expectedSignature, $signature)) {
|
|
Log::warning('[InternalTokenService] Invalid signature', [
|
|
'user_id' => $userId,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
return ['valid' => false, 'error' => __('error.internal.invalid_signature')];
|
|
}
|
|
|
|
return ['valid' => true];
|
|
}
|
|
|
|
/**
|
|
* 사용자 토큰 발급
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $tenantId 테넌트 ID
|
|
* @return array{access_token: string, token_type: string, expires_in: int}|null
|
|
*/
|
|
public function issueToken(int $userId, int $tenantId): ?array
|
|
{
|
|
$user = User::find($userId);
|
|
|
|
if (! $user) {
|
|
Log::warning('[InternalTokenService] User not found', ['user_id' => $userId]);
|
|
|
|
return null;
|
|
}
|
|
|
|
// 해당 테넌트에 소속되어 있는지 확인
|
|
$userTenant = $user->userTenants()->where('tenant_id', $tenantId)->first();
|
|
if (! $userTenant) {
|
|
Log::warning('[InternalTokenService] User not in tenant', [
|
|
'user_id' => $userId,
|
|
'tenant_id' => $tenantId,
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
|
|
// 기존 mng_session 토큰 삭제 (중복 방지)
|
|
$user->tokens()->where('name', 'mng_session')->delete();
|
|
|
|
// 새 토큰 발급
|
|
$token = $user->createToken('mng_session', ['*'], now()->addSeconds(self::TOKEN_EXPIRES_IN));
|
|
|
|
Log::info('[InternalTokenService] Token issued', [
|
|
'user_id' => $userId,
|
|
'tenant_id' => $tenantId,
|
|
'token_id' => $token->accessToken->id,
|
|
]);
|
|
|
|
return [
|
|
'access_token' => $token->plainTextToken,
|
|
'token_type' => 'Bearer',
|
|
'expires_in' => self::TOKEN_EXPIRES_IN,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 토큰 교환 실행 (검증 + 발급)
|
|
*
|
|
* @param int $userId 사용자 ID
|
|
* @param int $tenantId 테넌트 ID
|
|
* @param int $exp 만료 타임스탬프
|
|
* @param string $signature HMAC 서명
|
|
* @return array{success: bool, data?: array, error?: string}
|
|
*/
|
|
public function exchange(int $userId, int $tenantId, int $exp, string $signature): array
|
|
{
|
|
// 1. 서명 검증
|
|
$verification = $this->verifySignature($userId, $tenantId, $exp, $signature);
|
|
if (! $verification['valid']) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $verification['error'],
|
|
];
|
|
}
|
|
|
|
// 2. 토큰 발급
|
|
$tokenData = $this->issueToken($userId, $tenantId);
|
|
if (! $tokenData) {
|
|
return [
|
|
'success' => false,
|
|
'error' => __('error.internal.token_issue_failed'),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $tokenData,
|
|
];
|
|
}
|
|
}
|