Test basic auth - php

I want to test my basic auth protected pages. The test for unauthorization works fine. But I struggle on the authorized login, as I do not know how to set the headers on in the test.
I could not find a hint, how to set headers on $this->call(). The only information I could find was:
$this->call($method, $uri, $parameters, $cookies, $files, $server, $content);
and there are the headers missing.
How do I easily test basic auth on laravel. Concrete: How do I set the basic auth header for the test request?
What I currently have:
class ExampleTest extends TestCase {
public function test401UnauthorizedOnMe() {
$response = $this->call('GET', '/api/me');
$this->assertResponseStatus( 401);
}
public function testCorrectLoginOnMe() {
// http://shortrecipes.blogspot.de/2009/12/testing-basic-http-authentication-using.html
//send header with correct user and password i.e.
////YWRtaW46YWRtaW4xMg== is equal to base64_encode( "admin:admin12")
$this->request->setHeader( 'Authorization','Basic YWRtaW46YWRtaW4xMg==');
$response = $this->call('GET', '/api/me');
$this->assertResponseStatus(200);
}
}
I tried $this->$request->setHeader(); but with this I only get an error:
1) ExampleTest::testCorrectLoginOnMe
ErrorException: Undefined property: ExampleTest::$request

Found the solution with HTTP authentication with PHP. This can be used in the $server parameter of $this->call().
Here's my working function:
public function testCorrectLoginOnMe() {
// call( $method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
$this->call('GET', '/api/me', [], [], [], ['PHP_AUTH_USER' => 'admin', 'PHP_AUTH_PW' => 'admin12']);
$this->assertResponseStatus( 200 );
}

Basic Auth is usually achieved with a header key of 'Authorization'. For convenience, I have the following method in my base TestCase class:
protected function withBasicAuth(User $user, $password = 'password'): self
{
return $this->withHeaders([
'Authorization' => 'Basic '. base64_encode("{$user->email}:{$password}")
]);
}
Then in any of my test cases I can run a HTTP test with a user authenticated over basic auth like so:
$user = User::factory()->create();
$this->withBasicAuth($user)
->get('/');
->assertStatus(Response::HTTP_OK);
Note: the default password for a user created from the factory is 'password'.

$encoded_details = base64_encode('admin:admin12');
$headers = [
'HTTP_Authorization' => 'Basic '. $encoded_details
];
$response = $this->withHeaders($headers)->json('GET', '/api/me');
Just another solution which worked for me

protected function basicAuthenticate($detailsEncoded, $user, $password): self
{
$_SERVER['HTTP_AUTHORIZATION'] = $detailsEncoded;
$_SERVER['PHP_AUTH_USER'] = $user;
$_SERVER['PHP_AUTH_PW'] = $password;
return $this;
}
$response = $this->basicAuthenticate($detailsEncoded, $user, $password)->get($url);
$response->assertStatus(200);

Related

$user null but not null on json login

I have a very weird behavior on symfony 6 with json login. Json login is configured like in their example.
I added the most simple Cors-handling
class CorsSubscriber implements EventSubscriberInterface
{
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
$response->headers->add([
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Headers' => 'Content-Type'
]);
}
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
}
Login Controller
class LoginController extends AbstractController
{
#[Route('/login', name: 'api_login')]
public function login(#[CurrentUser] ?User $user): JsonResponse
{
if ($user === null) {
$this->json([
'message' => 'Not found',
], JsonResponse::HTTP_UNAUTHORIZED);
}
$token = "token"; // some token logik follows
$response = new JsonResponse([
'user' => $user->getUserIdentifier(),
'token' => $token,
]);
return $response;
}
}
Again, basically straight out of their tutorial.
DB is also set up and a user was added beforehand. If I now want to login via a Rest App (I use Insomnia) everything works as expected. I get the user identifier (email) and token back.
But when I want to do the exact same thing via an Angular web app, I get the following message in the log: [critical] Uncaught Error: Call to a member function getUserIdentifier() on null
let data = {username: "test#test.de", password: "test123"};
this.http
.post('http://localhost:8000/login', data)
.subscribe({
next: (value) => {
console.log(value);
},
});
Funny thing since I check if $user is null...
But the most funny thing is, if I change the $user->getUserIdentifier() call to just "$user" - it works and I get the whole User object returned (everything I defined in json_serializable)
Again, getUserIdentifier is from symfony itself
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUserIdentifier(): string
{
return (string) $this->email;
}
What am I missing?
In the documentation check_path: api_login is defined in the "security.yaml ".
In the ApiLoginController #[route('/api/login', name:'api_login')]
I wanted to change this to the path "/login".
Thereby I defined check_path: login in security.yaml
and in the controller #[route('/login', name:'api_login')]
However, the name must also be "login", like #[route('/login', name:'login')]
Found out what's wrong.
No return
if ($user === null) {
$this->json([
'message' => 'Not found',
], JsonResponse::HTTP_UNAUTHORIZED);
}
I missed the return before $this->json, allowing the call to proceed even while $user was indeed null.
Preflight Cors
I honestly don't have too much experience and knowledge about cors and wasn't really aware about the preflight calls being done. But that was the problem.
public function onKernelRequest(RequestEvent $event)
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$method = $request->getRealMethod();
if ('OPTIONS' == $method) {
$response = new Response();
$event->setResponse($response);
}
}
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => ['onKernelResponse', 9999],
KernelEvents::REQUEST => ['onKernelRequest', 9999]
];
}
I extended the CorsSubscriber to check for Method OPTION if it is not the main request and just sending a 200 to pass the preflight.
This helped me understand the logic behind it:
https://gist.github.com/rordi/42de5982e877e1177a0e6a98ee8415ea
https://www.upbeatproductions.com/blog/cors-pre-flight-requests-and-headers-symfony-httpkernel-component

