Skip to content

Commit 8f819da

Browse files
authored
Add Tokens::getSubjectIdFromToken (#22)
Add `Tokens::getSubjectIdFromToken` method for extracting the principal subject's client ID from a token Persona tokens include a `sub` claim, which identifies the client to whom the token belongs. This adds a method to get the client ID from an access token. Bumps to v0.6.0
1 parent 04785a3 commit 8f819da

File tree

3 files changed

+128
-1
lines changed

3 files changed

+128
-1
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "talis/talis-php",
33
"description": "This is a php client library for talis APIs",
4-
"version": "0.5.0",
4+
"version": "0.6.0",
55
"keywords": [
66
"persona",
77
"echo",

src/Talis/Persona/Client/Tokens.php

+45
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,51 @@ public function listScopes(array $tokenInArray)
287287
throw new InvalidTokenException('decoded token has neither scopes nor scopeCount');
288288
}
289289

290+
/**
291+
* Extract the client ID for the Subject (the `sub` claim) from a token.
292+
* @param string $accessToken An access token (JWT)
293+
* @return string The client ID
294+
*
295+
* @throws TokenValidationException If the token is not a string
296+
* @throws InvalidTokenException If the token is valid, but contains no client ID
297+
* @throws \Exception If the token is invalid in other ways
298+
*/
299+
public function getSubjectIdFromToken($accessToken)
300+
{
301+
$clientId = $this->getClaimForToken($accessToken, 'sub');
302+
if (empty($clientId)) {
303+
throw new InvalidTokenException('Decoded token contains no client ID');
304+
}
305+
return $clientId;
306+
}
307+
308+
/**
309+
* Extract a named claim from a token.
310+
* @param string $accessToken An access token (JWT)
311+
* @param string $claim A claim identifier
312+
* @return string|null The claim value from the token, or `null` if unavailable.
313+
*
314+
* @throws TokenValidationException If the token is not a string
315+
* @throws \InvalidArgumentException If the claim identifier is not a string
316+
* @throws \Exception If the token is invalid in other ways
317+
*/
318+
public function getClaimForToken($accessToken, $claim)
319+
{
320+
if (!is_string($accessToken)) {
321+
throw new TokenValidationException('Access token is not a string');
322+
}
323+
if (!is_string($claim)) {
324+
throw new \InvalidArgumentException('Claim is not a string');
325+
}
326+
327+
$decodedToken = $this->decodeToken($accessToken, $this->retrieveJWTCertificate());
328+
329+
if (isset($decodedToken[$claim]) && is_string($decodedToken[$claim])) {
330+
return $decodedToken[$claim];
331+
}
332+
return null;
333+
}
334+
290335
/**
291336
* Checks the supplied config, verifies that all required parameters are present and
292337
* contain a non null value;

test/unit/Persona/TokensTest.php

+82
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,38 @@ public function testRetrieveJWTCertificateCachingSaveFailure()
13091309
$tokens->retrieveJWTCertificate();
13101310
}
13111311

1312+
/**
1313+
* @covers Tokens::getSubjectIdFromToken
1314+
*/
1315+
public function testGetSubjectIdFromTokenReturnsClientIdFromToken()
1316+
{
1317+
$mockClient = $this->getMockTokensClientWithFakeCertificate();
1318+
1319+
$fakeClientId = 'this-is-a-fake-client-id';
1320+
$accessToken = $this->getFakeJWT([
1321+
'sub' => $fakeClientId,
1322+
'scopes' => [$fakeClientId]
1323+
]);
1324+
1325+
$clientIdFromToken = $mockClient->getSubjectIdFromToken($accessToken);
1326+
$this->assertEquals($fakeClientId, $clientIdFromToken);
1327+
}
1328+
1329+
/**
1330+
* @covers Tokens::getSubjectIdFromToken
1331+
*/
1332+
public function testGetSubjectIdFromTokenThrowsExceptionIfTokenContainsNoSubClaim()
1333+
{
1334+
$mockClient = $this->getMockTokensClientWithFakeCertificate();
1335+
1336+
$accessToken = $this->getFakeJWT([
1337+
'sub' => null
1338+
]);
1339+
1340+
$this->setExpectedException(InvalidTokenException::class);
1341+
$mockClient->getSubjectIdFromToken($accessToken);
1342+
}
1343+
13121344
/**
13131345
* Gets the client with mocked HTTP responses.
13141346
*
@@ -1323,4 +1355,54 @@ private function getMockHttpClient(array $responses = [])
13231355

13241356
return $httpClient;
13251357
}
1358+
1359+
/**
1360+
* Create a mock `Tokens` client, with the cache backend and certificate defined in instance variables
1361+
* for this class.
1362+
* @return \Talis\Persona\Client\Tokens
1363+
*/
1364+
private function getMockTokensClientWithFakeCertificate()
1365+
{
1366+
/** @var \Talis\Persona\Client\Tokens|\PHPUnit_Framework_MockObject_MockObject $mockClient */
1367+
$mockClient = $this->getMock(
1368+
\Talis\Persona\Client\Tokens::class,
1369+
['retrieveJWTCertificate'],
1370+
[
1371+
[
1372+
'userAgent' => 'unittest',
1373+
'persona_host' => 'localhost',
1374+
'cacheBackend' => $this->cacheBackend,
1375+
]
1376+
]
1377+
);
1378+
1379+
$mockClient->expects($this->once())
1380+
->method('retrieveJWTCertificate')
1381+
->willReturn($this->publicKey);
1382+
1383+
return $mockClient;
1384+
}
1385+
1386+
/**
1387+
* Creates a fake JWT with realistic-looking claim data.
1388+
*
1389+
* @param array $claims An array of JWT claims.
1390+
* @return string An encoded JWT encapsulating the specified claims
1391+
*/
1392+
private function getFakeJWT(array $claims = [])
1393+
{
1394+
$now = time();
1395+
$fakeSubjectClientId = "fake-sub-client-id-{$now}";
1396+
$fakeAudienceClientId = "fake-aud-client-id-{$now}";
1397+
$defaultClaims = [
1398+
'aud' => $fakeAudienceClientId,
1399+
'exp' => $now + 100,
1400+
'iat' => $now,
1401+
'jti' => $now,
1402+
'scopes' => [$fakeSubjectClientId],
1403+
'sub' => $fakeSubjectClientId
1404+
];
1405+
$claimsWithDefaults = array_merge($defaultClaims, $claims);
1406+
return JWT::encode($claimsWithDefaults, $this->privateKey, 'RS256');
1407+
}
13261408
}

0 commit comments

Comments
 (0)