krisanalfa / lumen-jwt

Lumen with JWT Authentication, Dingo API and CORS Support
258 stars 80 forks source link

How to customize JWT authentication process? #17

Closed smarques84 closed 7 years ago

smarques84 commented 7 years ago

I am modifying this project to authenticate with a custom user table from a Microsoft SQL Database with different column names and where the password is saved with a different hashing algorithm.

Because of this scenario i would like to check the credentials by my self while avoid using eloquent, to generate the token. So far i have this in AuthController.php $payload = JWTFactory::make($credentials); $token = JWTAuth::encode($payload);

Which generates a token although this method doesn't return a string (Is it normal?)

Now i think i need to authenticate the token so i had a look in the api.php inside routes folder $api->group([ 'middleware' => 'api.auth', ], function ($api) { The middleware part is responsible to check if the token is valid right? But where does this middleware points to? I am a newbie at both JWT and Dingo so any help will be appreciated.

krisanalfa commented 7 years ago

Which generates a token although this method doesn't return a string (Is it normal?)

Yes it is normal. JWTAuth::encode method would return Tymon\JWTAuth\Token, to make this as string you can implicitly cast it to string, like so:

$token = JWTAuth::encode($payload);
$tokenString = (string) $token;

For that case you need custom middleware, because you're about to validate payload with your custom claims. Are you ready for long story? If yes, here we go:

  1. You need to create custom authenticator:
namespace App\Auth;

use App\User;
use Tymon\JWTAuth\JWTAuth;
use BadMethodCallException;
use Tymon\JWTAuth\Providers\Auth\AuthInterface;

class CustomAuthenticator implements AuthInterface
{

    /**
     * Current authenticated user.
     *
     * @var \App\User|null
     */
    protected $user;

    /**
     * Check a user's credentials.
     *
     * @param  array  $credentials
     * @return bool
     */
    public function byCredentials(array $credentials = [])
    {
        throw new BadMethodCallException();
    }

    /**
     * Authenticate a user via the id. Here you can modify how you validate
     * your `credentials` from your payload inside the token.
     *
     * @param  mixed  $id
     * @return bool
     */
    public function byId($id)
    {
        // Because our `sub` is json_encoded type, we need to decode it first
        $credentials = json_decode($id, true);

        // Example to fetch user from it's email given by the payload
        // TODO: NEED TO VALIDATE THE PASSWORD
        $this->user = User::where('email', $credentials['email'])->first();

        return $this->user != null;
    }

    /**
     * Get the currently authenticated user.
     *
     * @return mixed
     */
    public function user()
    {
        return $this->user;
    }
}

I'll explain why we should write byId method below.

  1. Remember why we should write byId method? Actually, JWTAuth class need this method to authenticate the payload so we can get our current authenticated user, see here. From that code, we know that our payload should have sub "key". So when you create your token, you may fill sub "key":
// This json_encoded value will be passed to our `CustomAuthenticator::byId`
$payload = JWTFactory::make($credentials + ['sub' => json_encode($credentials)]);

$token = (string) JWTAuth::encode($payload);
  1. Change default authenticator in config/jwt.php
/*
|--------------------------------------------------------------------------
| Authentication Provider
|--------------------------------------------------------------------------
|
| Specify the provider that is used to authenticate users.
|
*/

'auth' => 'App\Auth\CustomAuthenticator'
  1. Create custom middleware:
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\JsonResponse;
use Tymon\JWTAuth\Facades\JWTAuth;

class CustomAuthMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (!JWTAuth::parseToken()->authenticate()) {
            return new JsonResponse(['message' => 'invalid_token'], 401);
        }

        $response = $next($request);

        return $response;
    }
}
  1. Register your middleware. Open your bootstrap/app.php find and change this line
$app->routeMiddleware([
    'auth.custom' => App\Http\Middleware\CustomAuthMiddleware::class,
]);
  1. Modify your route file:
$api->group(['middleware' => 'auth.custom'], function ($api) {
     // Register your protected route here
});

Have a try. Let me know if you need something.

UPDATE

You don't need to add custom middleware, it just works by skipping step 4 - 6. Just leave your middleware in route using api.auth

