Using PHP 8.1, Laravel 9, Guzzle 7.4.2.
I have this snippet in a service singleton registered in AppServiceProvider:
$middleware = new AccessTokenGuzzleMiddleware(
client: new Client([
'base_uri' => config('spica.url') . config('spica.space_api.path'),
'verify' => config('spica.verify_ssl'),
]),
cache: $this->app->make('cache')->store(),
tokenUrl: config('spica.url') . 'auth',
clientId: config('spica.space_api.client_id'),
clientSecret: config('spica.space_api.key'),
);
$stack = HandlerStack::create();
$stack->push($middleware);
$spaceClient = new Client([
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'handler' => $stack,
'base_uri' => config('spica.url') . config('spica.space_api.path'),
'verify' => config('spica.verify_ssl'),
]);
I can't test myself, because I have no access to base_uri, and it will only be tested once on development server, but I am curious if the Client inherits anything from the handler stack, and how does it do so if there are multiple middlewares stacked in handler?
Asking because all the examples in Guzzle docs create the final client with just $client = new Client(['handler' => $stack]);.
As you can see I currently repeat base_uri and verify lines.
Would the following work the same?
$middleware = new AccessTokenGuzzleMiddleware(
client: new Client([
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'base_uri' => config('spica.url') . config('spica.space_api.path'),
'verify' => config('spica.verify_ssl'),
]),
cache: $this->app->make('cache')->store(),
tokenUrl: config('spica.url') . 'auth',
clientId: config('spica.space_api.client_id'),
clientSecret: config('spica.space_api.key'),
);
$stack = HandlerStack::create();
$stack->push($middleware);
$spaceClient = new Client(['handler' => $stack]);
For completeness, here's the AccessTokenGuzzleMiddleware class:
<?php
declare(strict_types=1);
namespace App\Support;
use GuzzleHttp\Client;
use Psr\Http\Message\RequestInterface;
use Illuminate\Contracts\Cache\Repository as CacheContract;
/**
* This is a Guzzle middleware (see more at https://docs.guzzlephp.org/en/stable/handlers-and-middleware.html).
* It adds "Authorization: Bearer" header to requests.
*/
class AccessTokenGuzzleMiddleware
{
public function __construct(
private Client $client,
private CacheContract $cache,
private string $tokenUrl,
private string $clientId,
private string $clientSecret
) {
}
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use ($handler) {
$request = $request->withHeader('Authorization', 'Bearer ' . $this->getAccessToken());
return $handler($request, $options);
};
}
/**
* Defines cache key for access token.
*
* #return string
*/
private function cacheKey(): string
{
return self::class . $this->tokenUrl . $this->clientId . $this->clientSecret;
}
/**
* Get access token from cache or external API.
*
* #return string
*/
private function getAccessToken(): string
{
$cacheKey = $this->cacheKey();
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$tokenData = $this->retrieveClientAccessTokenData();
$this->cache->set(
key: $cacheKey,
value: $tokenData->access_token,
ttl: $tokenData->expires_in - 10
);
return $tokenData->access_token;
}
/**
* This is effectively the client (this service, not on behalf of a user)
* authenticating itself.
*/
private function retrieveClientAccessTokenData(): object
{
$authResponse = $this->client->post($this->tokenUrl, [
'json' => [
'apiKey' => $this->clientSecret,
'client_id' => $this->clientSecret,
],
]);
assertResponseOk($authResponse);
return json_decode($authResponse->getBody()->getContents());
}
}
EDIT 1
Until I get a better answer, I went with the following:
$this->app->singleton(KeycloakService::class, function (Application $app) {
// Log all Guzzle HTTP requests and responses.
// https://github.com/gmponos/guzzle-log-middleware#advanced-initialization
$middleware = new OAuthGuzzleMiddleware(
client: new Client(['verify' => config('keycloak.verify_ssl')]),
cache: $this->app->make('cache')->store(),
tokenUrl: config('keycloak.url') . 'auth/realms/' . config('keycloak.realm') . '/protocol/openid-connect/token',
clientId: config('keycloak.client_id'),
clientSecret: config('keycloak.client_secret'),
);
$stack = HandlerStack::create();
$stack->push($middleware);
$oAuthKeycloakClient = new Client([
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'handler' => $stack,
'base_uri' => config('keycloak.url') . 'auth/admin/realms/' . config('keycloak.realm') . '/',
'verify' => config('keycloak.verify_ssl'),
]);
return new KeycloakService(
client: $oAuthKeycloakClient,
groupHandler: new GroupHandler(),
userHandler: new UserHandler(),
);
});
$this->app->singleton(SpicaService::class, function (Application $app) {
// Time API
$timeClient = new Client([
'base_uri' => config('spica.url') . config('spica.time_api.path'),
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json',
'Authorization' => config('spica.time_api.auth_token'),
],
'verify' => config('spica.verify_ssl'),
]);
// Space API
$middleware = new AccessTokenGuzzleMiddleware(
client: new Client(['verify' => config('spica.verify_ssl')]),
cache: $this->app->make('cache')->store(),
tokenUrl: config('spica.url') . 'auth',
clientId: config('spica.space_api.client_id'),
clientSecret: config('spica.space_api.key'),
);
$stack = HandlerStack::create();
$stack->push($middleware);
$spaceClient = new Client([
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'handler' => $stack,
'base_uri' => config('spica.url') . config('spica.space_api.path'),
'verify' => config('spica.verify_ssl'),
]);
// Create service
return new SpicaService(
timeClient: $timeClient,
spaceClient: $spaceClient,
employeeHandler: new EmployeeHandler(),
);
});
That's not really how Guzzle works. So there's three concepts in Guzzle that you're trying to mix up, Client, Middleware and Handler which I'll try to explain.
With the Client you build the request and prepare it for the middleware/handler, this is where you define what the request is.
The Handler executes the request, note: when you create a Handlerstack::create() the default handler will be the CurlHandler, you can overwrite the handler here as a parameter
The Middleware is used to modify the request or response after the request is executed. But the request will eventually be passed to the handler to execute it.
What you can do is provide the client as a service in the AppServiceProvider
$this->app->bind(Client::class, function () {
return new Client([
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'base_uri' => config('spica.url') . config('spica.space_api.path'),
'verify' => config('spica.verify_ssl'),
]);
});
That way everywhere the Client is injected in the constructor it will inject that pre-configured client
Edit:
Just realised that would miss the handler in some cases, you could provide it as a singleton and overwrite the client when you've authenticated
Edit with contextual binding:
$this->app->when(KeycloakService::class)
->needs(Client::class)
->give(function () {
return new Client();
});
$this->app->when(KeycloakService::class)
->needs(Client::class)
->give(function () {
return new Client();
});
$this->app->when(AccessTokenGuzzleMiddleware::class)
->needs(Client::class)
->give(function () {
return new Client([
'headers' => [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'base_uri' => config('spica.url') . config('spica.space_api.path'),
'verify' => config('spica.verify_ssl'),
]);
});
Related
I want to pass the id from the controller to route but I'm having trouble with it. I am a newbie at this so I would really appreciate your help!
Controller:
public function fetchVideo(Request $request)
{
$client = new Client();
$input = $request->all();
$headers = [
'Authorization' => 'Bearer '.$input['token'],
'Content-type' => 'application/json',
'Accept' => 'application/json'
];
$params = [
'id' => $input['id'],
'fields' => $input['fields']
];
$response = $client->request ('GET', 'https://api.dailymotion.com/video/{id}', [
'headers' => $headers,
'query' => $params
]);
return json_decode($response->getBody(), true);
}
Route:
Route::post('/video/{id}', 'App\Http\Controllers\dailymotionController#fetchVideo');
public function fetchVideo(Request $request, $id) // <- {id} parameter in the route
{
...
$response = $client->request('GET', "https://api.dailymotion.com/video/{$id}", [...])
...
}
I have a method and variable which i want to be used on multiple controllers. I'm not too sure whether to use traits or services as both of them to my understanding serve almost the same purpose?
private $app_id;
private $archiving;
public function setArchieve()
{
$this->app_id = config('archiving.id');
$this->archiving = Application::findOrFail($this->app_id);
}
public function guzzleClient() {
$headers = [
'Content-Type' => 'application/json',
'X-Requested-With' => 'XMLHttpRequest',
'Authorization' => 'Bearer ' . $this->archiving->token,
];
$client = new \GuzzleHttp\Client([
'headers' => $headers,
'http_errors' => false,
'base_uri' => $this->archiving->url . '/api/',
]);
return $client;
}
I'm trying to write a test for the logout procedure for Laravel Passport. But every time I run it i am receiving the error expected 401 but got 200 which means that its not actually logging out the user.
The Logout functionality in the AuthController is as follows;
public function logout(Request $request): JsonResponse
{
$accessToken = $request->user()->token();
$refreshToken = DB::table('oauth_refresh_tokens')
->where('access_token_id', $accessToken->id)
->update([
'revoked' => true
]);
$accessToken->revoke();
return response()->json(['message' => 'Successfully logged out']);
}
This works fine, but the testing is the issue.
My test is as follows;
public function testUserIsLoggedOutProperly(): void
{
$user = factory(User::class)->create();
Passport::actingAs($user);
$this->json('GET', 'api/user')->assertStatus(JsonResponse::HTTP_OK);
$this->json('GET', 'api/logout')->assertStatus(JsonResponse::HTTP_OK);
$this->json('GET', 'api/user')
->assertStatus(JsonResponse::HTTP_UNAUTHORIZED);
}
The last assert is actually returning a HTTP_OK (200)
Any help would be greatly appreciated.
It's just a quick reflexion (not tested) but you should do your test with :
get a new token, (I think you can do that without api call)
call "api/logout" with token get in 1)
check with assertDatabaseHas function
$response = $this->json('POST','oauth/token', [
'form_params' => [
'grant_type' => 'password',
'client_id' => 'client-id',
'client_secret' => 'client-secret',
'username' => 'taylor#laravel.com',
'password' => 'my-password',
'scope' => '',
]
]);
$response->assertStatus(JsonResponse::HTTP_OK);
$token = json_decode((string) $response->getBody(), true)['access_token'];
$this->json('POST', 'api/logout')->withHeaders([
'Accept' => 'application/json',
'Authorization' => 'Bearer ' . $token
])->assertStatus(JsonResponse::HTTP_OK);
$this->assertDatabaseHas('oauth_refresh_tokens', [
'access_token_id' => $token,
'revoked' => true
]);
I have:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use GuzzleHttp\Client;
class GetBlahCommand extends Command {
protected $name = 'blah:blah';
protected $description = "name";
public function handle()
{
$client = new Client();
$res = $client->request('GET', 'https://someapi.com', [
'api_key' => ['privatekey']
]);
echo $res->getStatusCode();
}
}
But the param api_key isn't being passed along.
How can I get this to work?
I have adjusted my code, but now I am getting NULL returned:
$ndbnos = [
'ndbno' => '01009'
];
$client = new Client(['base_uri' => 'https://soemapi.com']);
$res = $client->request('GET', '/', [
'query' => array_merge([
'api_key' => 'somekey'
], $ndbnos)
])->getBody();
$res = json_decode($res);
var_dump($res);
I figured it out:
public function handle()
{
$ndbnos = [
'ndbno' => '01009'
];
$client = new Client(['base_uri' => 'https://someapi.com']);
$res = $client->request('GET', '', [
'query' => array_merge([
'api_key' => 'somekey',
'format' => 'json'
], $ndbnos)
]);
print_r(json_decode($res->getBody()));
}
You can be do it by the following way:
public function handle()
{
$client = new Client(['base_uri' => 'https://someapi.com/']);
$res = $client->request('GET', '/', [
'headers' => [
'api_key' => 'YOUR_KEY'
]
]);
}
I thought it's a header parameter. If it's a form input you can do it by the following way:
public function handle()
{
$client = new Client(['base_uri' => 'https://someapi.com/']);
$res = $client->request('GET', '/', [
'query' => [
'api_key' => 'YOUR_KEY'
]
]);
}
I try in postman like this :
I fill just input password. Then I click button update request
The view like this :
This is header :
This is body. I select raw and input data like this :
Then I click button send and it can get the response
But when I try use guzzle 6 like this :
public function testApi()
{
$requestContent = [
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
'json' => [
'auth' => ['', 'b0a619c152031d6ec735dabd2c6a7bf3f5faaedd'],
'ids' => ["1"]
]
];
try {
$client = new GuzzleHttpClient();
$apiRequest = $client->request('POST', 'https://myshop/api/order', $requestContent);
$response = json_decode($apiRequest->getBody());
dd($response);
} catch (RequestException $re) {
// For handling exception.
}
}
The result is empty
How can I solve this problem?
See in Postman, you correctly specify the field Authorization in the "headers" tab. So it sould be the same when you use Guzzle, put it in the headers:
public function testApi()
{
$requestContent = [
'auth' => ['username', 'password']
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'json' => [
'ids' => ["1"]
]
];
try {
$client = new GuzzleHttpClient;
$apiRequest = $client->request('POST', 'https://myshop/api/order', $requestContent);
$response = json_decode($apiRequest->getBody());
dd($response);
} catch (RequestException $re) {
// For handling exception.
}
}