Laravel API Endpoint "401 Unauthorized" on Server But Works Fine On Localhost - laravel

Background
I have developed a React app that uses Laravel as an API. I have added logins via Passport and have been using the Personal Access Client approach quite successfully. I can add new users and tokens, I can revoke tokens, I can reset passwords... All API calls (except login and register) are guarded by the API middleware and it works. If I remove the Bearer ${token} from the header on any of these calls it returns 401 unauthenticated due to the ->middleware('auth:api') wrapper.
The Problem
Everything works completely as expected... until I move everything to my Raspberry Pi server. As soon as I moved everything, the problem began. I can login and I can register, but as soon as I use the new bearer token (that I received from my login or register call(s)) on any of the endpoint calls that follow in my flow, it fails with 401 unauthenticated, immediately. I ran the php artisan passport:client --personal command and successfully entered the id and secret into my .env file as usual. I installed all the composer and vendor packages. I installed all passport package(s) and CLI commands.
It only fails on calls that use the auth middleware.
I have done some digging and it seems the only change I can find (significantly) is that the Pi runs a 32 bit PHP where my localhost runs a 64 bit PHP. Other than that its the same code, DB, versions of Laravel and PHP, everything.
I have tried using the command php artisan passport:client --personal --name="app-name" --redirect_uri="http://192.168.1.1/" which puts a record in the "oauth_clients" table but shows the redirect as http://localhost/. I then try to use SQL to change the value of the column named "redirect" to http://localhost/, manually... but again the change does nothing. Calls still return 401 unauthenticated.
The only other things I can find that might be an issue are:
The fact that all tokens in the database table "oauth_access_tokens", under the column called "redirect", are created with the redirect_uri of http://localhost. No matter what I do it's always localhost and not my servers domain or IP address (which is concerning). Manually changing SQL as I said does nothing but I know Laravel uses a few "read-only" columns for auth so I wonder if this is one of them... perhaps personal access tokens only work on localhost?
My email_verified_at column in my "users" table (generated by passport commands) is null because I was not able to setup the "forgot my password" flow of Passport on localhost since emails won't send out on localhost.
What I have setup is this:
public function boot()
{
$this->registerPolicies();
Passport::pruneRevokedTokens();
Passport::tokensExpireIn(Carbon::now()->addDays(1));
Passport::refreshTokensExpireIn(Carbon::now()->addDays(14));
Passport::personalAccessTokensExpireIn(Carbon::now()->addDays(1));
}
AuthServiceProvider Class
public function register(Request $request) {
$validatedData = $request->validate([
'image_url' => 'required',
'last_name' => 'required|max:55',
'image_url' => 'required|max:250',
'first_name' => 'required|max:55',
'password' => 'required|confirmed',
'email' => 'email|required|unique:users',
]);
$validatedData['password'] = bcrypt($request->password);
if ($request->hasFile('image_url')) {
$imageFile = $request->file('image_url');
$imageExtension = $imageFile->extension();
if (strtolower($imageExtension) === 'png' || strtolower($imageExtension) === 'jpg') {
$validatedData['image_url'] = Storage::url( $request->file('image_url')->store('user_pics', 'public') );
}
$user = User::create($validatedData);
date_default_timezone_set('UTC');
$date = new \DateTime( date('Y-m-d H:i:s') );
$user->email_verified_at = $date->format('c');
$accessToken = $user->createToken('authToken-'.$user->id, ['*'])->accessToken;
return response([ 'user' => $user, 'access_token' => $accessToken ]);
} else {
abort(404, 'Cannot register user without a user image!');
}
}
public function login(Request $request) {
$loginData = $request->validate([
'email' => 'email|required',
'password' => 'required'
]);
if (!auth()->attempt($loginData)) {
return response()->json(['statusText' => 'Unauthorized'], 401);
}
$user = auth()->user();
$accessToken = auth()->user()->createToken('authToken-'.$user->id, ['*'])->accessToken;
return response([ 'user' => $user, 'access_token' => $accessToken ]);
}
public function logout(Request $request) {
if (auth()->guard('api')->check()) {
auth()->guard('api')->user()->OauthAcessToken()->delete();
return response()->json([ 'msg' => 'Successfully logged out!' ]);
} else {
return abort(404, 'Must be logged in to log a user out');
}
}
public function refreshToken(Request $request) {
if (auth()->guard('api')->check()) {
$user = auth()->user();
$accessToken = auth()->user()->createToken('authToken-'.$user->id, ['*'])->accessToken;
return response([ 'user' => $user, 'access_token' => $accessToken ]);
} else {
return abort(404, 'Must be logged in to refresh a token!');
}
}
AuthController Class
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users'
],
],
config/Auth.php
APP_NAME=MyName
APP_ENV=dev
APP_DEBUG=true
APP_URL=http://192.168.1.1
PASSPORT_PERSONAL_ACCESS_CLIENT_ID="1"
PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET="[SOME LONG HASH]"
.env File

