I'm writing an API in Slim 3 that integrates with a legacy system. The client sends a token to my API in the querystring. I want to write a middleware that will authenticate the token and return an object that contains the necessary internal login credentials (which are different from the token) that are used by the legacy system.
I can authenticate the token now, but my problem is that Slim 3 requires that the middleware return a \Psr\Http\Message\ResponseInterface instance. I also want it to return a custom object back to the application.
I think I can achieve this by re-verifying the token outside of the middleware, and only use the middleware as a way to authenticate the token and return an error if it fails. I tend to think this kludgy way could be avoided so I just have to use the token once in the middleware and return the custom object at the same time so I don't have to use the token twice.
I have searched around for solutions, but all of the example middlewares I can find are similar to https://github.com/julionc/slim-basic-auth-middleware, where they are simply authenticating in the middleware but do not have the requirement to return a custom object. The documentation at http://www.slimframework.com/docs/concepts/middleware.html doesn't seem to help much either with this custom requirement.
Any ideas?
You could include a callback in your middleware which you can use to store the custom object somewhere. For example with slim-jwt-auth you can use callback to store the decoded contents of JWT using a callback.
$app->add(new \Slim\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub",
"callback" => function ($request, $response, $arguments) use ($app) {
$app->jwt = $arguments["decoded"];
}
]));
Note that this kind of callback is not a Slim 3 feature. Just something this middleware happens to use.
Related
TLDR; see image below 3 - is that possible and how?
I read about API protection - Sanctum & Passport, but none of these seems what I can accomplish with my app since it's a little specific and simplified in a way.
For example, Sanctum's way of authenticating sounds like something I'd like, but without the /login part (i have a custom /auth part, see below.): https://laravel.com/docs/8.x/sanctum#spa-authenticating.
If the login request is successful, you will be authenticated and
subsequent requests to your API routes will automatically be
authenticated via the session cookie that the Laravel backend issued
to your client.
My app has no login per se - we log-in the user if they have a specified cookie token verified by the 3rd party API (i know token-auth is not the best way to go, but it is quite a specific application/use). It's on /auth, so Sanctum's description above could work, I guess if I knew where to fiddle with it. Our logic:
VueJS: a mobile device sends an encrypted cookie token - app reads it in JS, sends it to my Laravel API for verification.
Get the token in Laravel API, decrypt, send to 2nd API (not in my control), verifying the token, and sends back an OK or NOT OK response with some data.
If the response was OK, the user is "logged-in."
The user can navigate the app, and additional API responses occur - how do I verify it's him and not an imposter or some1 accessing the API directly in the browser?
I guess the session could work for that, but it's my 1st time using Laravel, and nothing seemed to work as expected. Also, sessions stored in files or DB are not something I'm looking forward to if required.
For example, I tried setting a simple session parameter when step 3 above happened and sending it back, but the session store was not set up, yet it seemed at that point. Then I could check that session value to make sure he's the same user that was just verified.
For an easier understanding of what I'm trying to accomplish and if it's even feasible:
The main question is, what is the easiest way to have basic API protection/authentication/verification whilst sending the token for authentication to 3rd party API only on 1st request (and if the app is reopened/refreshed of course) - keeping in mind, that no actual users exist on my Laravel API.
Or would it be best to do the token-auth to the 3rd party API on each request?
If I understand your case correctly there's no real User model involved, right? If so, you'll not be able to use any of Laravel's built-in authentication methods as they all rely on the existence of such a model.
In that case you'll need one endpoint and a custom authentication Middleware that you'll need to create yourself in Laravel in order to handle everything:
The endpoint definition:
Route::post('authenticate', [TokenController::class, 'login']);
The controller:
class TokenController extends Controller
{
public function login(Request $request)
{
// First read the token and decrypt it.
// Here you'll need to replace "some_decryption()" with the required decrypter based on how your VueJS app encrypts the token.
$token = some_decryption( $request->input('token') );
// Then make the request to the verification API, for example using Guzzle.
$isTokenOk = Http::post('http://your-endpoint.net', [
'token' => $token,
])->successful();
// Now issue a Laravel API token only if the verification succeeded.
if (! $isTokenOk) {
abort(400, 'Verification failed');
}
// In order to not store any token in a database, I've chosen something arbitrary and reversibly encrypted.
return response()->json([
'api-token' => Crypt::encrypt('authenticated'),
]);
}
}
Subsequent requests should pass the api token in the Authorization header as a Bearer token. And then in the Middleware you'll check for Bearer token and check if it matches our expected value:
class AuthTokenAuthenticationMiddleware
{
public function handle($request, Closure $next)
{
$authToken = $request->bearerToken();
if (! $authToken || ! Crypt::decrypt($authToken) === 'authenticated') {
abort(401, 'Unauthenticated');
}
return $next($request);
}
}
The Middleware needs to be registered in app/Http/Kernel.php:
protected $routeMiddleware = [
...
'auth-token' => AuthTokenAuthenticationMiddleware::class,
];
And finally apply this new middleware to any of your routes that should be authenticated:
Route::middleware('auth-token')->get('some/api/route', SomeController::class);
Warning: this authentication mechanism relies on reversible encryption. Anyone able to decrypt or in possession of your APP_KEY will ultimately be able to access your protected endpoints!
Of course this is one way to deal with custom userless authentication and there are many more. You could for example insert an expiration date in the encrypted token instead of the string "authenticated" and verify if it's expired in the middleware. But you get the gist of the steps to be followed...
If you do have a User model in place, then you could use Laravel Sanctum and issue an API token after User retrieval instead of forging a custom encrypted token. See https://laravel.com/docs/8.x/sanctum#issuing-mobile-api-tokens
// Fetch the corresponding user...
$user = User::where('token', $request->input('token'))->first();
return $user->createToken('vuejs_app')->plainTextToken;
Subsequent requests should pass the token in the Authorization header as a Bearer token.
Protect routes using the middleware provided by Sanctum:
Route::middleware('auth:sanctum')->get('some/api/route', SomeController::class);
I see quite a few people having a similar issue with this, but no final resolved solutions. I have been trying to get this working for about 24 hours now and still no luck!
Goals
Build and API using Laravel 6 and Dingo API
Be able to consume the API externally, authenticating with Passport oAuth.
Be able to consume the API internally, via ajax, using passports self-authenticating feature.
Be able to consume the API internally, with PHP, using dingo's self-consuming methods.
What I have found out so far
Auth provider order
Most solutions I have seen suggest setting up both the passport auth and dingo alongside one another. This is auth:api (passport) and api.auth (dingo).
// API route middleware
$api->group(['middleware' => 'auth:api', 'api.auth'], function (Router $api) {
...
The api.auth here is actually a custom auth provider setup in laravel and configured to dingo, which bridges the passport logic into dingo.
// Auth provider
class DingoPassportAuthProvider extends Authorization
{
protected $guard;
public function __construct(AuthManager $auth)
{
dump('DingoPassportAuthProvider Instantiated');
$this->guard = $auth->guard('api');
}
public function authenticate(Request $request, Route $route)
{
if ($this->guard->check()) {
return $this->guard->user();
}
throw new UnauthorizedHttpException('Not authenticated via Passport.');
}
public function getAuthorizationMethod()
{
return 'Bearer';
}
}
// Configured in dingo (Api.php)
'auth' => [
'passport' => \App\Providers\DingoPassportAuthProvider::class,
],
If we put the dingo API provider first in the middleware stack we get:
Internal API requests work IF you specify the user for the call with the be() method: $this->api->be($request->user())->get('/api/profile')
External API requests and internal AJAX requests authenticate correctly and the user is returned from the custom dingo auth provider, however, for some reason you cannot then access this user from within the API controllers: $user = $request->user(); // null
If we put the Passport API provider first in the middleware stack we get:
Internal API requests do not work at all (401 always returned)
External API requests and internal AJAX requests work as intended.
The authenticate method on the dingo passport provider is no longer called. I think this may have something to do with the 401 returned on internal calls.
I believe the correct way around, is to put the passport authentication first. This way, we authenticate the user before calling the dingo authentication, resulting in 2 things:
Passport works natively as expected.
Dingo internal API calls should now just be able to be called with $this->api->get('/api/profile') (omit defining the user with be()), however this does not work.
At the moment I have the previous configuration. Passport works as intended for external and ajax calls, but the internal dingo calls always return 401.
There are a few boilerplate templates I have checked out and they do not seem to do anything different. I wonder if something changed in L6 to explain why the internal requests do not work.
I have found one work around for now, which gets most of the way there...
Within the custom dingo auth provider:
class DingoPassportAuthProvider extends Authorization
{
public function authenticate(Request $request, Route $route)
{
if (Auth::guard('web')->check()) {
return Auth::guard('web')->user();
}
if (Auth::guard('api')->check()) {
$user = Auth::guard('api')->user();
Passport::actingAs($user);
return $user;
}
throw new UnauthorizedHttpException('Not authenticated via Passport.');
}
public function getAuthorizationMethod()
{
return 'Bearer';
}
}
This now checks to see if the request is coming from either the web guard (internal request) or the api guard (external or ajax request) and returns the correct user.
For the api guard, there seems to be an issue that the user is authenticated but not actually available within the controllers. To get around this I added the Passport::actingAs($user). It is probably not best practice, but the guards are now acting as they should and as are all my different scenarios.
Then in the API route middleware, we only specify the custom dingo provider.
// API route middleware
$api->group(['middleware' => 'api.auth'], function (Router $api) {
...
One thing to note with this, is dingos be() method does not work quite as expected. Instead you need to switch the user as you would in a general laravel app.
\Auth::loginUsingId(2);
$user = $this->api->get('/api/profile');
I have a simple case and I need your advice. I am using tymon jwt package. I have JWT middleware and this is the part of it's code:
$user = JWTAuth::parseToken()->authenticate();
if(!$user){
return response()->json(['message'=>trans("responseMessages.user_not_exists")], 403);
}
$request->request->set('user', $user);
what this middleware does, is that it tries to create $user from given jwt token, if it succeeds, user is good to continue. so here is my question, in this code (final line) I pass user object to controller through request, so I can directly have access to user model in controller. I am just interested, is this a good idea? or maybe this will be problematic?
other option is to write $user = JWTAuth::toUser(JWTAuth::getToken()) in controller function or pass user id through request instead of whole model. but in these cases I communicate with database twice, in middleware and in controller in order to get user object.
also I tried to do something like that in controller constructor : $this->user = JWTAuth::toUser(JWTAuth::getToken()), but controller constructor executes before middleware so this one was problematic. so provide me with your ideas and advices if passing user model is good idea or not.
This is an opinionated question, so don't take my answer as your final solution.
I use Slim and made an authenticate-middleware that adds the user object to the request attributes. This is essentially what you are doing.
Keep in mind the folllowing problems though (at least with immutables Request/Response objects like with PSR7):
when you have middlewares BEFORE your authentication middleware (like catching Exceptions), the request does NOT have the user object, because the middlewares work in layers.
Vice versa: if you have middleware that first executes all other middleware and than executes itself
It's just pseudo-code, but you get the idea.
middlewarefunction($request, $response, $nextmiddleware)
{
$nextmiddleware->do($request, $response);
// from here on the $request has nothing set to the request by the $nextMiddleware
// because it is immutable
}
// Edit
If you look at other middlewares, they are setting the request attribute with the decoded JWT token too:
https://github.com/DASPRiD/Helios/blob/master/src/IdentityMiddleware.php
https://github.com/tuupola/slim-jwt-auth/blob/3.x/src/JwtAuthentication.php
I am building a REST user-microservice using Laravel 5.5 + Passport.
I am using the standard Passport::routes(), but I have had to modify the Auth::routes in order to make them return JSON responses, and to make them work with Passport.
I have added the following lines to my routes/web.php file:
Route::group(['middleware' => 'auth:api'], function () {
$this->post('logout', 'Auth\LoginController#logout')->name('logout');
});
This allows me to POST https://myapi/logout
If I make the call with the header "Authorization => Bearer TOKEN", I get a successful logout response.
If I provide no header at all, I get a "not authenticated" message (which is good)
However, if I provide the header with a revoked token, I get a recursive deadloop of the function: Illuminate\Auth\RequestGuard->user() (it keeps calling itself recursively until stack-overflow)
This is all done in the auth:api middleware, my logout code is not reached, but my LoginController constructor is called. Constructor code:
public function __construct(Application $app)
{
$this->apiConsumer = $app->make('apiconsumer');
$this->middleware('guest')
->except('logout');
}
I'm struggling to understand if it's my code causing this issue, or some combination of Laravel + passport + auth.
My first thought was that the auth:api middleware fails to authenticate the user, and as a result redirects the user to /home, where for some reason it's triggered again, recursively. But if that was the case, why would it work correctly with no header?
My current thinking is that the token in question does exist in the database, but Laravel is failing to figure out that it's revoked.
Any suggestions appreciated,
I found an answer (if not the answer) after a lot of research. It appears this is a Laravel bug (https://github.com/laravel/passport/issues/440). The solution is to add OAuthServerException to the $dontReport array in app/Exceptions/Handler.php:
class Handler extends ExceptionHandler
{
protected $dontReport = [
...
\League\OAuth2\Server\Exception\OAuthServerException::class,
];
}
This will avoid trying to log user information, thereby avoid the deadloop.
I have faced this in localhost. in my case, I have used xampp server and facing this issue
after creating a virtual host like "testlarave.test" then solve the error
is theoretically possible use more services for routing?
For example if somebody use Silex and has this code:
$app = new Silex\Application();
$app->get('/test/{id}', function ($id) {
// ...
});
$app->run();
And i create api using Slim like that:
$app = new \Slim\Slim();
$app->get('/api/' . $version . 'something', function () use ($app){
$data = $app->request->params();
});
$app->run();
How user could implement my API withou rewrite Slim route function to Silex route function?
Thank you very much.
Giving a quick though, the way I see it you have 3 options:
Refactor controller closures to a named function
Both, Silex and Slim[1] can use any form of callable, so instead of passing a closure just pass a function name (or an array with class name, method name) or any other callable. This way with 1 declaration you'll be able to call it from both Slim and Silex (yes, you have to define the routes on both sides).
This has its drawbacks as the controller signature is different for the 2 frameworks, so you'll need to hook into silex flow and change the parameters (you have the Kernel.controller event to do that).
Also you'll need to redefine all your services and use the container wisely (i.e, don't use it as a service locator in your controllers).
This is probably the most inflexible way.
Define the routes in Silex and instatiate a Slim APP inside to call and return that
You'll need to define the api routes again in Silex and in each route you can try to instatiate the Slim APP (using something as require "path/to/the/slim/app/php/file.php") and then force the run method with the silent option on[2]
You'll probably need to refactor your Slim application a bit, you'll have to define your app in a file and call the run method in another.
Create a Silex middleware to handle all the /api/ calls
The easiest (?) way that I can think of is redefine the second option and create a catch all incoming request to the /api/ mount point by creating a Silex middleware that checks for each incoming request if the request has the /api/ string inside the request. Then you'll simply forawrd the request to the Slim application as I've told you in the second option.
Using this method you don't need to redefine the routes in Silex as everything under the /api/ point will be forwarded to the Slim application.
NOTE
In all cases you'll probably need to transform the response given from Slim to a Symfony response. Slim 3 uses the new PSR-7 HTTP interface so you have a solution here already. Slim 2 echos the response directly so you'll need to catch that and put it inside a Symfony response (new Response(ob_get_clean())
[1] This is for the Slim3, Slim2 may also be able to do that but I'm not sure (and the signature is different!)
[2] Again this is for Slim3, Slim2 does not has this option, so you'll need to figure something to get the response (maybe ob_get_clean()?)