Creating a proxy in Laravel with Guzzle - php

I need to use an api in my website frontend, but I don't want to expose that api's key to my frontend users, so I've decided to make a proxy. However, I don't think I've necessarily done it in the most clean, straight-forward, Laravel-like or Guzzle-like way. I'll show my work:
In web.php I added a route that looks like this: Route::post('/address-api/{path?}', 'Controller#addressApi')->where('path', '.*'); That way, the entire path after /address-api is passed to my controller, so I can proxy hypothetically ANY post request to that api.
Then in Controller.php I've done this:
public function addressApi($path, Request $request)
{
if (!Str::startsWith($path, '/')) $path = '/' . $path; // make sure it starts with /
$url = 'https://api.craftyclicks.co.uk/address/1.1' . $path;
$postData = $request->all();
$postData['key'] = env('CRAFTYCLICKS_KEY');
$client = new Client();
$response = $client->request('POST', $url, [
'json' => $postData
]);
return response()->json(json_decode($response->getBody(), true));
}
So, whatever json they post to my api, I post to the CraftyClicks api, but I add our secret key to the json. The code above is working, it just doesn't seem like the right way to do it.
The thing I'm not sure about is json_decoding the body and returning it, return response()->json(json_decode($response->getBody(), true));. I feel like there's something... dirty about this. I feel like there must be a cleaner way to return the actual API response exactly as it came in.
At first I was doing return $response->getBody();, but I didn't like that because it didn't have the Content-type: application/json header in the response when I did it that way. Does Guzzle provide, out of the box, a way of just returning their response entirely as-is, headers and all?

Let Laravel have the output; this is cleaner.
return response($response->getBody())
->withHeaders($response->getHeaders());

Related

I'm transitioning my API from slim-3 to slim-4 and I'm struggling to figure out how to add JWT to the middleware

I'm using composer to install the slim-skeleton. Those built in routes work as expected. I understand how to add in my previous routes and database connections, but I've been struggling on how to add in any JWT library. I've searched and searched but I'm not finding much documentation for Slim-4 and what I've tried always seems to fail one way or another.
So for example I use composer to install tuupola/slim-jwt-auth and it says to add the following code:
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
but where or how exactly do I add it to the middleware? Does it need to be added to app/middleware.php? All the documentation I read has a completely different file structure with other directories and whatnot. Once this is placed in the correct spot it looks like when a request is made without a token I should get a 401 Unauthorized response.
After that part is working I know I need to create a route to get my access token, but I'm not seeing anything about that in this library so I would assume I need another library to encode my token and return it from my request.
Once I actually get a token response and pass it in the headers for my actual request route I would assume I do something like the following
$app->get("/protected-route-name", function ($request, $response, $arguments) {
$token = $request->getAttribute("token");
// Not sure what to put next to verify the token and allow the response or display a error if there is no token or the token in invalid.
});
I'm open to firebase or any JWT library if someone has one they like and that works well, I just need some direction as I feel all the documentation is lacking.
use \Firebase\JWT\JWT;
get token
$headers = apache_request_headers();
if(isset($headers['Authorization'])){
$decoded = JWT::decode($headers['Authorization'], $publicKey, array("RS256"));
.... verify token.
}
$jwt = JWT::encode($payload, $privateKey, "RS256");
boom done.
you don't even really need to use middle ware to do this.
slim made itself way overly complex with that.
But the truth is between slim3 and slim 4, on a very basic setup, the only thing that has changed is the getBody() on the json writing.
honestly, not really sure how useful this is anymore to be honest. everything is cloudbased now. Only reason I found this is trying to figure out how to use Google Identity Platform with Slim.

How to verify the origin domain of a request in PHP (Laravel)

