본문 바로가기
Programming/ETC

[JWT] Auth 동작 분석 및 User 인증(Password 방식 변경) 커스텀 생성

by 8ugust 2022. 2. 10.

 

 사용중인 User 테이블의 Password 암호화 방식을 변경했다. 당연하게도 JWT가 401 Unauthorized를 내뱉기 시작했고, 이를 해결하기 위해 고군분투 했지만 아무리 검색해도 원하는 답이 나오지 않았다. 결국 JWT 라이브러리를 직접 분석하기 시작했고, 마침내 401 Unauthorized를 내뱉는 이유를 깨달았다. 해당 내용을 검색하니 그제서야 그토록 원했던 내용을 찾는데 성공했다(원인을 모르면 검색도 못한다는 사실을 뼈저리게 느꼈다). 이렇게 된 김에 JWT 분석 내용을 기록으로 남겨보고자 한다.

 

 

 


1. JWT 401 Unauthorized 원인 분석

변경 전 User 생성 방식
변경 후 User 생성 방식

 원인은 간단하다. 변경 전 User 생성 방식은 password 알고리즘을 Bcrypt로 사용했는데, 변경 후에는 MySQL AES 알고리즘을 사용했기 때문이다. 또 User 테이블의 PK 값도 eamil 에서 user_id로 변경되었다. JWT 에선 당연히 초기 방식인 Bcrypt 알고리즘으로 인증을 시도할텐데, DB에는 AES 암호화 데이터가 담겨져 있으니, 아무리 인증을 요청해도 실패하여 401 Unauthorized가 떨어질 수 밖에.

 

 

 


2. JWT 동작 방식 분석 

  TL;DR  

일반적으로 사용하는 JWT 로그인 방식

 auth()->attempt($credentials) 

에서  attempt()  함수 찾기

 

 

 

attempt() 정의 위치 =  vender/tymon/jwt-auth/src/JWTAuth 

$this->auth =  Tymon/JWTAuth/Contracts/Providers/Auth 

함수 내  byCredentials()  함수 호출

 

 

 

 Tymon/JWTAuth/Contracts/Providers/Auth  는 Interface 파일

 Tymon/JWTAuth/Providers/Auth/Illuminate  에서 Auth 상속

$this->auth =   Laravel/Illuminate/Contracts/Auth/Guard 

함수 내  once()  함수 호출

 

 

 

 Illuminate/Contracts/Auth/Guard 는 Interface 파일

 Vender/tymon/jwt-auth/src/JWTGaurd  에서 Guard 상속

 once()  ->  validate()  ->  attempt()  순서로 함수 호출

 

 

$this->provider =  Illuminate/Contracts/Auth/UserProvider 

함수 내  retrievByCredentails()  함수 호출

 

 

 

 

 Illuminate/Contracts/Auth/UserProvider  는 Interface 파일

 Illuminate/Auth/EloquentUserProvider  에서 UserProvider 상속

 

 

 retrieveByCredentials()  = credential의 PK 값으로 User 정보 조회

해당 User 값으로  Tymon/JWTAuth/JWTGuard  에서

 Illuminate/Auth/EloquentUserProvider  의  validateCredentials()  호출

 

 

 validateCredentials()  = User의 Password 값과 credential의 password 값 비교

 

 

$user->getAuthPassword()로 부터 User 테이블에 담긴 해당 사용자의 password 암호화 문자열을 가져오고, 이를 credenttial의 password 값과 hash 비교를 통해 인증 여부를 확인하는 것이다. hasher는 Bcrypt로 credential['password'] 를 암호화 할 것이고, $user->getAuthPassword() 는 MySQL AES로 암호화 되어있으니, 단순히 문자열 비교만 해도 달라서 인증을 통과하지 못한다. => 즉 validateCredentials() 함수 내용만 변경해주면 된다. 

 

 

 


3. UserProvider 커스텀 생성

 

 위에서 차근차근 봤다면 알겠지만, validateCredentials() 함수가 정의된 EloquentUserProvider는 UserProvider를 상속받아 만들어진 클래스이다. EloquentUserProvider 클래스의 validateCredentials() 함수를 수정해도 당연히 동작하겠지만, Vender에 정의된 라이브러리 소스이기 때문에 건드리지 않는게 정신건강에 좋다. 그래서 우리는 UserProvider를 상속받는 CustomUserProvider 클래스를 새롭게 생성할 것이다.

 

 TL;DR 

// 신규 생성.
// App/Provider/CustomUserProvider.php


<?php

namespace App\Provider;

use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;

class CustomUserProvider extends EloquentUserProvider
{
    public function validateCredentials(UserContract $user, array $credentials)
    {
        $plain = $credentials['password'];
        $password = DB::select("SELECT HEX(AES_ENCRYPT('".$plain."', '".env('DB_ENCRYPT')."')) AS password")[0]->password;
        return $password === $user->getAuthPassword();
    }
}

 

// 기존 페이지에 추가
// App/Providers/AuthServiceProvider.php


.
.
use App\Providers\CustomUserProvider;

class AuthServiceProvider extends ServiceProvider
{
    .
    .
    public function boot()
    {
        $this->registerPolicies();
        
        // Custom
        Auth::provider('custom', function($app, array $config) {
            return new CustomUserProvider($app['hash'], $config['model']);
        });
    }
}

 

// 기존 페이지 수정
// config/auth.php


.
.
'providers' => [
    'user' => [
        'driver => 'custom',    // 'eloquent' 에서 'custom' 으로 변경
        'model' => App/Models/User::class
    ]
]

 

// 설정파일 캐시 리프레시

$ php artisan config:cache
$ php artisan config:clear

 

이제 JWTAuth가 다시 동작하기 시작할 것이다.

 

 

댓글