Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
TRACK

Dev Tools >

Client Credentials

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Meet Brent. He's the hardworking, beard-growing, kale-munching type who has a coop of the nicest, smartest, and best egg-laying chickens this side o' the Mississippi! But feeding his chickens and doing other things around the farm has always taken a lot of time.

But great news! The brand new "Chicken Oversight Operations Platform", or COOP site has just launched! With COOP, you can login to the site and collect your chicken eggs, unlock the barn, and do all kinds of other things just by clicking a button.

Noticing that COOP has an API, Brent wonders if he could write a little script that would collect his eggs automatically. Yea, if he had a script that made an API request on his behalf, he could run it on a CRON job daily and sleep in!

So COOP is real, sort of. You can find this make-believe website by going to http://coop.apps.knpuniversity.com. Go ahead and create an account, and start controlling your virtual farm. It's the future!

Starting our Command-line Script

COOP's API is simple, with just a few endpoints, including the one we want for our little command-line script: eggs-collect.

I've already made a cron/ directory with a script called collect_eggs.php that'll get us started:

// collect_eggs.php
include __DIR__.'/vendor/autoload.php';
use Guzzle\Http\Client;

// create our http client (Guzzle)
$http = new Client('http://coop.apps.knpuniversity.com', array(
    'request.options' => array(
        'exceptions' => false,
    )
));

Tip

Code along with us! Click the Download button on this page to get the starting point of the project, and follow the readme to get things setup.

It doesn't do anything except create a Client object that's pointing at the COOP website. Since we'll need to make HTTP requests to the COOP API, we'll use a really nice PHP library called Guzzle. Don't worry if you've never used it, it's really easy.

Before we start, we need to use Composer to download Guzzle.

This tutorial uses an old (version 3) version of Guzzle! It doesn't affect the tutorial, but if you decide to install it manually, be sure to install guzzle/guzzle.

Download Composer into the cron/ directory and then install the vendor libraries:

php composer.phar install

Tip

New to Composer? Do yourself a favor and master it for free: The Wonderful World of Composer.

Let's try making our first API request to /api/2/eggs-collect. The 2 is our COOP user ID, since we want to collect eggs from our farm. Your number will be different:

// collect_eggs.php
// ...

$request = $http->post('/api/2/eggs-collect');
$response = $request->send();
echo $response->getBody();

echo "\n\n";

Try it by executing the script from the command line:

php collect_eggs.php

Not surprisingly, this blows up!

{
  "error": "access_denied",
  "error_description": "an access token is required"
}

OAuth Applications

But before we think about getting a token, we need to create an application on COOP (http://coop.apps.knpuniversity.com/api - click "Your Applications" and then "Create your Application"). The application represents the external app or website that we want to build. In our case, it's the little command-line script. In OAuth-speak, it's this application that will actually ask for access to a user's COOP account.

Give it a name like "Brent's Lazy CRON Job", a description, and check only the box for "Collect Eggs from Your Chickens". These are "scopes", or basically the permissions that your app will have if a token is granted from COOP.

When we finish, we now have a Client ID and an auto-generated "Client Secret". These are a sort of username and password for the application. One tricky thing is that the terms "application" and "client" are used interchangeably in OAuth. And both are used to refer to the application we just registered and the actual app you're building, like the CRON script or your website. I'll try to clarify along the way.

Now, let's get an access token!

Client Credentials Grant Type

The first OAuth grant type is called Client Credentials, which is the simplest of all the types. It involves only two parties, the client and the server. For us, this is our command-line script and the COOP API.

Using this grant type, there is no "user", and the access token we get will only let us access resources under the control of the application. When we make API requests using this access token, it's almost like we're logging in as the application itself, not any individual user. I'll explain more in a second.

If you visit the application you created earlier, you'll see a nice "Generate a Token" link that when clicked will fetch one. Behind the scenes, this uses client credentials, which we'll see more closely in a second.

http://coop.apps.knpuniversity.com/token
    ?client_id=Your+Client+Name
    &client_secret=abcdefg
    &grant_type=client_credentials

But for now, we can celebrate by using this token immediately to take actions on behalf of the application!

Access Tokens in the API

Exactly how to do this depends on the API you're making requests to. One common method, and the one COOP uses, is to send it via an Authorization Bearer header.

GET /api/barn-unlock HTTP/1.1
Host: coop.apps.knpuniversity.com
Authorization: Bearer ACCESSTOKENHERE

Update the script to send this header:

// collect-eggs.php
// ...

$accessToken = 'abcd1234def67890';

$request = $http->post('/api/2/eggs-collect');
$request->addHeader('Authorization', 'Bearer '.$accessToken);
$response = $request->send();
echo $response->getBody();

echo "\n\n";

When we run the script again, start celebrating, because it works! And now we have enough eggs to make an omlette :)

