Passport: Client Credentials Grant - How Does it Work?

Created on 26 Jan 2017  ·  13Comments  ·  Source: laravel/passport

It's not clear to me that a Passport Server would ever be able to authenticate a client credentials token using the TokenGuard. It seems that tokens passed out for a client_credentials grant type do not contain a subject claim ('sub').

This subject gets translated into what the League's server calls 'oauth_user_id' but that is set to null (because of the no subject issue).

The default checks try to find a user but there is never any user with a null identifier so it should always fail.

The code is a little hairy to track down but:

  1. There's a client repository bridge;
  2. This calls "$record = $this->clients->findActive($clientIdentifier);" on line 39 of passport/src/Bridge/ClientRepository.php;
  3. The method it is in gets called from the TokenGuard which does " $user = $this->provider->retrieveById($psr->getAttribute('oauth_user_id')); if (! $user) { return }" of passport/src/Guards/TokenGuard (lines 115-120).

I used a basic route like this for testing, it was in the API routes (routes/api.php):

Route::middleware('auth:api')->get('/test', function () {
    return response()->json([ 'status' => 'ok' ], 200);  
});                                                      

After successfully retrieving a client_credentials token and passing it, though, I'd always get unauthenticated. By putting a bunch of 'dds' (I know, kludgy but it worked) I found what I've noted above.

It's possible I'm doing something wrong, of course.

System details:

php 7.1.0 run via fastcgid on apache

from composer.lock:

582             "name": "laravel/passport",
 583             "version": "v2.0.0",
 584             "source": {
 585                 "type": "git",
 586                 "url": "https://github.com/laravel/passport.git",
 587                 "reference": "da617aa36720d11ad7f358630715b4ebd585b596"
 588             },

453             "name": "laravel/framework",
 454             "version": "v5.4.0",
 455             "source": {
 456                 "type": "git",
 457                 "url": "https://github.com/laravel/framework.git",
 458                 "reference": "7212b1e9620c36bf806e444f6931cf5f379c68ff"
 459             },

Most helpful comment

@lloy0076 Your die statement won't be called if you are not explicitly using the CheckClientCredentials middleware. You have to define the middleware and apply it to your routes. Lumen example below (should be pretty similar in Laravel, defining middleware in your service provider instead):

bootstrap/app.php

$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
    'client_credentials' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
]);

routes/web.php

$api->group(['middleware' => ['client_credentials']], function () use ($api) {
    // define client_credentials protected routes
});

I have my own issue though. I was wondering how one could use both auth and this client_credentials middleware depending on the type of token used, so I could use the same route for both.

All 13 comments

Client credentials are validated with a different middleware since there is no user. https://github.com/laravel/passport/blob/1.0/src/Http/Middleware/CheckClientCredentials.php

@RDelorier - that makes sense but then it also means it should make sense that the default Middleware SHOULD NOT appear to grant access when that access isn't valid or there's a bug where a clien_credentials grant is being sent to the wrong Middleware during the authentication process (or more likely a misconfiguration somewhere).

I adjusted the handle method in a local copy of https://github.com/laravel/passport/blob/1.0/src/Http/Middleware/CheckClientCredentials.php to:

 31     /**
 32      * Handle an incoming request.
 33      *
 34      * @param  \Illuminate\Http\Request $request
 35      * @param  \Closure $next
 36      * @return mixed
 37      *
 38      * @throws \Illuminate\Auth\AuthenticationException
 39      */
 40     public function handle($request, Closure $next, ...$scopes)
 41     {
 42         $psr = (new DiactorosFactory)->createRequest($request);
 43         die($psr);
 44
 45         try{
 46             $psr = $this->server->validateAuthenticatedRequest($psr);
 47         } catch (OAuthServerException $e) {
 48             throw new AuthenticationException;
 49         }
 50
 51         foreach ($scopes as $scope) {
 52            if (!in_array($scope,$psr->getAttribute('oauth_scopes'))) {
 53              throw new AuthenticationException;
 54            }
 55          }
 56
 57         return $next($request);
 58     }

(the first two numbers are line numbers)

...and that's not being called.

i.e.

