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
Related
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
}
}
I'm developing a React/CakePHP app with a dockerized dev environment. For authentication, I'm using an OpenID Connect provider to establish the user identity, which I then encapsulate in a JWT as suggested in this article. Using the CakePHP/Authentication plugin, I redirect requests from 'https://mydomain.local/' to 'https://mydomain.local/login', which handles the OIDC logic. Once authenticated, the user is again redirected back to the site root, now with the JWT in two cookies.
My problem is that the request hangs on that final redirect. If I disable the redirect and manually navigate back to root after the cookies are set, the request works fine and my app sees the authenticated user correctly via the JWT.
For my dev environment, I'm using a Caddy container as a proxy to terminate https and a php-apache container to host the app itself. Neither server's logs show the final request occuring.
Here are the relevant portions of my code:
docker_compose.yml:
services:
caddy:
image: "abiosoft/caddy:latest"
volumes:
- ./caddy/certs:/root/certs
- ./caddy/Caddyfile:/etc/Caddyfile
- ./caddy/logs:/var/log
ports:
- "443:2015"
depends_on:
- web
web:
build:
context: .
links:
- db
volumes:
- "./src:/var/www/html/src:rw"
db:
image: mysql:latest
caddy/Caddyfile:
mydomain.local {
log /var/log/access.log
# Mkcert - https://github.com/FiloSottile/mkcert
tls /root/certs/mydomain.local.pem /root/certs/mydomain.local-key.pem
proxy / http://web:80 {
transparent
}
}
src/Application.php:
public function middleware($middlewareQueue)
{
$middlewareQueue
->add(new ErrorHandlerMiddleware(null, Configure::read('Error')))
->add(new AssetMiddleware([
'cacheTime' => Configure::read('Asset.cacheTime')
]))
->add(new RoutingMiddleware($this))
->prepend(new JwtMiddleware())
->add(new AuthenticationMiddleware($this));
return $middlewareQueue;
}
public function getAuthenticationService(ServerRequestInterface $request, ResponseInterface $response)
{
$service = new AuthenticationService([
'unauthenticatedRedirect' => Router::url(['controller' => 'Main', 'action' => 'login']),
'queryParam' => 'redirect',
]);
$service->loadIdentifier('Authentication.JwtSubject', [
'tokenField' => 'id',
'dataField' => 'sub',
'resolver' => 'Authentication.Orm',
]);
$service->loadAuthenticator('Authentication.Jwt', [
'header' => 'Authorization',
'queryParam' => 'token',
'tokenPrefix' => 'Bearer',
'algorithms' => ['HS256'],
'returnPayload' => 'false',
'secretKey' => Security::getSalt(),
]);
return $service;
}
src/Middleware/JwtMiddleware.php:
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\ValidationData;
class JwtMiddleware
{
public function __invoke(RequestInterface $request, ResponseInterface $response, $next)
{
$jwt[0] = $request->getCookie('sa');
$jwt[1] = $request->getCookie('sb');
if (!empty($jwt[0]) && !empty($jwt[1])) {
$data = new ValidationData();
$data->setIssuer('mydomain');
$data->setAudience('mydomain.local');
$data->setId('mydomain');
$jwt = implode('.', $jwt);
$token = (new Parser())->parse((string) $jwt);
if ($token->validate($data)) {
$request = $request->withAddedHeader('Authorization', 'Bearer ' . $jwt);
$response = $response->withCookie((new Cookie('sa'))
->withValue($token->getPayload())
->withExpiry(new \DateTime('+30 minutes'))
->withPath('/')
->withHttpOnly(false)
);
}
}
return $next($request, $response);
}
}
src/Controller/MainController.php:
use Jumbojett\OpenIDConnectClient;
use Jumbojett\OpenIDConnectClientException;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key;
/**
* Main Controller
*
* #property UsersTable $Users
*/
class MainController extends AppController
{
public function beforeFilter(Event $event)
{
$this->Authentication->allowUnauthenticated(['login']);
return parent::beforeFilter($event);
}
/**
* Index method
*
* #return Response|null
*/
public function index()
{
$filePath = WWW_ROOT . '/static.html';
$file = new File($filePath);
$index = $file->read();
$file->close();
return $this->response->withStringBody($index);
}
/**
* Login method
*
* #return Response|null
* #throws OpenIDConnectClientException
*/
public function login()
{
$oidc = new OpenIDConnectClient(
env('OIDC_URL'),
env('OIDC_CLIENT'),
env('OIDC_SECRET')
);
$oidc->addScope('openid');
$oidc->addScope('profile');
$oidc->authenticate();
$this->loadModel('Users');
$user = $this->Users->find()
->where(['auth_id' => $oidc->requestUserInfo('sub')])
->firstOrFail();
$signer = new Sha256();
$time = time();
$token = (new Builder())
->issuedBy('mydomain')
->permittedFor('mydomain.local')
->identifiedBy('mydomain')
->issuedAt($time)
->expiresAt($time + 3600)
->withClaim('sub', $user->id)
->getToken($signer, new Key(Security::getSalt()));
$signature = explode('.', $token->__toString())[2];
$sa = (new Cookie('sa'))
->withValue($token->getPayload())
->withExpiry(new \DateTime('+30 minutes'))
->withPath('/')
->withHttpOnly(false);
$sb = (new Cookie('sb'))
->withValue($signature)
->withPath('/')
->withHttpOnly(true);
$this->response = $this->response
->withCookieCollection(new CookieCollection([$sa, $sb]));
/**** HANG OCCURS ON THIS LINE ****/
return $this->redirect($this->Authentication->getLoginRedirect());
}
}
Any advice/suggestions greatly appreciated!!!
The issue was that the redirect was insecure because the app server was running HTTP (with SSL terminated at the proxy). Changing the last line of login() in MainController.php to
return $this->redirect(Router::url('/', true)); // generate full URL
and setting fullBaseUrl in config/app.php to 'https://mydomain.local' fixed the issue.
I need to configure my providers dynamically.
$config = [
'client_id' = 'xxxxxxx',
'client_token' = 'xxxxxxx',
'redirect' = 'http://example.com/'
];
return Socialite::with($provider)->setConfig($config)->redirect();
But unfortunately there is no function setConfig.
I need to set provider, client_id, client_secret and redirect dynamically
Is there any ideas?
Thank you!
You could use the Socialite buildProvider method like:
$config = [
'client_id' = 'xxxxxxx',
'client_token' = 'xxxxxxx',
'redirect' = 'http://example.com/'
];
return Socialite::buildProvider(\Laravel\Socialite\Two\FacebookProvider::class, $config);
Where \Laravel\Socialite\Two\FacebookProvider::class would be swapped with your service (if different) as provided in either folder One/Two in https://github.com/laravel/socialite/tree/2.0/src
I use the following service provider in order to automatically fill in the redirect for each provider where it's empty.
It could be modified to update your configuration on the fly. It depends exactly what you're trying to do I suppose.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class SocialServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
collect(config('services'))
->only(config('social.providers'))
->reject(function($config) {
return array_get($config, 'redirect', false);
})
->each(function($config, $key) {
$url = url("login/{$key}/callback", [], true);
config(["services.{$key}.redirect" => $url]);
});
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
}
}
This could help if anyone still faces the problem
you can set the Redirect Url manually
$driver = Socialite::driver('google');
$driver->redirectUrl('your-custom-url');
I am working on integrating LDAP authentication in my project. and I followed the tutorial from official CakePHP site that guides through how to create a custom object in application src path and using those custom objects in AuthController.
So I created a folder called Auth in src with the file name called LdapAuthorize.php. The path looks like this src/Auth/LdapAuthorize.php
Here is my LdapAuthorize.php code:
namespace App\Auth;
use Cake\Auth\BaseAuthorize;
use Cake\Network\Request;
class LdapAuthorize extends BaseAuthorize {
public function authorize($user, Request $request) {
if ($user == 'username') { // where username is logged on ldap user on a computer.
return true;
}
}
}
I also called the object in AppController.php file. Here is my code:
public function initialize()
{
parent::initialize();
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'loginRedirect' => [
'controller' => 'Customers',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display',
'home'
]
]);
$this->Auth->config('authenticate', [
'Ldap'
]);
}
So when I access the url http://localhost/AppPath/Dashboard/index I get Authentication adapter "Ldap" was not found.
Since this is my first experience with CakePHP, I couldn't find that many solution online that help troubleshoot any issues.
Adding additional code for LdapAuthenticate.php:
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Network\Request;
use Cake\Network\Response;
class OpenidAuthenticate extends BaseAuthenticate
{
public function authenticate(Request $request, Response $response)
{
$users = ["john", "ray"];
return $users;
}
}
What you need is a custom authentication adapter, your LdapAuthorize is a custom authorize adapter:
// in src/Auth/LdapAuthenticate.php
namespace App\Auth;
use Cake\Auth\BaseAuthenticate;
use Cake\Network\Request;
use Cake\Network\Response;
class LdapAuthenticate extends BaseAuthenticate {
protected $_host = 'your_ldap_server' ;
public function authenticate(Request $request, Response $response) {
$username = $request->data['username'] ;
$password = $request->data['password'] ;
$ds = #ldap_connect($this->_host) ;
if (!$ds) {
throw \Cake\Error\FatalErrorException ('Unable to connect to LDAP host.') ;
}
$basedn = "your ldap query... "
$dn = "uid=$username, ".$basedn;
$ldapbind = #ldap_bind($ds, $dn, $password);
if (!$ldapbind) {
return false ;
}
// Do whatever you want with your LDAP connection...
$entry = ldap_first_entry ($ldapbind) ;
$attrs = ldap_get_attributes ($ldapbind, $entry) ;
$user = [] ;
// Loop
for ($i = 0 ; $i < $attrs["count"] ; $i++) {
$user[$attrs[$i]] = ldap_values ($ldapbind, $entry, $attrs[$i])[0] ;
}
// Then close it and return the authenticated user
ldap_unbind ($ldapbind) ;
ldap_close ($ldapbind);
return $user ;
}
}
I was still having the same error after creating the custom authentication adapter suggested above.
I solved it changing
namespace App\Auth;
for
namespace Cake\Auth;
In LdapAuthenticate.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);