{
  "action": "eggs-collect",
  "success": true,
  "message": "Hey look at that, 3 eggs have been collected!",
  "data": 3
}

Trying to Collect Someone Else's Eggs

Notice that this collects the eggs for our user becase we're including our user ID in the URL. What happens if we change id to be for a different user?

/api/3/eggs-collect

If you try it, it fails!

{
  "error": "access_denied",
  "error_description": "You do not have access to take this action"
}

Technically, with a token from client credentials, we're making API requests not on behalf of a user, but on behalf of an application. This makes client credentials perfect for making API calls that edit or get information about the application itself, like a count of how many users it has.

We decided to build COOP so that the application also has access to modify the user that created the application. That's why we are able to collect our user's eggs, but not our neighbor's.

Getting the Token via Client Credentials

Put the champagne away: we're not done yet. Typically, access tokens don't last forever. COOP tokens last for 24 hours, which means that tomorrow, our script will break.

Letting the website do the client-credentials work for us was nice for testing, but we need to do it ourselves inside the script. Every OAuth server has an API endpoint used to request access tokens. If we look at the COOP API Authentication docs, we can see the URL and the POST parameters it needs:

http://coop.apps.knpuniversity.com/token

Parameters:
    client_id
    client_secret
    grant_type

Let's update our script to first make this API request. Fill in the client_id, client_secret and grant_type POST parameters:

// collect-eggs.php
// ...

// run this code *before* requesting the eggs-collect endpoint
$request = $http->post('/token', null, array(
    'client_id'     => 'Brent\'s Lazy CRON Job',
    'client_secret' => 'a2e7f02def711095f83f2fb04ecbc0d3',
    'grant_type'    => 'client_credentials',
));

// make a request to the token url
$response = $request->send();
$responseBody = $response->getBody(true);
var_dump($responseBody);die;
// ...

With any luck, when you run it, you should see a JSON response with an access token and a few other details:

{
  "access_token": "fa3b4e29d8df9900816547b8e53f87034893d84c",
  "expires_in": 86400,
  "token_type": "Bearer",
  "scope": "chickens-feed"
}

Let's use this access token instead of the one we pasted in there:

// collect-eggs.php
// ...

// step1: request an access token
$request = $http->post('/token', null, array(
    'client_id'     => 'Brent\'s Lazy CRON Job',
    'client_secret' => 'a2e7f02def711095f83f2fb04ecbc0d3',
    'grant_type'    => 'client_credentials',
));

// make a request to the token url
$response = $request->send();
$responseBody = $response->getBody(true);
$responseArr = json_decode($responseBody, true);
$accessToken = $responseArr['access_token'];

// step2: use the token to make an API request
$request = $http->post('/api/2/eggs-collect');
$request->addHeader('Authorization', 'Bearer '.$accessToken);
$response = $request->send();
echo $response->getBody();

echo "\n\n";

Now, it still works and since we're getting a fresh token each time, we'll never have an expiration problem. Once Brent sets up a CRON job to run our script, he'll be sleeping in 'til noon!

Why, What and When: Client Credentials

Every grant type eventually uses the /token endpoint to get a token, but the details before that differ. Client Credentials is a way to get a token directly. One limitation is that it requires your client secret, which is ok now because our script is hidden away on some server.

But on the web, we won't be able to expose the client secret. And that's where the next two grant types become important.

Leave a comment!

56
Login or Register to join the conversation
Default user avatar
Default user avatar kenton_2233 | posted 4 years ago

Can you please explain what is the meaning of this sentence? Sorry, I am not able to understand it.

We decided to build COOP so that the application also has access to modify the user that created the application. That's why we are able to collect our user's eggs, but not our neighbor's.

It appears to be contradictory.

15 Reply