curl -X POST -H "Cache-Control: no-cache" -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "grant_type=client_credentials" -F "client_id=2" -F "client_secret=Eou6kZjdETSQjf2mAlwbSXxlkfAnnkQNkaNCHO61" "http://playground.dev/oauth/token"

That just granted me:

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImE4NjFmM2NlNTI1Zjg0NDM0NDJhZTE1ZTY4NzhmMTE0ODY3OGY4NGY2NjYxNTM2MjEzNmMyMGI2OWRlMDQyZjZhMjliY2RmMWE4MzQ2MGI2In0.eyJhdWQiOiIyIiwianRpIjoiYTg2MWYzY2U1MjVmODQ0MzQ0MmFlMTVlNjg3OGYxMTQ4Njc4Zjg0ZjY2NjE1MzYyMTM2YzIwYjY5ZGUwNDJmNmEyOWJjZGYxYTgzNDYwYjYiLCJpYXQiOjE0ODU0NTAzMDgsIm5iZiI6MTQ4NTQ1MDMwOCwiZXhwIjoxNDg2NzQ2MzA4LCJzdWIiOiIiLCJzY29wZXMiOltdfQ.fi3jP5BAvTPXV-c5O36oezibbRd5GwsFpEgNU1uE76zpAsojGpqdb-dDPKA5kMgh75Z3zCLxxA79YKmQEFTCJ_Ablm32f39sNnt7YpJq3ddVGFjyDceXYM2aanwMGEUjAXr1Q9Mt3OXGHju1lPLs7WY7uNJL1HGTAaaWCBFHgUi9DiO-l6A9WUwrlLj914RRqK_b3b7SLfUJ15nO-3uju9QXhtqZ1t51MHxAtqy4fUY1EMM_oJ_03L1wOgg7Soa2hfWOoH6HUp1vuMRvg5NIxZWJeA6V4Zk9MYB-nfTJfpZV13vUYoYeNDnKL7n6d_ROG9Vlu5Rgxb6MGk2Prl4KElbSITn2rgIS9S0I_vvNt1XDf8SHRUBLlKMPLYaBVl7GymAJEbp1nC7N6JTyIIhv3ZJv-b0j-vjLwcS7fcNk7XNCD8xFhK0iUlehXnLFGTnH6ZorKWOiA841snSicJl1fYzDdZBV4ljCPHzOweylcJeplSYSyPrNzqk6RfeWt6r1txpggi0fvKNmmPVF9X9V1pY6Zon5WGzMD-SlXqfCUaGavweK-UUt4CKzQPHXXiHiIexFXxxD__F0r7byszaXOGPaHB8gH4KqU4dlYnqrLMaFrYIsNxWA8qPYOQrGlp0W3f07hj7_g6sGErlCgT4Yf2Pzq1n1MKHx2MSR5-eKVzI

I added a 'testapi' to my kernel and added a 'routes/testapi' as attached.

I also created a 'slow' middleware group (bad name but I'm just testing) because leaving the bindings in seemed to trigger some error (i.e. I couldn't get the bearer token to work).

