I have an issue with logging out using JWT package. On Angular side I am removing the token from local storage and calling Laravel API:
logout(): void {
this.cacheHandler.clearCache();
return this.http.get(environment.apiUrl + 'logout')
.toPromise()
.then(response => {
const responseData = response.json();
return responseData.success;
})
.catch(error => {
return error;
});
}
And on Laravel side:
public function logout()
{
try {
JWTAuth::invalidate(JWTAuth::getToken());
return response()->json(['success' => true, 'message' => 'Logout successful'], 200);
} catch (JWTException $e) {
return response()->json(['success' => false, 'error' => 'Failed to logout, please try again.'], 500);
}
}
API method is protected with JWT auth middleware, so when calling API I am forwarding Authorization Bearer token which was stored in local storage. Method passes, token is valid.
After that it enters the logout() method and I am getting the response that logout was successful.
Problem is that I can trigger the same request, with same token, always getting the same message. If it was invalidated, it couldn't trigger the second request because it is behind a middleware.
Also, with the same token, which was supposed to be invalidated, I can call other API methods which require authentication (without the token they don't work)
try using the new JWTGuard and call it like this:
public function logout()
{
$this->guard()->logout(); // pass true to blacklist forever
}
That logout functions calls the following code:
public function logout($forceForever = false)
{
$this->requireToken()->invalidate($forceForever);
$this->user = null;
$this->jwt->unsetToken();
}
Related
I'm trying to implement authentication & authorization of users between my microservices and API Gateway.What I have now:
API Gateway which can request to any microservice.
User microservice - where I'm storing all users. laravel/passport implemented to authenticate user in this microservice. Works as it should be, login route returns token which I'm using to authenticate user in this microservice.
Other 5 microservices without any authentication or authorization.
Question is: what is the right way to use authentication & authorization with microservices? I know that I should authenticate users in my API Gateway and authorization will happen inside microservices. But how authorization in other microservices happening if they don't know anything about users?
I'm planning to use somehow JWT token with information about user roles but haven't found yet how to put that information into token
I'll try to explain with a basic example for API.
Let's say you have currently 3 microservices :
Users
Posts
Core
I assume you're using httpOnly cookie to store user token.
In Core microservice I have this route structure:
Route::prefix('core')->group(function () {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware('scope.trader')->group(function () {
Route::get('user', [AuthController::class, 'user']);
});
});
Now i want to login which i should send an API request, and I should think of a solution to send token anytime I need it.
login(this is where you get token) and register don't need token
user need token (this is where you asked for solution)
So in addition to get a result, I should create a service for user, and here how I've done it :
UserService :
class UserService extends ApiService
{
public function __construct()
{
// Get User Endpoint Microservice API URL
$this->endpoint = env('USERS_MS') . '/api';
}
}
ApiService :
abstract class ApiService
{
protected string $endpoint;
public function request($method, $path, $data = [])
{
$response = $this->getRequest($method, $path, $data);
if ($response->ok()) {return $response->json();};
throw new HttpException($response->status(), $response->body());
}
public function getRequest($method, $path, $data = [])
{
return \Http::acceptJson()->withHeaders([
'Authorization' => 'Bearer ' . request()->cookie('token')
])->$method("{$this->endpoint}/{$path}", $data);
}
public function post($path, $data)
{
return $this->request('post', $path, $data);
}
public function get($path)
{
return $this->request('get', $path);
}
public function put($path, $data)
{
return $this->request('put', $path, $data);
}
public function delete($path)
{
return $this->request('delete', $path);
}
}
If you're wondering where, this UserService come from, then I should say, I've created a package to use it in other microservices, so you can do the same or just create a service and use it in your microservices or etc.
Everything is obvious about ApiService, but I'll try to explain the base.
Anytime we want to do an API call, we can simply call Allowed methods in this class, then our methods, will call request, to pass common arguments, and eventually using those arguments to do the API call.
getRequest method, is doing the call and get the stored token from httpOnly cookie, and will send it as an Authorization header to the target endpoint, and eventually it'll return whatever it get from target.
So If we want to use this, we can simply do like this in our controller :
class AuthController extends Controller
{
// use Services\UserService;
public UserService $userService;
/**
* #param UserService $userService
*/
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public function register(RegisterRequest $request)
{
$data = $request->only('name', 'email', 'password') + ['additional_fileds' => 0 ];
// additional fields can be used for something except from request and
// optional, like is it admin or user or etc.
// call the post method, pass the endpoint url(`register`), pass $data
$user = $this->userService->post('register', $data);
// get data from target endpoint
// and ...
return response($user, Response::HTTP_CREATED);
}
public function login(Request $request)
{
// same thing here again, but this time i passed scope to help me
// get the specific user scope
$data = $request->only('email', 'password') + ['scope' => 'writer'];
$response = $this->userService->post('login', $data);
// as you can see when user do success login, we will get token,
// which i got that token using Passport and set it to $cookie
$cookie = cookie('token', $response['token'], 60 * 24); // 1 day
// then will set a new httpOnly token on response.
return response([
'message' => 'success'
])->withCookie($cookie);
}
public function user(Request $request)
{
// Here, base on userService as you saw, we passed token in all requests
// which if token exist, we get the result, since we're expecting
// token to send back the user informations.
$user = $this->userService->get('user');
// get posts belong to authenticated user
$posts = Post::where('user_id', $user['id'])->get();
$user['posts'] = $posts;
return $user;
}
}
Now, how about user microservice? well Everything is clear here, and it should work like a basic app.
Here's the routes :
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::middleware(['bunch','of', 'middlewares'])->group( function (){
Route::get('user', [AuthController::class, 'user']);
});
And in controller :
class AuthController extends Controller
{
public function register(Request $request)
{
$user = User::create(
$request->only('first_name', 'email', 'additional_field')
+ ['password' => \Hash::make($request->input('password'))]
);
return response($user, Response::HTTP_CREATED);
}
public function login(Request $request)
{
if (!\Auth::attempt($request->only('email', 'password'))) {
return response([
'error' => 'user or pass is wrong or whatever.'
], Response::HTTP_UNAUTHORIZED);
}
$user = \Auth::user();
$jwt = $user->createToken('token', [$request->input('here you can pass the required scope like trader as i expalined in top')])->plainTextToken;
return compact('token');
}
public function user(Request $request)
{
return $request->user();
}
}
So here's the complete example and you can use the Core microservice approach on other microservices to get your information related to authenticated user, and as you can see everything will be authenticated due to those requests from core to other microservices.
I am trying to register a user on my website with the laravel/vue.js/vuex. In my store actions, I'm calling the tryRegister action to post a request. But it keeps responding with a 401 error not authorizaed, and I don't understand why. So I made my controller as basic as it can get to just get a response and even then it keeps throwing the 401 authorization error. I'm new to back-end developing and just can't understand why this happens. I do know for sure that the route is working. How can I make my controller function give a basic response to see if it is working? And why is it giving an authorization error even tho I'm not doing anything with authorization, is that just the standard error a controller gives?
Try register action
tryRegister(context, credentials) {
return new Promise((resolve, reject) => {
axios
.post("/api/auth/register", credentials)
.then(response => {
console.log(response.data);
//context.commit("registerSucces", response.data);
resolve(response.data);
})
.catch(error => {
console.log(error.response);
reject(error);
});
});
}
Authorization controller register function
public function register(Request $request)
{
// $user = User::create([
// 'email' => $request->email,
// 'password' => $request->password,
// ]);
//$token = auth('api')->login($user);
//return $this->respondWithToken($token);
return response()->json(['message' => 'controller register']);
}
your register method of the controller is not accessible because of the auth middleware. so you are getting not authorized error. make it accessible without authorization. in the constructor method of the controller change this line like below.
$this->middleware('auth:api', ['except' => ['login', 'register']]);
login and register are controller's method which will be now accessible without authorization.
Now, I'm using api.php route for requests from Axios on VueJS , And I need to logout from Auth::guard('web')->logout(); command but, at the moment, I cannot do this.
routes/api.php
Route::group([ 'prefix' => 'v1/auth', 'middleware' => 'jwt'], function () { //
Route::get('me', 'Auth\UserController#me');
Route::get('gg', 'Auth\UserController#test');
});
app/Http/sMiddleware/JwtMiddleware.php
<?php
namespace App\Http\Middleware;
use Closure;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Tymon\JWTAuth\Exceptions\JWTException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;
use Tymon\JWTAuth\Exceptions\TokenExpiredException;
use Tymon\JWTAuth\Facades\JWTAuth;
use Illuminate\Support\Facades\Auth;
class RefreshToken extends BaseMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
try
{
if (! $user = JWTAuth::toUser(JWTAuth::getToken()))
{
return response()->json([
'code' => 101, // means auth error in the api,
'response' => 'not authenticate' // nothing to show
]);
}
}
catch (TokenExpiredException $e)
{
// If the token is expired, then it will be refreshed and added to the headers
try
{
$refreshed = JWTAuth::refresh(JWTAuth::getToken());
header('Authorization: Bearer ' . $refreshed);
}
catch (JWTException $e)
{
return response()->json([
'code' => 103, // means not refreshable
'response' => 'token jwt exception' // nothing to show
]);
}
}
catch (JWTException $e)
{
Auth::guard('web')->logout(); // here
return response()->json([
'code' => 101, // means auth error in the api,
'response' => 'jwterror' // nothing to show
]);
}
return $next($request);
}
}
But when i migrated from api.php to web.php. I can use Axios to post for logout
Please, tell me how to use Auth::logout in api route file.
Sorry I'm not good at english.
Logout is implemented with the session driver, and unlike there web guard, the api guard is using a token driver not session driver.
Basically, the user is not logged into the API, but WEB part of your application.
In the api; find a way to invalidate/expire the token so that the user with that token can no longer access the api resources.
try {
JWTAuth::invalidate($request->input('token'));
return response()->json(['success' => true, 'message'=> "You have successfully logged out."]);
} catch (JWTException $e) {
// something went wrong whilst attempting to encode the token
return response()->json(['success' => false, 'error' => 'Failed to logout, please try again.'], 500);
}
Web logout
Session Logout
I am trying to authorize user from android device. I integrated the JWT package in my project and made the token based authorization. Here is how my Controller looks like:
class AdminLoginController extends Controller
{
public function __construct()
{
$this->middleware('jwt.auth',['except'=>['authenticate']]);
}
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
try {
// verify the credentials and create a token for the user
if (! $token = JWTAuth::attempt($credentials))
{
return response()->json(['error' => 'invalid_credentials'], 401);
}
} catch (JWTException $e) {
// something went wrong
return response()->json(['error' => 'could_not_create_token'], 500);
}
// if no errors are encountered we can return a JWT
return response()->json(compact('token'));
}
With this i am being to get token for the valid user request but i need to pass the parameters from the parameters section in postman.I have the put on the picture here . But if i try to post parameters from the body as a form data it says invalid credentials for the same user. How can i solve it. Any kinds of help are appreciated. Thank you.
Fisrt step check:
dd($credentials);
The ouput should be like this:
array:2 [
"email" => "kajal#gmail.com"
"password" => "kajal1"
]
Second step check:
dd($token);
And show the ouput.
I managed to adjust the default Laravel auth so that it will work as an API for my AngularJS, and so far everything works well. Can go to /reset and enter an email and get sent an email with a password reset link which goes to /reset/{token} and if you don't get any validation errors, your password will successfully be changed.
The only problem is since I am using an Angular view, there isn't really anything validating the token and making sure it's not just gibberish before showing the reset-password state. I tried adding this to the top of the controller:
if ($stateParams.token != $cookies.get('XSRF_TOKEN')) {
$state.go('reset');
}
...which would basically see if the token is the current CSRF token, but that doesn't work because when the password reset link is sent the CSRF token is changed or something... it is no longer the token from the session.
Anyone have any ideas how I can do this? I want to just redirect the user if the token entered in the url on `/reset/:token' is not valid.
Here is my code..
App.js:
.state('reset', {
url: '/reset',
data: {
permissions: {
except: ['isLoggedIn'],
redirectTo: 'user.dashboard'
}
},
templateUrl: 'views/auth/forgot-password.html',
controller: 'ForgotPasswordController as forgot'
})
.state('reset-password', {
url: '/reset/:token',
data: {
permissions: {
except: ['isLoggedIn'],
redirectTo: 'user.dashboard'
}
},
templateUrl: 'views/auth/reset-password.html',
controller: 'ResetPasswordController as reset'
})
This is in the ResetsPassword trait in ResetsPassword.php. Most was already set up but I removed/changed a lot to work as an API:
/**
* Send a reset link to the given user.
*/
public function postEmail(EmailRequest $request)
{
$response = Password::sendResetLink($request->only('email'), function (Message $message) {
$message->subject($this->getEmailSubject());
});
switch ($response) {
case Password::RESET_LINK_SENT:
return;
case Password::INVALID_USER:
return response()->json([
'denied' => 'We couldn\'t find your account with that information.'
], 404);
}
}
/**
* Get the e-mail subject line to be used for the reset link email.
*/
protected function getEmailSubject()
{
return property_exists($this, 'subject') ? $this->subject : 'Your Password Reset Link';
}
/**
* Reset the given user's password.
*/
public function postReset(ResetRequest $request)
{
$credentials = $request->only(
'password', 'password_confirmation', 'token'
);
$response = Password::reset($credentials, function ($user, $password) {
$this->resetPassword($user, $password);
});
switch ($response) {
case Password::PASSWORD_RESET:
return;
default:
return response()->json([
'error' => [
'message' => 'Could not reset password'
]
], 400);
}
}
/**
* Reset the given user's password.
*/
protected function resetPassword($user, $password)
{
$user->password = bcrypt($password);
$user->save();
}
Figured this out.
For anyone having a similar problem... Here's how I solved it (will probably make it better later on, but for now it works).
I added another url for the API which is reset/password and it takes a GET request. I pass it the token based on the $stateParams value and if that token does not exist in the password_resets table OR if that token does exist and is expired, return some errors. In the controller, I handle the errors with a redirect. Again I don't think this is ideal because anyone looking at the source could change it up and remove the redirect so I have to find a better way to implement this.
But again, for now it works and it is a solution nonetheless.
ResetsPasswords.php (added a method for the get request):
public function verifyToken(Request $request)
{
$user = DB::table('password_resets')->where('token', $request->only('token'))->first();
if ($user) {
if ($user->created_at > Carbon::now()->subHours(2)) {
return response()->json([
'success' => [
'message' => 'Token is valid and not expired.'
]
], 200);
} else {
return response()->json([
'error' => [
'message' => 'Token is valid but it\'s expired.'
]
], 401);
}
} else {
return response()->json([
'error' => [
'message' => 'Token is invalid.'
]
], 401);
}
}
and in my resetPasswordController.js I just check if the respose returns 'success' or any of the 'error' responses, and if it's an 'error' response I would just do something like $state.go('reset') which would redirect them back to the "forgot password" form where they enter their email to get a reset password link.
EDIT:
I figured that checking for a valid token in the controller was bad because it would always load the view at least for a split second. I was looking for some kind of middleware but then I forgot that I was already using the angular-permission package, which sort of acts as a front-end middleware.
I defined a role isTokenValid and set it up such that it automatically calls a function in the authService I have and gets a response from my API based on the validity of the token. If successful, the role allows the user to enter the state. Otherwise it redirects to the reset state. This prevents the view showing for the split second. Acts very similar to Laravel middleware.
Only problem is since it's happening on the front end, any hacker can bypass that but that's okay because the server-side code is still there so even if they access the view they can't do anything with the passwords they enter cause the token is still invalid and has to match a particular user.
Another improvement would be to find a way to disallow the view even without the front-end middleware implementation. Maybe I'll update this again if I find a way to do that.
Implementation
The role:
.defineRole('isTokenValid', function(stateParams) {
var token = stateParams.token;
var deferred = $q.defer();
authService.verifyToken(token)
.then(function(res) {
if (res.success) {
deferred.resolve();
}
}, function(res) {
if (res.error) {
deferred.reject();
}
});
return deferred.promise;
});
And the state:
.state('reset-password', {
url: '/reset/:token',
data: {
permissions: {
only: ['isTokenValid'],
redirectTo: 'reset'
}
},
templateUrl: 'views/auth/reset-password.html',
controller: 'ResetPasswordController as reset'
})
Hope it helps anyone with the same problem.