Hi kenton_2233!

Yes, let me see if I can make this sentence more clear :).

When you authenticate via "client credentials", you are not really "authenticating" or "logging in" as a specific user. You're actually "authenticating" as the "app" you created. In this example, we went to the COOP site and created an app called "Brent's Lazy CRON Job". So when we authenticate via "client credentials", we are really "authenticating" or logging in *as* that "app" - not as our user. This normally means that we would only have access to view and modify settings on our app - but NOT to do things that our user account normally could do (e.g. collect eggs). Thus, "client credentials" is often pretty limiting.

So, when our "imaginary" company created COOP, we had to make a decision: if someone authenticates via "client credentials", should they *only* have access to modify the application, or should we *also* allow them to take actions on behalf of the user who owns the application? In this sentence, I'm describing that we (creators of the COOP site) decided to make our OAuth server a bit more flexible so that you *can* take user actions even though you're only authenticating with "client credentials". In other OAuth servers, this might not be true. For example, if you use client credentials to authenticate with Facebook, you don't have access to some of the normal things you do with a normal access token. For example, I believe that you cannot use the /me endpoint (because you're not authenticated as a user!) but you *can* pull some stats from your application.

I hope this helps!

Reply
Default user avatar
Default user avatar Mike Ill Kilmer | posted 4 years ago

Here's a version of the whole thing for GuzzleHttp/Guzzle version 6, which might require updating the composer.json file as follows:

```
{
"require": {
"guzzlehttp/guzzle": "~6.2",
"psr/log": "^1.0"
},
"minimum-stability": "dev"
}
```

Then running composer update.

```

include __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Client;

// create our http client (Guzzle)
$client = new Client(['base_uri' => 'http://coop.apps.knpunivers...', 'defaults' => [
'exceptions' => false ]]);

$res = $client->request('GET', 'http://coop.apps.knpunivers...', array(
'request.options' => array(
'exceptions' => true,
)
));
echo $res->getStatusCode();
echo "\n\n";

$http = new Client(['base_uri' => 'http://coop.apps.knpunivers...', 'defaults' => [
'exceptions' => false ]]);

$response = $http->request('POST', '/token', [
'form_params' => [
'client_id' => 'ABC',
'client_secret' => 'credential',
'grant_type' => 'client_credentials'
]
]);

$responseBody = $response->getBody(true);
$responseArr = json_decode($responseBody, true);
$accessToken = $responseArr['access_token'];
// step2: use the token to make an API request
$headers = array('Authorization' => 'Bearer '.$accessToken);
$response = $http->request('POST', '/api/2027/eggs-collect', array(
"headers" => $headers
));
echo $response->getBody();

echo "\n\n";
```

11 Reply
Default user avatar
Default user avatar Sébastien Rosset | posted 4 years ago

For information, the code given in this tutorial works with GuzzleHTTP 3 or earlier. On later version, it is very different.

Here's what I did to make it work:

include __DIR__.'/vendor/autoload.php';
use GuzzleHttp\Client;

$client = new Client( [

'base_uri' => 'http://coop.apps.knpunivers...',
'defaults' => [
'exceptions' => false,

]
]);

$response = $client->post( 'api/2/eggs-collect', [
'headers' => [
'Authorization' => 'Bearer tokensdcsdcsdc66561'
]
]);

echo $response->getBody();

1 Reply

Thanks for posting that Sébastien Rosset! You're absolutely right - and fortunately, version 3 of Guzzle is still available (but getting quite old). But yes - if you are ok re-working some of your Guzzle code (just like you did), then feel free to use the new version :).

Cheers!

Reply
Abelardo Avatar

This course has to be marked as deprecated or obsolete since many changes have happened since its release.

Reply

Hey AbelardoLG,

Thank you for your interest in SymfonyCasts tutorials!

Could you give us a bit more information about what exactly changed? Yes, this course is old, and yes, it's based on some old PHP dependencies, but the concepts we teach in this course related to OAuth 2 are still valid for today and still used in literally all platforms like Google, Facebook, etc.

Cheers!

Reply
Abelardo Avatar

Hey Victor.
First of all, sorry for replying too late!

I tried to reproduce this course but Guzzle changed its API, methods, and so on.

I am stuck with this video.

Best regards and my best wishes to Symfony Casts staff!