$api->group([
    'middleware' => 'api.auth',
], function ($api) {
    // rest of your protected routes
});
smarques84 commented 7 years ago

Ok Thank you so much i manage to successfully authenticate but i noticed that the username and password go on the sub key whats the recommended way to store that info inside the token? Just hash the password?

The expiration date is set inside the token correct?

Edit: For what i see i can't hash the password inside the token if i want to use password_verify php function (password is hashed with bcrypt in the db). So is it supposed for the username & password to be in plain text inside the token? That way anyone can just grab the token with wireshark and simply decode the token and retrieve the user credentials from the sub key!! Or am i missing something here?

krisanalfa commented 7 years ago

... whats the recommended way to store that info inside the token? Just hash the password?

~No, JWT has encrypted anything in it, just make sure you have generate your JWT_KEY, from that you can decrypt the payload. It means, as long as eavesdropper cannot "guest" your JWT_KEY, then THERE IS NO WAY THEY CAN SEE THE PLAIN PASSWORD.~ But I really recommend you to NOT sending the password inside the token. Although it seems secure (since it's encrypted), but, who knows?


The expiration date is set inside the token correct?

Yes it is.


So is it supposed for the username & password to be in plain text inside the token?

If you really want to decrypt the password. You may see this snippet. You can decrypt and encrypt it safely.

smarques84 commented 7 years ago

@krisanalfa Thanks for all the help but for what i read the password should never go inside the token. https://stormpath.com/blog/jwt-the-right-way read where it says "These tokens are usually signed to protect against manipulation (not encrypted) so the data in the claims can be easily decoded and read"

I just tried decoding my token with this website https://py-jwt-decoder.appspot.com/ and can see the sub in plain text! Just test with this token i generated: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImZpcnN0bGFzdEB3aGVyZS5jb20iLCJwYXNzd29yZCI6Im15X3Bhc3N3b3JkX2lzX2luX3BsYWluX3RleHQiLCJzdWIiOiJ7XCJlbWFpbFwiOlwiZmlyc3RsYXN0QHdoZXJlLmNvbVwiLFwicGFzc3dvcmRcIjpcIm15X3Bhc3N3b3JkX2lzX2luX3BsYWluX3RleHRcIn0iLCJpc3MiOiJodHRwOlwvXC8xMjcuMC4wLjE6ODg4OFwvbHVtZW4tand0XC9wdWJsaWNcL2FwaVwvYXV0aFwvbG9naW4iLCJpYXQiOjE0ODUyNjMxMjQsImV4cCI6MTQ4NTI2NjcyNCwibmJmIjoxNDg1MjYzMTI0LCJqdGkiOiJlZmNhYTg4NGE5MjllYWY1OWVjNTNkZmQ1ODFmNmNkMiJ9.xLA-gbxPFGDn0sWhn_3T9zxflgdG1zTtLYeqXczzNlQ

So its really easy to get the data, for what i understand it seems the real protection in JWT is protecting against tempering the token.

Reading on the first link it says: "Always verify the signature before you trust any information in the JWT."

The correct way to use this is just insert a value that identifies the user in the database (like the unique id) and then verify the token signature with the JWT_SECRET to make sure its a valid token. Now how do I verify the token signature inside the public function byId($id) function?

krisanalfa commented 7 years ago

My, bad. It's decode-able. You may change your authentication process like so:

  1. When user send POST /auth/login request, validate their credentials. Get the User corresponds to the credentials and create your payload like this:
$user = $this->getUserByCredentials($credentials);
$payload = JWTFactory::make(['sub' => $user->getKey()]);
$token = (string) JWTAuth::encode($payload);
  1. When you want to validate the token:
public function byId($id)
{
    $this->user = User::where('id', $id)->first();

    return $this->user != null;
}

Now how verify the token signature inside the byID function?

It is automatically verified by the authenticate process. Just change your JWT_SECRET value, your old token will be invalid with this error response message:

Token Signature could not be verified.

smarques84 commented 7 years ago

Thank you so much :1st_place_medal: i successfully implemented the api the way i needed