How do I add basic authentication to a Codeigniter 4 REST API? - php

I have a very basic Codeigniter 4 REST API with only one user and one function (POST). I need to add basic authentication where the username and password are in the HTTP authentication header. How do I add that to the code below? i don't need anything very sophisticated, just enough to authenticate the one user.
namespace App\Controllers;
use CodeIgniter\RESTful\ResourceController;
use CodeIgniter\API\ResponseTrait;
use App\Models\ApiModel;
use Config\Services;
class Api extends ResourceController {
use ResponseTrait;
protected $request;
public function create() {
$model = new ApiModel();
$data = [
'uid' => $this->request->getPost('uid'),
'request' => $this->request->getPost('request'),
'data' => $this->request->getPost('data')
];
$model->insert($data);
$response = [
'status' => 201,
'error' => null,
'messages' => [
'success' => 'Data Saved'
]
];
return $this->respondCreated($data, 201);
}
}

Related

Laravel test kept failing due to wrong method

I have the following test to check my simple crud application in laravel.
I have my SalesManagersTest.php in tests/features
<?php
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SalesManagersTest extends TestCase
{
use RefreshDatabase;
public function test_can_see_index_screen()
{
$response = $this->get('/salesmanagers');
$response->assertStatus(200);
}
public function test_can_see_add_record_screen()
{
$response = $this->get('/salesmanagers/create');
$response->assertStatus(200);
}
public function test_can_add_new_record()
{
$response = $this->POST('/salesmanagers/create', [
'_token' => csrf_token(),
'full_name' => 'Test User',
'email' => 'test#example.com',
'telephone' => '0775678999',
'current_route' => 'Negombo',
'joined_date'=>'2022/09/12',
'comments' => 'Test comment'
]);
$response->assertStatus(200);
$response->assertRedirectedTo('salesmanagers');
}
}
The first two tests work well but the third test is giving me an error
but since I'm trying to insert new record the method has to be POST
this is my web.php
Route::get('/', [ SalesManagersController::class, 'index' ]);
Route::resource('salesmanagers', SalesManagersController::class);
Route::get('salesmanagers.create', [ SalesManagersController::class, 'create' ]);
Route::post('salesmanagers.create', [ SalesManagersController::class, 'store' ]);
What could be the the issue with my test?
The issue is your route definition. It appears you're using a route name for the URI of your route. Change your route to the following and try again:
Route::post('/salesmanagers', [SalesManagersController::class, 'store']);
Oh and POST your data to /salesmanagers not salesmanagers/create.

How add parameter in FormRequest Laravel

I'm doing a login with passport (in API) I'm trying to get the tokens generated by the authentication server. However, I can't add extra parameters to the request which is an instance of FormRequest.
On the other hand, if I change my request to an instance of Request, it works.
So, my question how I can add parameters to my query $loginRequest (which is instance of FormRequest)
$loginRequest->request->add($params);
Here my code:
class AuthController extends Controller
{
use ThrottlesLogins;
public function store(LoginRequest $loginRequest)
{
$loginRequest->validated();
if ($this->hasTooManyLoginAttempts($loginRequest)) {
$this->fireLockoutEvent($loginRequest);
return $this->sendLockoutResponse($loginRequest);
}
if (Auth::attempt($this->credentials($loginRequest))){
$client = $this->getClient($loginRequest->name);
$params = [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $loginRequest->email,
'password' => $loginRequest->password,
'scopes' => 'fd',
];
$loginRequest->request->add($params);
$req = Request::create('oauth/token', 'POST');
$response = Route::dispatch($req)->getContent();
return $response;
}
$this->incrementLoginAttempts($loginRequest);
$this->sendFailedLoginResponse($loginRequest);
}
}
To append properties to an instance of FormRequest you can use the merge() method.
public function store(LoginRequest $loginRequest) {
$params = [
'foo' => 'bar',
];
$loginRequest->merge($params);
}

Yii2: Simultaneous authentication between frontend and API