curl -X GET -H "Accept: application/json" -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImZkZGM3M2NhNGMwOWVlNzVhNzY1ZWM4ZWEwMGUwNTc3ZDliYzJlNTM3MDcyZTRmNjQzOGQxM2JiNWQxZjliY2FmZmY4OWJjOWYwYWU2ZWZmIn0.eyJhdWQiOiIyIiwianRpIjoiZmRkYzczY2E0YzA5ZWU3NWE3NjVlYzhlYTAwZTA1NzdkOWJjMmU1MzcwNzJlNGY2NDM4ZDEzYmI1ZDFmOWJjYWZmZjg5YmM5ZjBhZTZlZmYiLCJpYXQiOjE0ODU0NTA1ODQsIm5iZiI6MTQ4NTQ1MDU4NCwiZXhwIjoxNDg2NzQ2NTg0LCJzdWIiOiIiLCJzY29wZXMiOltdfQ.Vvnte_alru9PIArvXNtybXixuNtHMdOhztoWMPKUgEb6iM956r3f4IbHNpcngRGmWKP6wha_NhMlnMetLaU1QumZQBXhuVThX-6W3DQQG2AgspV8vDTc2PGB-23v7PGwsS7_vE5gHJ3t51PKEVCAHHyDfOFKM3jOrDj4tUDNL7ndJI1TBWhqhifnPtu04jn3RzaadvJPfiKWimc5eFYEpOEYevyL0IbJb3bPORSghMkdacuY6k_C88eNRd3z9ld4clI37_-H0SRfj4QFdl_pS5d_OlghcoIa7SFhbNuHJ6i3x5BLQKIhdePeyL72v5idJt8vYZXAsPSO1T7Cx-Kos-LhYPqhWVWMNO13_Oat-cPrljA6k3nTg7T_zBlUzUzMS03eCszMP3g3a5OMBQQ_ROAcHI0STObbEqBNy5ewCRBfGSPyfzWpETfdFHbgwqe_FGLKeuaYFDqCnyORCULA1jzKFE-MeUruX8_1r6fQ_QZfK4BeDTFGqnVGVSY7yoVmo-yyg_5exkWlEn_HBw_PpNXutyy1stfAC9yq_LefbQ2YFjU8eqPWoQ5VnXymCrWu_deN5vWkz9a4Zsc8HVnpIORmN4xSFycfU8kEJMO00bXqpbSfq4q4fS-XLGYRGX7ZLPPUO7akiSdd1X1_106rji3zrhTDSiWnGrdsPvaCvdQ" -H "Cache-Control: no-cache" -H "Postman-Token: 2894f548-8d6a-fc93-2678-1d6306d2e175" "http://playground.dev/testapi/status

app.zip

Did you also enable implicit grant?
https://github.com/laravel/passport/blob/1.0/src/Passport.php#L89

Passport::enableImplicitGrant();

@RDelorier - no, I don't have implicit grant(s) enabled - is that a requirement? If it is, I think I missed where it said it is.

I'm using passport in lumen 5.4 and I have problems to.
if I use the CheckClientCredentials instead auth middleware I can authenticate the client but not retrieve the authenticated user (Auth::user() return null)
if I use both middlewares all requests return a 401 Unauthorized

@mapb1990 - The CheckClientCredentials is not meant to return any user.

See:

@lloy0076 Your die statement won't be called if you are not explicitly using the CheckClientCredentials middleware. You have to define the middleware and apply it to your routes. Lumen example below (should be pretty similar in Laravel, defining middleware in your service provider instead):

bootstrap/app.php

$app->routeMiddleware([
    'auth' => App\Http\Middleware\Authenticate::class,
    'client_credentials' => \Laravel\Passport\Http\Middleware\CheckClientCredentials::class,
]);

routes/web.php

$api->group(['middleware' => ['client_credentials']], function () use ($api) {
    // define client_credentials protected routes
});

I have my own issue though. I was wondering how one could use both auth and this client_credentials middleware depending on the type of token used, so I could use the same route for both.

@lloy0076 I found the same issue today.
And I found it caused by league/oauth2-server. in LeagueOAuth2ServerGrantClientCredentialsGrant, Line 38, it set null for user_id, thus no user_id while issueAccessToken, (the same: no user_id store in database table 'oauth_clients').
`class ClientCredentialsGrant extends AbstractGrant
{
/**
* {@inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));

    // Finalize the requested scopes
    $scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client);

    // Issue and persist access token
    $accessToken = $this->issueAccessToken($accessTokenTTL, $client, null, $scopes);

    // Inject access token into response type
    $responseType->setAccessToken($accessToken);

    return $responseType;
}

/**
 * {@inheritdoc}
 */
public function getIdentifier()
{
    return 'client_credentials';
}

}`

Then I think this should be update in TokenGuard, after Line 128, if $user == null, find user by clientId

Closing for lack of activity, hope you got the help you needed :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

seriousjelly picture seriousjelly  ·  3Comments

raksrivastava picture raksrivastava  ·  3Comments

andcl picture andcl  ·  3Comments

rudolfdobias picture rudolfdobias  ·  3Comments

s4uron picture s4uron  ·  3Comments