Listeners and Cookies in Laravel

I try to use setcookie in Laravel listeners with queue, setcookie return true but the problem is in chrome (Or Firefox) DevTools> Application> Cookies cookies are not set and not working.
My Class:
class FlarumEventSubscriber implements ShouldQueue
{
public function onUserLogin($event)
{
$user = $event->user;
$response = $this->authenticate($user->mobile, $user->password);
$token = $response['token'] ?: '';
setcookie('flarum_session', $token, time() + 99999999 , '/', 'localhost'); // ======> The problem
}
public function subscribe($events)
{
$events->listen(
'Illuminate\Auth\Events\Login',
'App\Listeners\FlarumEventSubscriber#onUserLogin'
);
}
private function authenticate($id, $password)
{
$endpoint = '/api/token';
$method = 'POST';
$data = [
'identification' => $id,
'password' => $password,
'lifetime' => 99999999
];
return $this->sendRequest($endpoint, $method, $data);
}
}
EventServiceProvider :
class EventServiceProvider extends ServiceProvider
{
/**flarum subscriber */
protected $subscribe = [
'App\Listeners\FlarumEventSubscriber',
];
}
In FlarumEventSubscriber class, I send a request after the user login to Laravel, and after that, some information needs to be stored in the cookie.
My efforts:
Use Cookie::queue() instead of setcookie() => Not working
Test on other domains => Not working
Info:
PHP v7.4.26
Laravel 8.0
Wampserver v3.2.6 64bit

PHP Unit testing setting JWT token after login test completes

Hi I'm trying to set JWT token after the login test is complete in a global var and then extend all tests off of that class, now I'm not sure if I am even doing it right so any help is appreciated or documentation I could look up
<?php
use Illuminate\Support\Facades\Config;
class AuthenticationTest extends TestCase {
/**
* #var string
*/
public $token = '';
/**
* Register test user
*
* #return void
*/
public function testRegister()
{
$this->json('post', '/auth/register', [
'email' => Config::get('tests.email'),
'password' => Config::get('tests.password'),
'companyName' => Config::get('tests.companyName'),
'firstname' => Config::get('tests.firstname'),
'lastname' => Config::get('tests.lastname')
]);
$this->assertEquals(200, $this->response->status());
$this->seeJsonEquals([
'message' => "Registration successful, please confirm email to login",
'status' => true,
]);
}
/**
* Login test user
*
* #return void
*/
public function testLogin()
{
$email = Config::get('tests.email');
$password = Config::get('tests.password');
$encryptedToken = base64_encode($email . ":" . $password);
$this->json('post', 'auth/login', ['token' => $encryptedToken]);
$this->assertEquals(200, $this->response->status());
$this->seeJsonStructure([
'token',
'refresh',
'status'
]);
$content = json_decode($this->response->getContent());
$this->assertObjectHasAttribute('token', $content);
$this->token = $content->token;
$this->token;
}
}
?>
The login test works as it should but the token isn't being set for other tests that extend off of this test case ad use $this->token to send the JWT token
PHPUnit creates a new instance of AuthenticationTest for every test method to run each test in isolation.
For tests that need an authenticated user, you have to obtain the token somehow. You could make an api request like you did in testLogin. A bit faster and less brittle way would be to create the token programatically using the same service as in your production code.
To reduce boilerplate, create a test case for tests with an authenticated and create the token in the setup method. Tests that extend from this class have the token property. Alternatively, you could also make this functionality a trait.
EDIT:
Here's a simple code example.
abstract class AuthenticatedUserTestCase extends TestCase
{
protected function token(): string
{
$email = Config::get('tests.email');
$password = Config::get('tests.password');
$encryptedToken = base64_encode($email . ':' . $password);
$response = $this->json('post', 'auth/login', ['token' => $encryptedToken]);
$content = json_decode($response->getContent());
if (!isset($content->token)) {
throw new RuntimeException('Token missing in response');
}
return $content->token;
}
}
final class UserProfileTest extends AuthenticatedUserTestCase
{
public function test_that_user_can_access_their_profile_page()
{
$response = $this->json('get', '/user/profile', [], ['token' => $this->token()]);
// ... assert stuff
}
}