Reply

Hey AbelardoLG,

Yeah, major versions of libraries we use in the course might lead some BC breaks, I'd recommend you to download the course code from this page and start from the start/ folder following instructions in the README.md file inside. This way you would be able to follow the course and match the code in the video.

Cheers!

1 Reply

Chapters should be marked complete when videos or script is completely viewed.

Reply

Hey @Akshit!

Ah, sorry you're having problems! Indeed, the chapter *should* be marked as complete when the video is completely viewed (or, more technically, when you view, I think, beyond around 90% of the video, it is marked as completed). But there is no feature currently to mark the chapter is viewed by viewing the script. Is that the part that you would like to see? We're always collecting feedback to help us improve - so please let me know!

Cheers!

Reply
Peder N. Avatar
Peder N. Avatar Peder N. | posted 3 years ago

I just bought this course and http://coop.apps.knpunivers... doesnt seems to work

I only receive a http response 522

Reply

Hey Peder N.

Thanks for informing us about that problem. I'm happy to tell you that it's already fixed, you should be able to keep going with this tutorial. I'm sorry for any inconvenience

Reply
Julia V. Avatar
Julia V. Avatar Julia V. | posted 4 years ago

Please, remember to change the baseUrl to 'http://coop.apps.symfonycas...
If you don’t do so, you should get a "405 Method Not Allowed" error.
This is happening because 'http://coop.apps.knpunivers...' is trying to redirect to 'http://coop.apps.symfonycas... through a GET request.

Reply

Hi Julia V.

Yes it was not perfect redirect, but we fixed it. Now everything works good!

Cheers!

1 Reply
Łukasz K. Avatar
Łukasz K. Avatar Łukasz K. | posted 4 years ago

Hello,
I'm stuck in the very begining. When i try to send the request with command "php collect_eggs.php" i get "No route found for "POST /application/api/api/25/eggs-collect"

Please help

Reply
Łukasz K. Avatar

Sorry, stupid mistake with slash in a wrong place. Solved :)

Reply

Hey Lukasz,

I'm glad you got it working so fast by yourself.

Cheers!

Reply
Default user avatar
Default user avatar Mohammed Majid Ali | posted 4 years ago

When I try to run the code php collect_eggs.php, It is showing the php code, not the response in braces { }. Please help. I tried this in both Windows and Fedora, thank you.

Reply

Hey Mohammed Majid Ali!

Hmm. Make sure you have the

<?php

line at the top of your PHP file. It sounds like you may be missing this :).

Cheers!

Reply
Default user avatar
Default user avatar Serdan1689 | posted 4 years ago

$request = $client->post('/token', null, array(
'grant_type' => 'client_credentials',
'client_id' => "Daniel's CRON Job",
'client_secret' => 'KEY',
));

Is there something wrong with this request? I keep getting a 400 response:

{"error":"invalid_request","error_description":"The grant type was not specified in the request"}

When I comment that section out it works fine. I'm not sure what's going wrong.

Reply

Hey Serdan1689 ,

Please, check out Ryan's explanation on the similar comment below: https://knpuniversity.com/screencast/oauth/client-credentials#comment-2802524257 . What REST client are you using here?

Cheers!

1 Reply
Default user avatar

Hi guys,

I want to use Google Analytics for my website (to embed a nice looking graph, e.g. with Chart.js, which should show the unique pageviews for the last 30 days ). For this, I thought it would be good to use client credentials.

I have problems to receive the access token, since I believe Google does not support 'grant_type' => 'client_credentials'? Is this right? When i use Google OAuth 2.0 Playground and use their token, the GET-Request https://www.googleapis.com/... in the second step works totally fine for me. The problem consists in fetching the access token in the first step.

Anyways this is how my code looks like in testing.php:

require __DIR__.'/vendor/autoload.php';

$http = new \Guzzle\Http\Client([
'defaults' => [
'exceptions' => false,
]
]);

$request = $http->post('https://www.googleapis.com/..., null, array(
'client_id' => 'XXXX',
'client_secret' => 'XXXX',
'grant_type' => 'client_credentials',
));

$response = $request->send();

--> HERE it fails ( Client error response, [status code] 400, [reason phrase] Bad Request )

