Please help me in fixing this problem. I want to try sizeg/yii2-jwt (https://github.com/sizeg/yii2-jwt). I followed the Step-by-step usage example but I always get authorization issues. I also want to change the Model (I want to replace it with something other than the User model).
On Github it says after installing the plugin I have to edit web.php
'jwt' => [
'class' => \sizeg\jwt\Jwt::class,
'key' => 'secret',
'jwtValidationData' => \app\components\JwtValidationData::class,
],
After that I should create JwtValidationData class. where you have to configure ValidationData informing all claims you want to validate the token:
class JwtValidationData extends \sizeg\jwt\JwtValidationData
{
/**
* #inheritdoc
*/
public function init()
{
$this->validationData->setIssuer('');
$this->validationData->setAudience('');
$this->validationData->setId('4f1g23a12aa');
parent::init();
}
}
in the User model:
public static function findIdentityByAccessToken($token, $type = null)
{
foreach (self::$users as $user) {
if ($user['id'] === (string) $token->getClaim('uid')) {
return new static($user);
}
}
return null;
}
And the controller:
class ProfileController extends Controller {
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => JwtHttpBearerAuth::class,
'optional' => [
'login',
],
];
return $behaviors;
}
private function generateJwt($id) {
$jwt = Yii::$app->jwt;
$signer = $jwt->getSigner('HS256');
$key = $jwt->getKey();
$time = time();
return $jwt->getBuilder()
->issuedBy('')
->permittedFor('')
->identifiedBy('4f1g23a12aa', true)
->issuedAt($time)
->expiresAt($time + 3600)
->withClaim('uid', $id)
->getToken($signer, $key);
}
public function actionLogin($person_id)
{
$token = $this->generateJwt($person_id);
return $this->asJson([
'id' => $token->getClaim('uid'),
'token' => (string) $token
]);
}
public function actionData()
{
return $this->asJson([
'success' => true
]);
}
}
I thought it was the same as the tutorial but I always get unauthorized. How to solve this problem?
You just created a token for the user, but where you use that?
you have to send token as "Bearer" authentication in your header to achieve this goal if you want to authenticate the user by "JwtHttpBearerAuth" behavior.
otherwise, you have to login the user manually in your code.
Related
Beware with me for a second as I try to lay the background to my issue.
So I having using the python web framework Flask close to a year now and it has a wonderful extension called Flask-Login that helps provide user session management kind of like this in laravel.
Having said all that, there is a certain feature in Flask-Login that provides the functionality that when a user is not logged or signed in and tries to access that a page that requires one to be authenticated for example /create_post, they will be redirected back to the login page with that page encoded in the query string like /login?next=%2Fcreate_post.
Am trying to implement the same feature in a laravel project that am working on so I can redirect the user to the page they probably wanted to go to in the first place or to a different route in case that query string doesn't exist and I cannot seem to find where to put my code to do just that and I don't want to mess with anything in the vendor directory(because of the obvious issues that come with that), and I have tried manipulating the file app/Http/Middleware/RedirectIfAuthenticated.php by doing what is below but with no success.
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
return redirect('/');
}
$previous_url = url()->previous(); // how do I insert this in query string
return $next($request);
}
Will I have to create my own middleware or is there another way of implementing this kind of feature in laravel?
NOTE: I am not using the default laravel authentication system. I have created my own controller SessionsController to handle logins which contains the below code.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\User;
class SessionsController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['create', 'login']);
}
public function create()
{
$data = [
'title' => 'Login',
'body_class' => 'hold-transition login-page',
];
return view('auth.login', $data);
}
public function login(Request $request)
{
$this->validate($request, [
'username' => 'required',
'password' => 'required',
]);
$user = User::checkCredentials($request->username, $request->password);
if (!$user) {
return back()->with([
'class' => 'alert-danger',
'message' => 'Please check your credentials',
]);
}
// set session active flag to true
$user->session_active = true;
$user->save();
auth()->login($user);
return redirect()->route('dashboard');
}
public function destroy()
{
$user = auth()->user();
$user->last_login = date('Y-m-d H:i:s');
$user->session_active = false;
$user->save();
auth()->logout();
return redirect()->route('login')->with([
'class' => 'alert-success',
'message' => 'You logged out successfully',
]);
}
}
Thank you.
I managed to somewhat solve my issue even though I didn't use query strings as I had wanted.
I create a helper function get_previous_url as shown below
/**
* Gets the previous url
*
* #return null|string
*/
function get_previous_url()
{
$host = $_SERVER['HTTP_HOST'];
$previous_url = url()->previous();
// check if previous url is from the same host
if (!str_contains($previous_url, $host)) {
return null;
}
// get the previous url route
list(, $route) = explode($host, $previous_url);
// make sure the route is not the index, login or logout route
if (in_array(substr($route, 1), ['', 'login', 'logout'])) {
$route = '';
}
return $route;
}
And then I called the same function in my SessionsController class in the create method by doing this
public function create()
{
$previous_url = get_previous_url();
if ($previous_url) {
session(['previous_url' => $previous_url]);
}
...
}
And then I changed my login method to
public function login(Request $request)
{
...
$redirect = redirect()->route('dashboard'); // '/'
if (session()->has('previous_url')) {
$redirect = redirect(session()->pull('previous_url'));
}
return $redirect;
}
I have a front-end SPA built on Angular 6 and back-end on Laravel 5.6. I'm trying to make a facebook auth using ngx-social-login on the front-end and a Socialite on the back-end.
That is code in my component
signInWithFacebook(): void {
this.sas.signIn(FacebookLoginProvider.PROVIDER_ID).then(
userData => this.as.fb(userData.authToken).subscribe(x => {
console.log(x);
})
);
}
And this is a service
fb(data): Observable<any> {
return this.http.get(this.API_URL, data);
}
And here is my Laravel routes
$api->version('v1', function ($api) {
$api->get('auth/facebook', 'SocialAuthFacebookController#redirectToProvider');
$api->get('auth/facebook/callback', 'SocialAuthFacebookController#callback');
});
That is a controller
public function redirectToProvider()
{
return Socialite::driver('facebook')->stateless()->redirect();
}
public function callback()
{
$user = Socialite::driver('facebook')->stateless()->user();
$authUser = $this->findOrCreateUser($user, 'facebook');
$token = JWTAuth::fromUser($authUser);
return Response::json(compact('token'));
}
public function findOrCreateUser($user, $provider)
{
$authUser = User::where('provider_id', $user->id)->first();
if ($authUser) {
return $authUser;
}
return User::create([
'name' => $user->name,
'email' => $user->email,
'provider' => $provider,
'provider_id' => $user->id
]);
}
Since I'm using Laravel as an API-only so I suppose that I cannot access redirectToProvider so that I tried to call auth/facebook/callback and pass it an authToken that I get after a login on my SPA. However, it doesn't seem to work.
I'm experiencing the next error
Thanks to Facebook there is so much information so that I don't know what's wrong and what to do with it.
Here's an example that might help:
/**
* Redirect the user to the Facebook authentication page.
*
* #return Response
*/
public function redirectToProvider()
{
return Socialite::driver('facebook')->stateless()->redirect();
}
/**
* Obtain the user information from Facebook.
*
* #return JsonResponse
*/
public function handleProviderCallback()
{
$providerUser = Socialite::driver('facebook')->stateless()->user();
$user = User::query()->firstOrNew(['email' => $providerUser->getEmail()]);
if (!$user->exists) {
$user->name = $providerUser->getName();
$user->save();
}
$token = JWTAuth::fromUser($user);
return new JsonResponse([
'token' => $token
]);
}
Background
I have a microservice setup the flow is:
client > api gateway > auth server > api gateway > microservice
The client has a 'external' JWT from Laravel passport
Client sends request to the api gateway with the 'external' JWT
The api gateway sends a request to the auth server (Laravel passport) with the 'external' JWT
The auth server verifies the user is still active and returns a new 'internal' JWT to the api gateway containing the users profile, groups etc
The api gateway forwards the request with this new 'internal' JWT to the microservice
(all fine up to this point)
The Microservice verifies the 'internal' JWT using the auth servers public key
The microservice decodes the 'internal' JWT and creates a user object from the profile contained within
If the microservice has a local users table (e.g. for microservice specific user data), merge the local data with the JWT data
Microservice Authentication
I have created a JwtGuard that can decode the JWT and create a user using GenericUser:
auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
],
AuthServiceProvider.php
public function boot()
{
$this->registerPolicies();
Auth::extend('jwt', function ($app) {
return new JwtGuard($app['request']);
});
}
JwtGuard.php
<?php
namespace App\Services\Auth;
use Illuminate\Auth\GenericUser;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use \Firebase\JWT\JWT;
use Illuminate\Http\Request;
class JwtGuard implements Guard {
use GuardHelpers;
/**
* #var Request
*/
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
/**
* Get the currently authenticated user.
*
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
{
if (!is_null($this->user)) {
return $this->user;
}
if(!$jwt = $this->getJwt()) {
return null;
}
return $this->decode($jwt);
}
/**
* Validate a user's credentials.
*
* #param array $credentials
* #return bool
*/
public function validate(array $credentials = [])
{
if(!$jwt = $this->getJwt()) {
return false;
}
return !is_null($this->decode($jwt))?true:false;
}
/**
* Decode JWT and return user
*
* #return mixed|null
*/
private function decode($jwt)
{
$publicKey = file_get_contents(storage_path('oauth-public.key'));
try {
$res = JWT::decode($jwt, $publicKey, array('RS256'));
return $this->user = new GenericUser(json_decode(json_encode($res->user), true));
} catch (\Exception $e) {
return null;
}
}
private function hasAuthHeader()
{
return $this->request->header('Authorization')?true:false;
}
private function getJwt()
{
if(!$this->hasAuthHeader()){
return null;
}
preg_match('/Bearer\s((.*)\.(.*)\.(.*))/', $this->request->header('Authorization'), $jwt);
return $jwt[1]?$jwt[1]:null;
}
}
The problem
This works ok(ish), except that:
I can't use authorization policies properly as GenericUser doesn't have the can() method
There is no easy way to merge with a local user object
What I have so far
I have tried the following to merge the local user data with the JWT profile:
private function decode($jwt)
{
$publicKey = file_get_contents(storage_path('oauth-public.key'));
try {
$res = JWT::decode($jwt, $publicKey, array('RS256'));
$this->user = new GenericUser(json_decode(json_encode($res->user), true));
$this->user->localUser = \App\User::where('user_id', $this->user->id)->first();
return $this->user;
} catch (\Exception $e) {
return null;
}
}
but this still leaves GenericUser not having the can() function.
Help...please!
I can't help feel there is a better (proper?) way to achieve this using 'User' instead of 'GenericUser' which will allow all the Authentication/Authorization features in Laravel to work properly, and to merge the data easily.
I solved it by adding $jwt_user to User construct to skip 'fillable':
auth.php
'defaults' => [
'guard' => 'api',
],
'guards' => [
'api' => [
'driver' => 'jwt',
],
],
AuthServiceProvider.php
use App\User;
use \Firebase\JWT\JWT;
public function boot()
{
$this->registerPolicies();
Auth::viaRequest('jwt', function ($request) {
$publicKey = file_get_contents(storage_path('oauth-public.key'));
if(!$hasAuthHeader = $request->header('Authorization')?true:false){
return null;
}
preg_match('/Bearer\s((.*)\.(.*)\.(.*))/', $request->header('Authorization'), $jwt);
try {
$res = JWT::decode($jwt[1], $publicKey, array('RS256'));
$jwt_user = json_decode(json_encode($res->user), true);
$local_user = User::find($jwt_user['id']);
$jwt_user['local_profile'] = $local_user?$local_user:[];
$user = new User([], $jwt_user);
return $user;
} catch (\Exception $e) {
return null;
}
});
}
User.php
public function __construct(array $attributes = array(), $jwt_user = array())
{
parent::__construct($attributes);
foreach($jwt_user as $k=>$v){
$this->$k = $v;
}
}
An easy way to achieve this is:
use Firebase\JWT\JWT;
use Laravel\Passport\Token;
$jwt = 'eyJ0...';
$publicKey = file_get_contents(storage_path('oauth-public.key'));
$res = JWT::decode($jwtToken, $publicKey, ['RS256']);
$user = Token::findOrFail($res->jti)->user;
I have implemented jasig/phpCas authentication in My Silex App.
It is almost done, but I can't Handle authfailure Response correclty.
$app['app.token_authenticator'] = function ($app) {
return new MyApp\Domain\MyTokenAuthenticator($app['security.encoder_factory'],$app['cas'],$app['dao.usersso']);
};
$app['security.firewalls'] = array(
'default' => array(
'pattern' => '^/.*$',
'anonymous' => true,
'guard' => array(
'authenticators' => array(
'app.token_authenticator'
),
),
'logout' => array ( 'logout_path' => '/logout', 'target_url' => '/goodbye' ),
'form' => array('login_path' =>'/login', 'check_path' =>'/admin/login_check', 'authenticator' => 'time_authenticator' ),
'users' => function () use ($app) {
return new MyApp\DAO\UserDAO($app['db']);
},
),
);
MyTokenAuthenticator class :
class MyTokenAuthenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
private $cas_settings;
private $sso_dao;
public function __construct(EncoderFactoryInterface $encoderFactory, $cas_settings, MyApp\DAO\UserSsoDAO $userdao)
{
$this->encoderFactory = $encoderFactory;
$this->cas_settings = $cas_settings;
$this->sso_dao = $userdao;
}
public function getCredentials(Request $request)
{
$bSSO = false;
//Test request for sso
if ( strpos($request->get("ticket"),"cas-intra") !==false )
$bSSO = true;
if($request->get("sso") == "1")
$bSSO=true;
if ($bSSO)
{
if ($this->cas_settings['debug'])
{
\CAS_phpCAS::setDebug();
\CAS_phpCAS::setVerbose(true);
}
\CAS_phpCAS::client(CAS_VERSION_2_0,
$this->cas_settings['server'],
$this->cas_settings['port'],
$this->cas_settings['context'],
false);
\CAS_phpCAS::setCasServerCACert('../app/config/cas.pem');
// force CAS authentication
\CAS_phpCAS::forceAuthentication();
$username = \CAS_phpCAS::getUser();
return array (
'username' => $username,
'secret' => 'SSO'
);
}
//Nothing to do, skip custom auth
return;
}
/**
* Get User from the SSO database.
* Add it into the MyApp users database (Update if already exists)
* {#inheritDoc}
* #see \Symfony\Component\Security\Guard\GuardAuthenticatorInterface::getUser()
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
//Get user stuf
....
//return $userProvider->loadUserByUsername($credentials['username']);
return $user;
}
/**
*
* {#inheritDoc}
* #see \Symfony\Component\Security\Guard\GuardAuthenticatorInterface::checkCredentials()
*/
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// return true to cause authentication success
if ( $this->sso_dao->isBAllowed($user->getLogin() ) )
return true;
else
throw new CustomUserMessageAuthenticationException("Sorry, you're not alllowed tu use this app.");
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data,403);
}
Issue is when a valid user from SSO is denied in app. It displays
a page with json Message, without any rendering.
My workaround is to use minimal html page with sso logout link as response and session_destroy(), but its quick and dirty fix.
I'd like a redenring via twig with a nice error message. Maybe some other class to extend ? Silex's Documentation was no help. Thank you !
Back to this question as I was on others apsects of the dev.
#mTorres solution is working. I had to store whole app object via constructor as twig is not set at this time in service registry.
class MyTokenAuthenticator extends AbstractGuardAuthenticator
{
private $app;
public function __construct($app)
{
$this->app=$app;
}
then custom event
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new \Symfony\Component\HttpFoundation\Response(
$this->app['twig']->render( 'logout.html.twig',array(
'error' => $data,
));
}
Many thanks !
Hi can someone help me to prevent bjyauthorize to catch my api event error raised?
bjyauthorize redirect non logged user to login form as added to config. But since my api are allowed for all roles even for guest i just want it to return Json error message catched by ApiProblemListener
ApplicationRest\Module.php
class Module implements
ConfigProviderInterface,
AutoloaderProviderInterface
{
public function onBootstrap(MvcEvent $e)
{
$app = $e->getApplication();
$sm = $app->getServiceManager();
$events = $app->getEventManager();
$listener = $sm->get('ApplicationRest\ApiAuthenticationListener');
$events->getSharedManager()->attach('ApplicationRest\Controller', 'dispatch', $listener, 500);
$events->attach('render', array($this, 'onRender'), 100);
$events->attach($sm->get('ApplicationRest\ApiProblemListener'));
}
/**
* Listener for the render event
* Attaches a rendering/response strategy to the View.
*
* #param \Zend\Mvc\MvcEvent $e
*/
public function onRender($e)
{
$result = $e->getResult();
if (!$result instanceof RestfulJsonModel) {
return;
}
//var_dump(123);exit();
$app = $e->getTarget();
$services = $app->getServiceManager();
$view = $services->get('View');
$restfulJsonStrategy = $services->get('ApplicationRest\RestfulJsonStrategy');
$events = $view->getEventManager();
// register at high priority, to "beat" normal json strategy registered
// via view manager
$events->attach($restfulJsonStrategy, 500);
}
}
Have many modules and i am really thinking to move away my apiModule "ApplicationRest" to another project but don't really want to update model and service each time i make some updates on main project.
Any suggestions would welcome!
Thanks for your time!
EDIT: Provided more HeaderAuthentication class
class HeaderAuthentication implements AdapterInterface
{
const AUTHORIZATION_HEADER = 'Authorization';
const CRYPTO = 'sha256';
protected $request;
protected $repository;
public function __construct(RequestInterface $request, UserRepository $repository)
{
$this->request = $request;
$this->repository = $repository;
}
/**
* Authorization: Key={key} Timestamp={timestamp} Signature={signature}
* #return Result
*/
public function authenticate()
{
$request = $this->getRequest();
if (!$request instanceof Request) {
return;
}
$headers = $request->getHeaders();
// Check Authorization header presence
if (!$headers->has(static::AUTHORIZATION_HEADER)) {
return new Result(Result::FAILURE, null, array(
'Authorization header missing'
));
}
$authorization = $headers->get(static::AUTHORIZATION_HEADER)->getFieldValue();
// Validate public key
$publicKey = $this->extractPublicKey($authorization);
$user = $this->getUserRepository()
->findOneByApiSecret($publicKey);
if (null === $user) {
$code = Result::FAILURE_IDENTITY_NOT_FOUND;
return new Result($code, null, array(
'User not found based on public key'
));
}
// Validate signature
$signature = $this->extractSignature($authorization);
/*$hmac = $this->getHmac($request, $user);
if ($signature !== $hmac) {
$code = Result::FAILURE_CREDENTIAL_INVALID;
return new Result($code, null, array(
'Signature does not match'
));
}*/
return new Result(Result::SUCCESS, $user);
}
}
ApiAuthenticationListener
class ApiAuthenticationListener
{
protected $adapter;
public function __construct(HeaderAuthentication $adapter)
{
$this->adapter = $adapter;
}
public function __invoke(MvcEvent $event)
{
$result = $this->adapter->authenticate();
if (!$result->isValid()) {
$response = $event->getResponse();
// Set some response content
$response->setStatusCode(401);
return $response;
}
// All is OK
$event->setParam('user', $result->getIdentity());
}
}
I'm guessing you configured guards on your route. You need to tell BJYAuthorize, through your module config, that this controller or route shouldn't be protected.
'bjyauthorize' => [
'default_role' => 'guest',
...
'guards' => [
'BjyAuthorize\Guard\Controller' => [
// system tools
['controller' => 'Application\Controller\Api', 'roles' => [] ],
['controller' => 'error', 'roles' => []],
],
],
],
I cut out the nitty gritty that's app specific, but this type of thing is quickly solved. I had a similar need for CLI routes to be unprotected by what is otherwise, http auth.