CakePHP and REST Api for ionic (angular) app

Hello I try to setup cakephp for rest client (with login auth) for ionic (angular) app.
Ok, I configure CakePhp like this setup tutorial and for example I get data that:
public function projects()
{
$projects = $this->Projects->find('all');
$this->set([
'projects' => $projects,
'_serialize' => ['projects']
]);
}
and get data via $.http in Ionic
This work perfectly but I try to configure cake auth for mobile client.
I don't know how I do this. In my Resttest Controller i wrote code where set session Id for ionic app, but ionic not cache this session and I think is my cakePhp code is wrong.
CakePHP controller:
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Controller\Component\RequestHandlerComponent;
// use Cake\View\Helper\SessionHelper;
class ResttestController extends AppController
{
public function initialize()
{
parent::initialize();
$this->loadComponent('RequestHandler');
$this->loadModel('Projects');
$this->loadModel('Task');
$this->loadModel('User');
$this->viewBuilder()->layout(false);
$this->response->header('Access-Control-Allow-Origin', '*');
$this->loadComponent('Auth', [
'loginAction' => [
'controller' => $this->name,
'action' => 'login',
// '_ext'=>'json'
],
'authorize'=>['Controller'],
]);
// Basic setup
$this->Auth->config('authorize', ['Controller']);
}
public function login(){
header('Access-Control-Allow-Headers: Content-Type, x-xsrf-token');
$this->response->header('Access-Control-Allow-Methods', '*');
if($this->request->is('post')){
$postdata = file_get_contents("php://input");
$d = json_decode($postdata);
if($this->Auth->user()){
$response =array("success"=>2,'msg'=>'logged After');
}
// $d = $this->request->data;
if(!$d->password || !$d->login){
$response = array("success"=>0,'msg'=>'n');
}
$u = $this->User->find()
->where(['email'=>$d->login])
->first();
if($u){
$salt = $u->salt;
$input_password = crypt($d->password, '$2y$12$' . $salt);
$password = $u->password;
if($password == $input_password){
$tok = self::getToken();
$u->token = $tok;
$out = $this->Auth->setUser($u);
$response = array("success"=>1,'msg'=>'logged', 'token'=>$tok, 'out'=>$out,'sadga'=>$this->Auth->identify,'asf'=>$this->Auth,'adsafsfq'=>$d,'$this->request'=>$this->request,'$this->response'=>$this->response,'apache_request_headers '=>apache_request_headers());
}else{
$response = array("success"=>0,'msg'=>'n');
}
}else{
$response = array("success"=>0,'msg'=>'n');
}
}else{
$response =array("success"=>0,'msg'=>'n');
}
$this->set([
'response' => $response,
'_serialize' => ['response']
]);
}
private function getToken(){
return crypt(sha1(md5(uniqid(rand(), true))));
}
public function testAuth(){
}
}
This code return session and user data but not work and I think is not good method for mobile auth. Do you have any idea for auth for cakephp ?
How I make my code more security ?
When we split application to backend api and frontend, we should consider backend as stateless application. This mean you can't use session for auth.
Instead you should implements auth/login and auth/register rest endpoints that will return some token for example JWT.
For cakephp2 you can easely find such library: https://github.com/t73biz/cakephp2-jwt-auth
Use this authenticator instead of Form when you configure Auth component.
From front end side pass token like it is described in the plugin.

Slim Framework endpoint unit testing