$responseBody = $response->getBody(true);
$responseArr = json_decode($responseBody, true);
$accessToken = $responseArr['access_token'];

$request = $http->get('https://www.googleapis.com/...
$request->addHeader('authorization', 'Bearer '.$accessToken);

$query = $request->getQuery();
$query->set('ids', 'XXXX');
$query->set('start-date', '30daysAgo');
$query->set('end-date', 'yesterday');
$query->set('metrics', 'ga:pageviews');
$query->set('metrics', 'ga:uniquePageviews');

$response = $request->send();

echo $response->getBody();

echo "\n\n";

Or is there another way?

Thank you for your help,
Chris

Reply
Default user avatar
Default user avatar Brent Shaffer | Chris | posted 4 years ago

Hey Chris!
Yes, you're right. You'll want to use the google auth library to make this call. What you're doing looks mostly correct, but rather than using Client Credentials, you'll use Service Account Credentials. Download a service account credentials file from the Developer Console and then do the following:


require __DIR__.'/vendor/autoload.php';

use Google\Auth\CredentialsLoader;

// this is downloaded from the Developer Console
$credentialsFile = '/path/to/your_service_account_credentials.json';

// JSON-decode the credentials into a PHP array
$jsonKey = json_decode(file_get_contents($credentialsFile), true);

// Define the scope required to access analytics data
$scopes = ['https://www.googleapis.com/auth/analytics'];

// Create a credentials object for using a service account
$credentials = CredentialsLoader::makeCredentials($scopes, $jsonKey);

// Create a Guzzle HTTP client from those credentials
$http = CredentialsLoader::makeHttpClient($credentials);

// Make an authenticated request to the API!
$response = $http->post('https://www.googleapis.com/...);
2 Reply
Default user avatar

Hey Brent Shaffer,

thank you very much for your help. Now it`s working fine.

For those who are interested, but still struggling (This refers especially to the Google analytics api): 1. Make sure you download the service account credentials file. Nothing else. 2. In Google Analytics under administration>user-administration you have to grant permission to the beforehand created Service Account, otherwise you will get an error.

The google auth library is easy to implement, but unfortunately I went the wrong way first :)

Thank you for your work!
Chris

1 Reply
Default user avatar
Default user avatar Kelvin Dijkstra | posted 4 years ago

What technologies were used to create the coop api/oauth app and api docs/sandbox?

Reply

Yo Kelvin Dijkstra!

I could tell you, or I could show you :). You can find the code here: https://github.com/knpunive.... It's a simple Silex application, which leverages our co-author's OAuth server library: https://github.com/bshaffer.... The API docs (I assume you basically mean this page: http://coop.apps.knpunivers... are, unfortunately, just manually created in the Silex app: the API is so simple that we could do this (and we wanted it to look fun and be user-friendly).

If you have any other questions, let me know!

Cheers!

Reply
Default user avatar
Default user avatar Kelvin Dijkstra | weaverryan | posted 4 years ago | edited

weaverryan Thanks! The api looked nice and user-friendly. I've done stuff with swagger before but I liked the "feel" of your docs so I wanted to see if it was custom or something automated (like swagger)

Reply
Default user avatar
Default user avatar Rex Tsao | posted 4 years ago

Hi, where can I download the start project? I can't find it.

Reply
Default user avatar

I guess I got the start project from Github, thanks!

Reply

Hi Rex!

Yep, that's one way - glad you figured that out :). There's also a download link on the upper right of this page if you're a subscriber.

Cheers!

Reply
Default user avatar

How to use it with FOSOAuthServerBundle for symfony 3? I couldn't find any explanations or tutorials... Could you, please, provide some help?

Reply

Hey Andjii!

We don't talk about FOSOAuthServerbundle on any tutorials, but that might be great idea for a tutorial! Is the documentation unclear? Or are you stuck on anything specifically? This tutorial talks entirely about the *client* side of OAuth (not the server) - but it should help make OAuth in general a lot more clear (which is important for using OAuth as a client or a server).

Cheers!

Reply
Default user avatar
Default user avatar Julián May | posted 4 years ago

Hi Guys.

If one of you have problems getting 400 response.
This is of the way that worked fine for me.

I used Guzzle 6.


use GuzzleHttp\Client;
$http = new Client(['base_uri' => 'http://coop.apps.knpuniversity.com', 'defaults' => [
'exceptions' => false ]]);

$response = $http->request('POST', '/token', [
'form_params' => [
'client_id' => 'Your Id',
'client_secret' => 'Your Secret',
'grant_type' => 'client_credentials'
]
]);

echo $response->getBody();
Reply

Yo Julián May!

Thanks for sharing :). Just to be clear for others - be careful with that `exceptions => false` option - it "suppresses" the error when you have a 400 error. So, if you use it - just realize that if you get a 400 error, it might *look* like it's working, when it's actually not.

Cheers!

Reply
Default user avatar

Hi, did you change something on your page http://coop.apps.knpunivers... ?
I have generated a token on the web site and tried to collect eggs with this token. But then, I receive "The request requires higher privileges than provided by the access token"

Reply

Hey einue!

We haven't changed anything on the site... at least not on purpose :). But, I do have on idea. Go to your the Applications tab (http://coop.apps.knpunivers... and click to edit/view your application. Under the "Scope", do you see "eggs-collect"? If not, then it means that you have not authorized access keys created for this app to be able to take this action.

Cheers!

1 Reply
Default user avatar

Thank you! This was my mistake. ;-)

Reply
Default user avatar
Default user avatar Pierre de LESPINAY | posted 4 years ago

You are talking about installing the vendors with composer, but I can't find your requirements.
I have the impression you start from existing files, where can I find them if it is the case ?

Reply

Hey Pierre!

Yes, we do start with some files - if you're a subscriber, there's a Download button on this page that has the stuff you'll need. That download comes with a start directory and a finish directory - we're working in the start.

If you don't have a subscription, you can still get the start code directly form the repository: https://github.com/knpunive....

Does that help?

Cheers!

Reply
Default user avatar
Default user avatar Pierre de LESPINAY | weaverryan | posted 4 years ago

It sure does, thanks. Maybe you should mention the fact that one needs to subscribe to get the files, for those coming from a Google search like me.

Reply

Will do - it's not smooth yet - I probably would have had the same question as you :)

Reply
Default user avatar

Thanks!!

Reply
Default user avatar
Default user avatar Souhail Merroun | posted 4 years ago

Hey folks,
For those who have the error "The grant type was not specified in the request".
The video is done with an old Guzzle version.
Here is a snippet with a new one.

https://gist.github.com/sou...

Reply

Thanks for sharing Souhail Merroun!

Reply
Default user avatar
Default user avatar Souhail Merroun | weaverryan | posted 4 years ago | edited

weaverryan a pleasure :)

Reply
Default user avatar
Default user avatar kyaw zin win | Souhail Merroun | posted 4 years ago | edited

Hi Souhail Merroun,

Request api for eggs-collect is only working with "POST" on me. I got "405 method not allowed" response when I try with "GET".

Thanks for sharing.

Reply
Default user avatar
Default user avatar Rui Peng | posted 4 years ago

Guzzle seems to have changed a lot. The current version is Guzzle 6. The way it is used has also changed. Would you please add more information on how to use Guzzle 6? It is just a little confusing. I am looking for some information on OAuth2 for Nodejs.

Reply

Yep, unfortunately - Guzzle came out with a flurry of releases in a short time (4, 5 & 6). Fortunately, you an still install version 3 without (most) conflicts - because they renamed the library afterwards and still maintain those docs. And (also fortunately) the HTTP client itself isn't the central focus of the tutorial.

That being said, I *love* this tutorial - so it's on our list to update it someday to make it easier to use Guzzle :).

Cheers!

Reply
Default user avatar
Default user avatar Raghu Sodha | posted 4 years ago

I am not much familiar with PHP, So I thought of learning and following tutorial with a stand-alone REST client.

I this PHP piece of code to get access token.

$request = $http->post('/token', null, array(
'client_id' => 'Brent\'s Lazy CRON Job',
'client_secret' => 'a2e7f02def711095f83f2fb04ecbc0d3',
'grant_type' => 'client_credentials',
));

My question is, How do I pass these parameters in stand alone REST
client? Options I have are 1. Query Parameter 2. Headers 3. a JSON post
data.

I tried all three and get following error for all three.
{"error":"invalid_request","error_description":"The grant type was not specified in the request"}

Could you please suggest what could be going wrong here.

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!