I am working on a Laravel app where I am building some API for other websites. But I am trying to make the implementation of my API as easy as possible. My expectation is that the user will only use this tag in the HTML head:
<script src="api.mydomain.com">
Now I have a controller on this URL that provides the source javascript with the content-type header, but before it goes there, the router will first execute my authentication middleware. Let's say it looks something like this:
public static $users = [
'client1.com',
'client2.com',
'client3.com'
];
public function handle(Request $request, Closure $next)
{
$origin = "HERE I NEED THE ORIGIN URL"; // e.g. client4.com
if ( !in_array($origin, self::$users) ) {
abort(401);
}
return $next($request);
}
As you can see from the code, I need to retrieve the $origin variable. So if a website client1.com will try to insert my javascript, it will successfully get the javascript code. If client4.com tries to access it, it will get a 401 error.
I found out methods with $_SERVER['HTTP_REFERER'] or Laravel's $request->server('HTTP_REFERER'), but this data might be spoofed, right?
In the best-case scenario, I would like to retrieve the original domain and when not available (e.g. from a private cURL request), I would like to get the IP address. And of course, I need it to be secure - clients1/2/3 paid for my API, others didn't.
How can I do it? Or is there any better method for origin authentication?
All that referer stuff can be spoofed.
Best way for paid API is to issue API calling key.
You API can display results or error depending if the client has proper API key and is Paid for.
You should also keep logs table for API calls with timestamp and clientID and IP addresses. So from time to time you can check if one of your paid client is sharing his key with others etc from call frequency and IP patterns.
Clean up this logs table from time to time to keep it small and efficient.
So I figured it out by adding headers (thanks for inspiration #jewishmoses) in the middleware handler. My Javascript is available basically to everyone, but it provides only a button, that tries to create a new element with an iframe inside (my app which also works as an API).
Let's say I have an associative array on the server, that I can dynamically fill from any database:
$clients = [
'client1' => 'paying-customer.com',
'client2' => 'also-paying-customer.com',
];
...my route for API is defined as 'api.mydomain.com/{client}' and 'api.mydomain.com/{client}/iframe' for iframed app. This handler takes care of adding headers:
public function handle(Request $request, Closure $next)
{
$client = $request->route('client',null);
$clientSet = $client !== null;
$clientAccepted = isset($clients[$client]);
if ( $clientSet and !$clientAccepted ) {
abort(401);
}
$response = $next($request);
if( $clientSet and isset($response->headers) and $response->headers instanceof ResponseHeaderBag){
$clientDomain = $clients[$client];
$response->headers->add([
'Content-Security-Policy' => "frame-ancestors https://*.$clientDomain/ https://$clientDomain/"
]);
}
return $response;
}
Now what might happen:
client1 successfully imports javascript from api.mydomain.com/client1, which will try to access api.mydomain.com/client1/iframe (also successfully)
client3 unsuccessfully tries to import javascript from api.mydomain.com/client3
client3 successfully imports javascript from api.mydomain.com/client1, which will try to access api.mydomain.com/client1/iframe (refused by headers)
Maybe there is a more elegant way to block loading the javascript, but providing my own app as API (in an iframe) is in my opinion secured enough because I can control who can use it and modern browsers will help me to stop the "thieves". This resource was most helpful to solve my problem.

Functional test in Laravel with External URL ( Google Login)

I'm trying to test the Google Login in my Laravel App.
When I try to reach Google Login page I get a 404.
My idea is that in TestCase.php, I have a variable:
protected $baseUrl = 'http://laravel.dev';
So, it could make a conflict.... well the thing is I don't know how to do it, or to fix it!
Here is my code:
$this->visit('/auth/login')
->click('google');
dump(Request::url()); // https://accounts.google.com/o/oauth2/auth
$this->dump(); --> Gives me a 404 page
Any idea is welcome !
The answer is NO. The framework doesn't support it. If you dig a little bit you will found when you test with "visit", the request was not actually send out. In the call method of class MakesHttpRequests, it initialize a laravel http kernel and put the request thru it. No http request was actually issued.
$kernel = $this->app->make('Illuminate\Contracts\Http\Kernel');
$this->currentUri = $this->prepareUrlForRequest($uri);
$this->resetPageContext();
$request = Request::create(
$this->currentUri, $method, $parameters,
$cookies, $files, array_replace($this->serverVariables, $server), $content
);
$response = $kernel->handle($request);
The only way you can try is to mock this part.

How Follow the Don't Repeat Yourself Principle When Consuming My Own Laravel API?

I'm developing a Laravel 4 app that will make the same CRUD operations on my data set available through a JSON REST API and a Web UI. It seems that to prevent breaking the DRY principle that my UI should consume my own API by routing all requests from the UI back to the API. I'm unsure though about the best approach to making this work. Presumably I would have separate UI and API controllers and somehow route the requests through. Or should I be looking at a different approach altogether?
I'm actually tinkering with the same idea and it's pretty neat. With Laravel you do have the ability to make internal requests (some might refer to this as HMVC, but I won't). Here's the basics of an internal request.
$request = Request::create('/api/users/1', 'GET');
$response = Route::dispatch($request);
$response will now contain the returned response of the API. Typically this will be returned a JSON encoded string which is great for clients, but not that great for an internal API request. You'll have to extend a few things here but basically the idea is to return the actual object back through for the internal call, and for external requests return the formatted JSON response. You can make use of things like $response->getOriginalContent() here for this kind of thing.
What you should look at doing is constructing some sort of internal Dispatcher that allows you to dispatch API requests and return the original object. The dispatcher should also handle malformed requests or bad responses and throw exceptions to match.
The idea itself is solid. But planning an API is hard work. I'd recommend you write up a good list of all your expected endpoints and draft a couple of API versions then select the best one.
NOTE: As vcardillo pointed out below, route filters are not called with these methods.
I am currently doing the same thing, and Jason's answer got me going in a great direction. Looking at the Symfony\Component\HttpFoundation\Request documentation, I figured out how to POST, as well as everything else I'd need to do. Assuming you're using a form, here is some code that could help you:
GET:
$request = Request::create('/api/users/1', 'GET');
$response = Route::dispatch($request);
POST:
$request = Request::create('/api/users/1', 'POST', Input::get());
$response = Route::dispatch($request);
POST w/ cookies
$request = Request::create('/api/users/1', 'POST', Input::get(), Cookie::get('name'));
$response = Route::dispatch($request);
POST w/ files
$request = Request::create('/api/users/1', 'POST', Input::get(), null, Input::file('file'));
$response = Route::dispatch($request);
I hope this helps someone else. If you aren't using a form, or you are but not using Laravel's Input / Cookie facade, replace the Input / Cookie facades with your own content.
Taylor Otwell suggested using app()->handle() rather than Route::dispatch() to achieve a clean request.
For Route::dispatch($request) I noticed if the endpoint of your non-GET request (parameters on the HTTP request body) uses a dependency injected \Illuminate\Http\Request or \Illuminate\Foundation\Http\FormRequest extending instance, state of the parameters, cookies, files, etc. are from the original HTTP request. i.e., for your application's controller action method.
If parameter names and post method type for your app controller and API controller are the same, you won't notice the difference since the original parameter values are passed on. But when you're manually assembling the 3rd parameter of Request::create(), Route::dispatch() will result in it being ignored.
app()->handle() fixes that context problem in the Laravel request lifecycle.
Caveat: app()->handle() affects Illuminate\Support\Facades\Request, refreshing it with this new request instance. As a knock-on effect, calls like Request::isXmlHttpRequest() or redirect()->back() invoked after app()->handle() will cause unpredictable behaviour. I'd suggest tracking the context of your original request and instead use redirect()->to(route('...')) so you strictly control flow and state of your app.
Given all these corner cases, it may be best to just do a manual curl using a Guzzle HTTP client.
If you are looking for using passport login api internally, then you need to add the parameters to original request:
protected function manualLogin(Request $request)
{
$email = $request->input('email');
$password = $request->input('password');
$request->request->add([
'username' => $email,
'password' => $password,
'grant_type' => 'password',
'client_id' => $clientID,
'client_secret' => $clientSecret,
'scope' => '*']);
$newRequest = Request::create('/oauth/token', 'post');
return Route::dispatch($newRequest)->getContent();
}
If you're consuming your own API, use app()->handle() instead of Route::dispatch() as Derek MacDonald has suggested.
app()->handle() creates a fresh request, while Route::dispatch() runs the route within the stack, effectively ignoring parameters that are part of the request that you're sending.
Edit: Just a heads-up. Taylor Otwell advises against using sub-requests to make internal API calls, as they mess the current route. You can an HTTP API client like Guzzle instead to make the API calls.
You can use Optimus API consumer, the API is clean and simple, example making an internal request:
$response = app()->make('apiconsumer')->post('/oauth/token', $data);
In it's core, it uses Illuminate\Routing\Router and Illuminate\Http\Request to make the call
// create the request
$this->request->create($uri, $method, $data, [], [], $server, $content);
// get the response
$response = $this->router->prepareResponse($request, $this->app->handle($request));

Using a webservice from my Zend project

I'm working with an external API webservice that returns a json output true or false. I visit a URL like
http://site.com/api/valid
and it gives me something like this, which looks like json
"true"
Right now I'm visiting the url manually, but I want to now do it programmatically from within my zend project. What should I use to get the result correctly
there are a lot of ways. The simplest is to use file_get_contents().
$result = file_get_contents("http://site.com/api/valid");
// if result is truly json
// data will be
// array( 0 => true)
$data = json_decode($result);
If it is a popular webservice . there might be a library written for it already. This is preferred since it will handle error conditions and corner cases. Google around for it first.
Sounds like you need a method that can grab the endpoint. Well, there's many ways but since you are already using Zend, you might as well read up on http://framework.zend.com/manual/1.11/en/zend.http.client.adapters.html
Here's a static method:
static function curl($url, $method, $params = array()){
$client = new Zend_Http_Client($url);
if($method == "POST"){
$client->setParameterPOST($params);
}else{
$client->setParameterGet($params);
}
$response = $client->request($method);
return $response->getBody();
}
Or use php's native method $response = file_get_contents($url);
Make sure to json_decode() your responses.

Categories