I'm trying to write some PHPUnit tests for my small slim framework app, but don't see anywhere in the docs that point to a way to do a full request and assert on the response (either containing text or a 200 status, or anything, really).
Is there any way to do this that anyone has found/used?
Here is example how you may test your Slim application:
https://github.com/mac2000/SlimTestable
Suppose we have simple application:
<?php
use Slim\Slim;
require_once 'vendor/autoload.php';
$app = new Slim();
$app->get('/', function(){
echo 'home';
})->name('home');
$app->get('/hello/:name', function($name){
echo "hello $name";
})->name('hello');
$app->map('/login', function() use($app) {
if($app->request()->params('login')) {
$app->flash('success', 'Successfully logged in');
$app->redirect($app->urlFor('hello', array('name' => $app->request()->params('login'))));
} else {
$app->flash('error', 'Wrong login');
$app->redirect($app->urlFor('home'));
}
})->via('GET', 'POST');
$app->run();
How do we test it?
Create App class:
<?php // src/App.php
use Slim\Slim;
class App extends Slim {
function __construct(array $userSettings = array())
{
parent::__construct($userSettings);
$this->get('/', function(){
echo 'home';
})->name('home');
$this->get('/hello/:name', function($name){
echo "hello $name";
})->name('hello');
$this->map('/login', function() {
if($this->request()->params('login')) {
$this->flash('success', 'Successfully logged in');
$this->redirect($this->urlFor('hello', array('name' => $this->request()->params('login'))));
} else {
$this->flash('error', 'Wrong login');
$this->redirect($this->urlFor('home'));
}
})->via('GET', 'POST');
}
/**
* #return \Slim\Http\Response
*/
public function invoke() {
$this->middleware[0]->call();
$this->response()->finalize();
return $this->response();
}
}
Notice that we move all our routes to new class constructor, also notice new invoke method, which do the same as run method except it returns response rather than echoing it out.
Now your index.php file might be like this one:
<?php
require_once 'vendor/autoload.php';
$app = new App();
$app->run();
And now it is time for tests:
<?php // tests/ExampleTest.php
use Slim\Environment;
class ExampleTest extends PHPUnit_Framework_TestCase {
private $app;
public function setUp()
{
$_SESSION = array();
$this->app = new App();
}
public function testHome() {
Environment::mock(array(
'PATH_INFO' => '/'
));
$response = $this->app->invoke();
$this->assertContains('home', $response->getBody());
}
public function testHello() {
Environment::mock(array(
'PATH_INFO' => '/hello/world'
));
$response = $this->app->invoke();
$this->assertTrue($response->isOk());
$this->assertContains('hello world', $response->getBody());
}
public function testNotFound() {
Environment::mock(array(
'PATH_INFO' => '/not-exists'
));
$response = $this->app->invoke();
$this->assertTrue($response->isNotFound());
}
public function testLogin() {
Environment::mock(array(
'PATH_INFO' => '/login'
));
$response = $this->app->invoke();
$this->assertTrue($response->isRedirect());
$this->assertEquals('Wrong login', $_SESSION['slim.flash']['error']);
$this->assertEquals('/', $response->headers()->get('Location'));
}
public function testPostLogin() {
Environment::mock(array(
'REQUEST_METHOD' => 'POST',
'PATH_INFO' => '/login',
'slim.input' => 'login=world'
));
$response = $this->app->invoke();
$this->assertTrue($response->isRedirect());
$this->assertEquals('Successfully logged in', $_SESSION['slim.flash']['success']);
$this->assertEquals('/hello/world', $response->headers()->get('Location'));
}
public function testGetLogin() {
Environment::mock(array(
'PATH_INFO' => '/login',
'QUERY_STRING' => 'login=world'
));
$response = $this->app->invoke();
$this->assertTrue($response->isRedirect());
$this->assertEquals('Successfully logged in', $_SESSION['slim.flash']['success']);
$this->assertEquals('/hello/world', $response->headers()->get('Location'));
}
}
You should notice few things:
While setting up test we are creating $_SESSION array for test purposes and instantiate our App class object.
In tests rather than run we are calling invoke which do the same, but returns response object.
Environment::mock used to mock requests which are processed with our application.
Ok, so I was able to rough it and make it work. Here's an example of an endpoint test class.
Assuming you're working in a development environment, you can execute curl requests to your own localhost, thus testing before committing to a repo.
First, create your class:
class ApiEndpointsTest extends PHPUnit_Framework_TestCase
{
protected $api_url = "http://localhost/api/v1";
//create a function that will allow you to call API endpoints at-will.
private function loadEndpoint($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
return array(
'body' => $output,
'info' => $info
);
}
//this allows you to write messages in the test output
private function printToConsole($statement) {
fwrite(STDOUT, $statement."\n");
}
Using this, you can write a test function for a particular endpoint response:
//this will test the actual body of the response against something expected.
public function testGetUserResponse() {
$this->printToConsole(__METHOD__);
$url = $this->api_url."/users/124";
$response = $this->loadEndpoint($url);
$expected = '[{"name":"John Smith","email":"john#acme.com"}]';
$this->assertEquals($response['body'], $expected);
}
In a separate test, you can test any other property of the API call's response:
public function testGetUserMimeType() {
$this->printToConsole(__METHOD__);
$url = $this->api_url."/users/124";
$response = $this->loadEndpoint($url);
$this->assertEquals($response['info']['content_type'], 'application/json');
}
Your info property options can be found here: http://php.net/manual/en/function.curl-getinfo.php
Side note: if anyone reading this is an expert at PHPUnit and knows a better way, I'm interested in learning about it -- I'm new to PHPUnit.

Categories