Finally solved it!!
Turns out it was Apache on the Raspberry Pi server blocking the Authorization header. This finally unblocked me and solved my issues.
For anyone else coming from a Google search, you can go into your /etc/apache2/apache2.conf file and at the very bottom, paste:
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
I am using a Raspberry Pi 4 with 32 bit PHP and Apache2.
Also, I didn't mention in my post that I have been using the following for my apache server root htaccess:
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
.htaccess file, server root

Related

Laravel 8 SSO implementation

I am tring to implement a SSO structure:
main application with all user to manage the login (sso-app)
multiple application that will authenticate to sso-app (app1, app2, ...)
I managed to make the base login with sso-app api using Laravel Passport package.
Here the app1 controller for the authorization process:
class SSOController extends Controller
{
public function getLogin(Request $request){
$request->session()->put("state", $state = Str::random(40));
$query = http_build_query([
'client_id' => env('SSO_CLIENT_ID'),
'redirect_uri' => env('APP_URL') . '/auth/callback',
'response_type' => 'code',
'scope' => '',
'state' => $state
]);
return redirect(env('SSO_HOST') . '/oauth/authorize?' . $query);
}
public function getCallback(Request $request){
$state = $request->session()->pull('state');
throw_unless(strlen($state) > 0 && $state == $request->state,
InvalidArgumentException::class
);
$response = Http::asForm()->post(
env('SSO_HOST') . '/oauth/token',
[
'grant_type' => 'authorization_code',
'client_id' => env('SSO_CLIENT_ID'),
'client_secret' => env('SSO_SECRET'),
'redirect_uri' => env('APP_URL') . '/auth/callback',
'code' => $request->code
]
);
$request->session()->put($response->json());
$token = $response->json()['access_token'];
$jwtHeader = null;
$jwtPayload = null;
$parsed_token = parse_jwt($token);
try{
$email = $parsed_token->payload->user->email;
}
catch(\Throwable $e){
return redirect('login')->withError("Failed to get login information! Try again.");
}
$user = User::firstOrCreate(['email' => $email], array_merge((array)$parsed_token->payload->user, ['name' => ($parsed_token->payload->user->first_name." ".$parsed_token->payload->user->last_name)]));
Auth::login($user);
return redirect(route('home'));
}
}
The app1 will redirect to sso-app login form than when user successfull login he will redirect back to app1.
Everything work as aspected, but how can I use this approach to authorize the api route?
This work only for the "web" guard because I had create a local user table for every app and made the login based on session as you can see on the end of SSOController.
But how can I use the token returned from sso-app to authenticate local app1, app2, ... api?
Should I have to create a middleware that call sso-app every time I call app1 api to check if the token is valid or there is a better approach to save time and increase speed?
Thanks.

How can I get api/register with Passport in laravel working?

Following a tutorial: https://www.twilio.com/blog/build-secure-api-php-laravel-passport
I manAged to get Laravel/Passport installed formy Laravel Api and Vue application.
I managed to create attoken with:
localhost:8000/oauth/token
get the login working in Postman:
localhost:8000/api/login?email=jennie05#example.com&password=password
Now when I try to register a user I get returned to the home-page.
I do get some "undefined method" errors from VS Code, but they show up in the login method,
and not in the failinf register method:
Here is the controller:
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
class AuthController extends Controller
{
public function register(Request $request)
{
$validatedData = $request->validate([
'name' => 'required|max:55',
'email' => 'email|required|unique:users',
'password' => 'required|confirmed'
]);
$validatedData['password'] = bcrypt($request->password);
$user = User::create($validatedData);
$accessToken = $user->createToken('authToken')->accessToken;
return response([ 'user' => $user, 'access_token' => $accessToken]);
}
public function login(Request $request)
{
$loginData = $request->validate([
'email' => 'email|required',
'password' => 'required'
]);
if (!auth()->attempt($loginData)) {
return response(['message' => 'Invalid Credentials']);
}
$accessToken = auth()->user()->createToken('authToken')->accessToken;
return response(['user' => auth()->user(), 'access_token' => $accessToken]);
}
}
In these are the routes in api.php:
Route::post( 'register', 'App\Http\Controllers\API\AuthController#register');
Route::post( 'login', 'App\Http\Controllers\API\AuthController#login');
// Route::prefix('v2')->group(function(){ // prefix voor versie 2
Route::apiResource('/cards', 'App\Http\Controllers\CardController');
Route::apiResource('/games', 'App\Http\Controllers\GameController');
//Route::get('games', 'App\Http\Controllers\GameController#index')->middleware('auth:api');
//Route::post('games', 'App\Http\Controllers\GameController#store')->middleware('auth:api');
//Route::get('games/{id}', 'App\Http\Controllers\GameController#show')->middleware('auth:api');
//Route::put('games/{id}', 'App\Http\Controllers\GameController#update')->middleware('auth:api');
//Route::delete('games/{id}', 'App\Http\Controllers\GameController#destroy')->middleware('auth:api');
Route::get('/gameByPin/{pin}', 'App\Http\Controllers\GameController#searchPin');
Route::apiResource('/speler', 'App\Http\Controllers\SpelerController');
//});
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
is there anyone who can help troubleshooting this?
Postman does not give me errors, just the redirect to homepage..
Wow,
Thanks Patrick, your the best.
Just checked all the steps in the documentation.
reran the installation command I ran when setting up everything. ( Apperently need to do this twice ?!):
php artisan passport:install
It returned:
Encryption keys already exist. Use the --force option to overwrite them.
Personal access client created successfully.
Client ID: 5
Client secret: ikJEfthxxxxxxxxxxxxxxxxxxxxxxxxxxxBjJ36
Password grant client created successfully.
Client ID: 6
Client secret: H42lpMxxxxxxxxxxxxxxxxxxxxxxxxxxxQGZgH
Now the post request results in a token!
The error VS Code shows d for "use HasApiToken" is:
Undefined type 'Laravel\Passport\HasApiTokens'.intelephense(1009)