I've turned the backend into an API. And the API controllers have HttpBasicAuth type authentication.
The problem is that even after authentication in the frontend, whenever a request is made to the API, the authentication window appears.
How can I do so that when the user authenticates in the frontend, is not requested again the username and password of access when a request is made to the API?
Example a controller in API:
class CategoryController extends ActiveController
{
public $modelClass = 'api\models\Category';
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => CompositeAuth::className(),
'authMethods' => [
[
'class' => HttpBasicAuth::className(),
'auth' => function($username, $password) {
$out = null;
$user = \common\models\User::findByUsername($username);
if ($user != null) {
if ($user->validatePassword($password)) $out = $user;
}
return $out;
}
],
],
];
return $behaviors;
}
}
It is called sharing sessions. It also depends on if your tier apps (frontend and api) are both in the same domain. If it is, configure your frontend and api settings (<app>/frontend/config/main.php and <app>/api/config/main.php) as follow:
'components' => [
...
'request' => [
'csrfParam' => '_csrf-shared',
],
...
'user' => [
'identityClass' => 'common\models\User',
'enableAutoLogin' => true,
'identityCookie' => ['name' => '_identity-shared', 'httpOnly' => true],
],
...
'session' => [
'name' => 'advanced-shared',
],
...
It means you save cookies and session with the same name, so that when you login in frontend, and go to backend/api, the backend side fetches same cookies, therefore you'll be detected as authenticated user.
Here one important note, in order to enableAutoLogin work for both tiers, you should set same cookieValidationKey for both main-local.php settings. You can just set them manually, or edit init.php file to generate one cookieValidationKey for all tiers. (Just make sure you know what you're doing).
By the way, I think it's not a good idea to make simultaneous authentication between frontend and api. If it's frontend and backend then it's still bearable, but api interaction is different compared to frontend.
I suggest to use headers like Authorization: Bearer <token>.. You can get more information about it here Yii2 Rest Authentication
Update
I assume this is what you need.. Create a class, i.e. ApiAuth in common/components folder and paste the following code:
<?php
namespace common\components;
use yii\filters\auth\HttpBasicAuth;
class ApiAuth extends HttpBasicAuth
{
/**
* #inheritdoc
*/
public function authenticate($user, $request, $response)
{
if ($user->identity) {
return $user->identity;
}
return parent::authenticate($user, $request, $response);
}
}
This class extends from yii\filters\auth\HttpBasicAuth. Before calling a browser prompt it checks whether user->dentity is populated. If so, no promt is required.
In your Controller behavior replace HttpBasicAuth with ApiAuth class:
use common\components\ApiAuth;
...
'authMethods' => [
[
'class' => ApiAuth::className(),
'auth' => function($username, $password) {
...
As the user is already authenticated, then I just set the "AccessControl" for connected users. If they are not connected they will receive code 403 instead of 401.
This solves the problem:
class CategoryController extends ActiveController
{
public $modelClass = 'api\models\Category';
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['access'] = [
'class' => \yii\filters\AccessControl::className(),
'rules' => [
[
'allow' => true,
'roles' => ['#'],
],
],
];
return $behaviors;
}
}

Multi Auth with Laravel 5.4 and Passport

I am trying to setup multi auth with Laravel Passport, but it doesn't seem to support it. I am using the Password Grant to issue tokens which requires me to pass username/password of the user wanting access tokens.
I have 3 auth guards/providers setup, 4 in total.
Users, Vendors, Admins and API
2 of the Auths need passport access, so each user needs to be able to issue tokens. But Passport automatically takes the API auth provider, but I want this to change based on which user is logging in.. If user does then the User provider and if its a vendor then the Vendor provider.
But the way Passport currently only supports only 1 user type, so its defaulting to the API provider.
Is there something better for this? or should I go with roles based authentication instead.
If you still need.
I prefer go with roles, there is an amazing plugin for that: https://github.com/larapacks/authorization
But if you somehow needs that, you will be able to use following the steps bellow.
For multi guards, you will have to overwrite some code.
Instead of loading PassportServiceProvider, you create your own and extends the PassportServiceProvider and overwrites the method makePasswordGrant.
On this method, you will change the Passport UserRepository for your own repository extended. On user repository you must to change the static model config for a dynamic one (I load from request attributes, but you can get from anywhere).
You may have to overwrite something else, but I made a test and works.
Ex:
PassportServiceProvider
namespace App\Providers;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Grant\PasswordGrant;
use Laravel\Passport\PassportServiceProvider as BasePassportServiceProvider;
use Laravel\Passport\Passport;
class PassportServiceProvider extends BasePassportServiceProvider
{
/**
* Create and configure a Password grant instance.
*
* #return PasswordGrant
*/
protected function makePasswordGrant()
{
$grant = new PasswordGrant(
$this->app->make(\App\Repositories\PassportUserRepository::class),
$this->app->make(\Laravel\Passport\Bridge\RefreshTokenRepository::class)
);
$grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());
return $grant;
}
}
UserRepository
namespace App\Repositories;
use App;
use Illuminate\Http\Request;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use Laravel\Passport\Bridge\UserRepository;
use Laravel\Passport\Bridge\User;
use RuntimeException;
class PassportUserRepository extends UserRepository
{
/**
* {#inheritdoc}
*/
public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
{
$guard = App::make(Request::class)->attributes->get('guard') ?: 'api';
$provider = config("auth.guards.{$guard}.provider");
if (is_null($model = config("auth.providers.{$provider}.model"))) {
throw new RuntimeException('Unable to determine user model from configuration.');
}
if (method_exists($model, 'findForPassport')) {
$user = (new $model)->findForPassport($username);
} else {
$user = (new $model)->where('email', $username)->first();
}
if (! $user ) {
return;
} elseif (method_exists($user, 'validateForPassportPasswordGrant')) {
if (! $user->validateForPassportPasswordGrant($password)) {
return;
}
} elseif (! $this->hasher->check($password, $user->password)) {
return;
}
return new User($user->getAuthIdentifier());
}
}
PS: Sorry my bad english.
You have to modify the main library Files.
1) File: vendor\laravel\passport\src\Bridge\UserRepository.php
Find getUserEntityByUserCredentials and Copy the complete method and Paste this method below with name getEntityByUserCredentials. Donot modify the main function because it is used somewhere.
//Add the $provider variable at last or replace this line.
public function getEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity, $provider)
Then, in the new duplicated function, find the below:
$provider = config('auth.guards.api.provider');
and Replace it with:
$provider = config('auth.guards.'.$provider.'.provider');
2) File: vendor\league\oauth2-server\src\Grant\PasswordGrant.php
In the function validateUser add the below code after line no. 88
$provider = $this->getRequestParameter('provider', $request);
if (is_null($provider)) {
throw OAuthServerException::invalidRequest('provider');
}
After adding replace the following code with
$user = $this->userRepository->getEntityByUserCredentials(
$username,
$password,
$this->getIdentifier(),
$client,
$provider
);
Now try this using postman
Add the provider field in your input field like
provider = api_vendors
OR
provider = api_admins
OR
provider = api_users
And so on....
make sure you have added your provider and set the drivers in the config/auth.php
'guards' => [
'api_admins' => [
'driver' => 'passport',
'provider' => 'admins',
],
'api_vendors' => [
'driver' => 'passport',
'provider' => 'vendors',
],
I hope this helps.
I have created a small package for this issue. Here's the link for the complete doc link
But the gist is, whenever a user entity gets logged in, it checks for the guards and providers.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
'customers' => [
'driver' => 'passport',
'provider' => 'customers'
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => 'App\User',
],
/**
* This is the important part. You can create as many providers as you like but right now,
* we just need the customer
*/
'customers' => [
'driver' => 'eloquent',
'model' => 'App\Customer',
],
],
You should have a controller like this:
<?php
namespace App\Http\Controllers\Auth;
use App\Customers\Customer;
use App\Customers\Exceptions\CustomerNotFoundException;
use Illuminate\Database\ModelNotFoundException;
use Laravel\Passport\Http\Controllers\AccessTokenController;
use Laravel\Passport\TokenRepository;
use League\OAuth2\Server\AuthorizationServer;
use Psr\Http\Message\ServerRequestInterface;
use Lcobucci\JWT\Parser as JwtParser;
class CustomerTokenAuthController extends AccessTokenController
{
/**
* The authorization server.
*
* #var \League\OAuth2\Server\AuthorizationServer
*/
protected $server;
/**
* The token repository instance.
*
* #var \Laravel\Passport\TokenRepository
*/
protected $tokens;
/**
* The JWT parser instance.
*
* #var \Lcobucci\JWT\Parser
*/
protected $jwt;
/**
* Create a new controller instance.
*
* #param \League\OAuth2\Server\AuthorizationServer $server
* #param \Laravel\Passport\TokenRepository $tokens
* #param \Lcobucci\JWT\Parser $jwt
*/
public function __construct(AuthorizationServer $server,
TokenRepository $tokens,
JwtParser $jwt)
{
parent::__construct($server, $tokens, $jwt);
}
/**
* Override the default Laravel Passport token generation
*
* #param ServerRequestInterface $request
* #return array
* #throws UserNotFoundException
*/
public function issueToken(ServerRequestInterface $request)
{
$body = (parent::issueToken($request))->getBody()->__toString();
$token = json_decode($body, true);
if (array_key_exists('error', $token)) {
return response()->json([
'error' => $token['error'],
'status_code' => 401
], 401);
}
$data = $request->getParsedBody();
$email = $data['username'];
switch ($data['provider']) {
case 'customers';
try {
$user = Customer::where('email', $email)->firstOrFail();
} catch (ModelNotFoundException $e) {
return response()->json([
'error' => $e->getMessage(),
'status_code' => 401
], 401);
}
break;
default :
try {
$user = User::where('email', $email)->firstOrFail();
} catch (ModelNotFoundException $e) {
return response()->json([
'error' => $e->getMessage(),
'status_code' => 401
], 401);
}
}
return compact('token', 'user');
}
}
and the request should be:
POST /api/oauth/token HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
grant_type=password&username=test%40email.com&password=secret&provider=customers
To check in your controller who is the logged in user, you can do:
auth()->guard('customers')->user()
I have done it in Laravel 7 without any custom code as suggested other answers. I have just changed 3 file as follows
config/auth.php file (My new table name is doctors)
'guards' => [
...
'api' => [
'driver' => 'passport',
'provider' => 'users',
'hash' => false,
],
'api-doctors' => [
'driver' => 'passport',
'provider' => 'doctors',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
'doctors' => [
'driver' => 'eloquent',
'model' => App\Doctor::class,
],
],
Revise my Doctor model similarly as User model (App/Doctor.php)
....
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
class Doctor extends Authenticatable
{
use HasApiTokens, Notifiable;
Finally define routes using middleware routes/api.php file as follows
//normal middleware which exist already
Route::post('/choose', 'PatientController#appointment')->middleware('auth:api');
//newly created middleware provider (at config/auth.php)
Route::post('/accept', 'Api\DoctorController#allow')->middleware('auth:api-doctors');
Now when you will create new oauth client you may use artisan passport:client --password --provider this command which prompt you in command line for choosing table
before that do not forget to run
php artisan config:cache
php artisan cache:clear
Also you can create user manually in oauth_clients table by replacing provider column value users to doctors
Some hints at reference link
https://laravel.com/docs/7.x/passport#customizing-the-user-provider
We are waiting for Laravel to add this feature to its package but for those who want to add this feature, I suggest using this package
Laravel passport is only working with User as a provider. Even if you fetch token by adding above changes with PasswordGrant & UserRepository, while you go for API call for Post and get requests are not working with changed passport provider other than User.
Better you create multi auth with session driver if must needed as Vendors and Customers. let 'User' model only for passport whose table columns supports admin, API, vendor, etc.
Repo here laravel-multiAuth
if you look for solution to the Passport Multi-Auth API
I recommend you this solution

Custom Authorisation in CakePHP 3

I have an intranet app running on IIS, using CakePHP 3. From IIS I am able to access the server var $_SERVER['AUTH_USER'] and I want to use this variable to authenticate users.
I have created a users table in my database with a username field that I want to match to AUTH_USER. I have created a custom Auth component like so:
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Network\Request;
use Cake\Network\Response;
use Cake\ORM\TableRegistry;
class AuthuserAuthenticate extends BaseAuthenticate
{
public function authenticate(Request $request, Response $response) {
$username = str_replace('DOMAIN\\', '', $_SERVER['AUTH_USER']);
$users = TableRegistry::get('Users');
$user = $users->find()->where(['username' => $username])->first();
if ($user) {
return $user;
} else {
$user = $this->Users->newEntity();
$user->username = $username;
if ($this->Users->save($user)) {
return $user;
} else {
return false;
}
}
}
And in the AppController initialize() I have tried to load Auth with the custom component.
$this->loadComponent('Auth', [
'authenticate' => [
'Basic' => [
'fields' => ['username' => 'username'],
'userModel' => 'Users'
],
],
'loginAction' => [
'controller' => 'Pages',
'action' => 'display'
],
'storage' => 'Memory',
'unauthorizedRedirect' => false
]);
$this->Auth->config('authenticate', 'Authuser');
At this point I just get redirected no matter what page I try to go on, I'm not really sure if it's failing to authenticate or something else is the problem.
I have tried adding this to AppController as a test:
public function isAuthorized($user)
{
return true;
}
But I am unable to access any pages with this code in place. Can anyone let me know what I'm doing wrong?
Thanks,
Kez
Your auth component is not implementing the authorize method.
public function authorize($user, Request $request) {
// return true if authorized
// return false if not authorized
}
Secondly, isAuthorized is called when using the ControllerAuthorize component. If you want to use controller authentication, you should use ControllerAuthorize insted.
$this->loadComponent('Auth', [
'authenticate' => 'Controller'
]);
Also: You are configuring the BasicAuthenticate component, then immediately overwriting the config.

Categories