Socialite stateless auth - php

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
]);
}

Related

Yii2 JWT 401 Unauthorized

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.

jwt-auth after upgrade - get user from request token

I upgraded:
"tymon/jwt-auth": "0.5.*",
from a very old version, and it seems like the API has changed. I managed to fix the login, using:
public function login(Request $request)
{
$credentials = $request->only(['username', 'password']);
$validator = Validator::make($credentials, [
'username' => 'required',
'password' => 'required',
]);
if($validator->fails()) {
throw new ValidationHttpException($validator->errors()->all());
}
if (!$token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$user = auth()->user();
$user->ip_address = $request->ip();
if ($request->device_token)
$user->device_token = $request->device_token;
$user->save();
$data['token'] = $token;
$data['email'] = $user->email;
return response()->json($data);
}
So my login work, but all API's that required the token - fail now.
Example of API that fail:
class UserController extends Controller
{
public function __construct()
{
// $this->middleware('auth:api', ['except' => ['login']]);
}
public function enterWorld(Request $request)
{
$token = $request->input('token');
$user = JWTAuth::toUser($token);
return $user;
}
Any idea how to convert the token from the request to the user with the new API?
I couldn't find any docs about it.
I tried:
return response()->json(auth()->user());
but in this API it return empty array. only in login it works.
Try the following:
$user = JWTAuth::setRequest($request)->user();
You may also explicitly set the guard when using the following syntax:
// pass the guard in to the auth() helper.
return response()->json(auth('jwt')->user());

Decode JWT and create authenticated User in Laravel microservice, merging with local user data

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;

Laravel Socialite Remember Me

I am using Socialite for user logins and I would like to set a remember_token to remember the user when they login through Socialite.
Right now I have the following service to create or log the user in:
class SocialAccountService {
public function createOrGetUser(ProviderUser $providerUser) {
$account = SocialAccount::whereProvider('google')
->whereProviderUserId($providerUser->getId())
->first();
if ($account) {
return $account->user;
} else {
$account = new SocialAccount([
'provider_user_id' => $providerUser->getId(),
'provider' => 'google'
]);
$user = User::whereEmail($providerUser->getEmail())->first();
if (!$user) {
$user = User::create([
'email' => $providerUser->getEmail(),
'name' => $providerUser->getName()
]);
}
$account->user()->associate($user);
$account->save();
return $user;
}
}
}
It is called with the following controller:
class AuthController extends Controller {
public function logout() {
Auth::logout();
return redirect('/');
}
public function redirectToGoogle() {
return Socialite::driver('google')->redirect();
}
public function handleGoogleCallback(SocialAccountService $service) {
$user = $service->createOrGetUser(Socialite::driver('google')->user());
auth()->login($user);
return redirect('/');
}
}
The issue is that when the user comes back they are not remembered and automatically logged in. How can I do this with Socialite?
According to the documentation, passing true as the second argument of login() will set the remember token.
// Login and "remember" the given user... Auth::login($user, true);
The Auth facade and auth() helper function access the same object.

zf2 api response event catched by bjyauthorize

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.

Categories