want to login in laravel passport multiauth

here how i can pass my new guard name is trainer
if (auth()->attempt($credentials)) {
$token = auth()->user()->createToken('TutsForWeb')->accessToken;
return response()->json(['token' => $token], 200);
} else {
return response()->json(['error' => 'UnAuthorised'], 401);
}
here I am using
'Auth::guard('trainer')->attempt($credentials)'
but it show Method Illuminate\Auth\RequestGuard::attempt does not exist.
i am using this code
https://github.com/tusharkhan/Laravel-Passport-MultiAuth/blob/master/app/Http/Controllers/PassportController.php
Actually,
The attempt method is part of the SessionGuard you cannot use Passport, and attempt to log in a user, Passport is used for API authentication, and APIs typically use tokens to authenticate users and do not maintain session state between requests.
To make it work change your driver for the trainer guard to session like this
'trainer' => [
'driver' => 'session',
'provider' => 'trainers',// Your trainer table name here
],

Laravel test fails, but works on postman

I have a test for a user logging out and having their token deleted.
use RefreshDatabase;
public function setUp() :void {
parent::setUp();
\Artisan::call('migrate',['-vvv' => true]);
\Artisan::call('passport:install',['-vvv' => true]);
\Artisan::call('db:seed',['-vvv' => true]);
}
...
/**
* #test
*/
public function a_user_has_tokens_removed_when_logged_out()
{
// login
$this->withoutExceptionHandling();
$user = factory('App\User')->create();
$response = $this->post('/api/login', [
'username' => $user->email,
'password' => 'password'
]);
$token = json_decode($response->getContent())->access_token;
$this->assertTrue(!$user->tokens->isEmpty());
// logout
Passport::actingAs($user, ['*']);
$logout = $this->json('POST', 'api/logout')->withHeaders([
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token
]);
$this->assertTrue($user->tokens->isEmpty());
}
First I'm creating a user and logging them in so a token is created and related to their user account.
I'm asserting that the token exists after hitting the login route, which passes.
Then I'm calling the logout route which will delete all the tokens the user has:
public function logout() {
auth()->user()->tokens()->each(function($token, $key) {
$token->delete();
});
return response()->json('Logged out successfully', 200);
}
routes/api.php
Route::middleware('auth:api')->post('logout', 'AuthController#logout');
This assertion on the test above is failing:
$this->assertTrue($user->tokens->isEmpty());
If I do a dd($user->tokens); before the assertion to check what's going on, the token shows up - it still exists.
But If I hit this api/logout route with Postman, which has everything stored in MySQL, all the tokens are being deleted successfully.
I don't understand what's going on and why this test is failing. Or rather, I don't understand why the $token->delete() doesn't work on the test, but does via Postman. What's different?
Before executing the assert, reload the user model relations via $user->fresh(), to ensure the deleted relations are reflected in the instance.
I don't know why, but within the testing context, this is not done automatically.

Strange error on JWT Laravel when using Multi Tenant Application

I'm using this package for "jwt-auth" for my Laravel backend project, here is the link for the package:
https://jwt-auth.readthedocs.io/en/develop/
I have 2 middleware that I put the names Tenant and JWT, when my user tries to log in to the app he must send the company code, so my middleware Tenant picks the information from the specific connection database client and all is working fine.
But when I use 2 middlewares together, he gives me an error that I don't have a user table. He is right because when I made a searching from the problem I found that the package executes a function before all my 2 middlewares that is this:
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
// MODIFICAÇÃO CUSTOMIZADA DA FUNÇÃO USER
if ($this->user !== null) {
return $this->user;
}
if ($this->jwt->setRequest($this->request)->getToken() &&
($payload = $this->jwt->check(true)) &&
$this->validateSubject()
) {
return $this->user = $this->provider->retrieveById($payload['sub']);
}
}
The problem was solved if I comment this line "return $this->user = $this->provider->retrieveById($payload['sub']);", but this is not a good practice. The main reason for this error is that this function is executed before my Tenant middleware that doesn't have a user table in the database that Tenant middleware tries to connect.
The file name is **JWT_GUARD.php in tymon\jwt-auth\src**, i'm think is something about my configuration that i must change in
config/auth.php from laravel
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
And here:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
I comment on this part "return $this->user = $this->provider->retrieveById($payload['sub']);" and waiting for a better solution

Resources