Skip to content

Commit 1385bbd

Browse files
authored
Merge pull request #6 from pdsinterop/code-cleanup
Added token generation code to support id_token
2 parents 4b24fae + e3a6091 commit 1385bbd

8 files changed

Lines changed: 184 additions & 3 deletions

File tree

src/Config/Server.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,21 @@ final public function __construct(array $data, bool $strict = false)
5656
{
5757
$data = array_merge([
5858
OidcMeta::ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED => ['RS256'],
59-
OidcMeta::RESPONSE_TYPES_SUPPORTED => \Pdsinterop\Solid\Auth\Enum\OAuth2\ResponseType::CODE_ID_TOKEN_TOKEN,
6059
OidcMeta::SUBJECT_TYPES_SUPPORTED => ['public'],
61-
], $data);
60+
OidcMeta::RESPONSE_TYPES_SUPPORTED => array("code","code token","code id_token","id_token code","id_token","id_token token","code id_token token","none"),
61+
OidcMeta::TOKEN_TYPES_SUPPORTED => array("legacyPop","dpop"),
62+
OidcMeta::RESPONSE_MODES_SUPPORTED => array("query","fragment"),
63+
OidcMeta::GRANT_TYPES_SUPPORTED => array("authorization_code","implicit","refresh_token","client_credentials"),
64+
OidcMeta::TOKEN_ENDPOINT_AUTH_METHODS_SUPPORTED => "client_secret_basic",
65+
OidcMeta::TOKEN_ENDPOINT_AUTH_SIGNING_ALG_VALUES_SUPPORTED => ["RS256"],
66+
OidcMeta::DISPLAY_VALUES_SUPPORTED => [],
67+
OidcMeta::CLAIM_TYPES_SUPPORTED => ["normal"],
68+
OidcMeta::CLAIMS_SUPPORTED => [],
69+
OidcMeta::CLAIMS_PARAMETER_SUPPORTED => false,
70+
OidcMeta::REQUEST_PARAMETER_SUPPORTED => true,
71+
OidcMeta::REQUEST_URI_PARAMETER_SUPPORTED => false,
72+
OidcMeta::REQUIRE_REQUEST_URI_REGISTRATION => false
73+
], $data);
6274

6375
$this->data = array_filter($data, [OidcMeta::class, 'has'], ARRAY_FILTER_USE_KEY);
6476
$this->strict = $strict;

src/Entity/Scope.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,8 @@ class Scope implements ScopeEntityInterface
1010
{
1111
use EntityTrait;
1212
use ScopeTrait;
13+
14+
public function getIdentifier() {
15+
return "openid";
16+
}
1317
}

src/Enum/OpenId/OpenIdConnectMetadata.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,9 @@ class OpenIdConnectMetadata extends AbstractEnum
204204
* JSON array containing a list of the JWS [JWS] signing algorithms (alg values) [JWA] supported by the UserInfo Endpoint to encode the Claims in a JWT [JWT]. The value none MAY be included.
205205
*/
206206
public const USERINFO_SIGNING_ALG_VALUES_SUPPORTED = 'userinfo_signing_alg_values_supported';
207+
208+
/* FIXME: Solid types here */
209+
public const TOKEN_TYPES_SUPPORTED = 'token_types_supported';
210+
public const CHECK_SESSION_IFRAME = 'check_session_iframe';
211+
public const END_SESSION_ENDPOINT = 'end_session_endpoint';
207212
}

src/Factory/ConfigFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ final public function create() : Config
4747
$grantTypes = [
4848
GrantType::AUTH_CODE,
4949
GrantType::CLIENT_CREDENTIALS,
50+
GrantType::IMPLICIT,
5051
GrantType::REFRESH_TOKEN,
5152
];
5253

src/Factory/GrantTypeFactory.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use League\OAuth2\Server\Grant\AuthCodeGrant;
99
use League\OAuth2\Server\Grant\ClientCredentialsGrant;
1010
use League\OAuth2\Server\Grant\GrantTypeInterface;
11+
use League\OAuth2\Server\Grant\ImplicitGrant;
1112
use League\OAuth2\Server\Grant\RefreshTokenGrant;
1213
use Pdsinterop\Solid\Auth\Config\Expiration;
1314
use Pdsinterop\Solid\Auth\Enum\OAuth2\GrantType;
@@ -52,6 +53,10 @@ final public function createGrantType(string $grantType) : GrantTypeInterface
5253
$grant = $this->createClientCredentialsGrant();
5354
break;
5455

56+
case GrantType::IMPLICIT:
57+
$grant = $this->createImplicitGrant($expiration->forAccessToken());
58+
break;
59+
5560
case GrantType::REFRESH_TOKEN:
5661
$grant = $this->createRefreshTokenGrant($factory);
5762
break;
@@ -88,6 +93,11 @@ private function createAuthCodeGrant(RepositoryFactory $factory, DateInterval $e
8893
);
8994
}
9095

96+
private function createImplicitGrant(DateInterval $expiration) : ImplicitGrant
97+
{
98+
return new ImplicitGrant($expiration);
99+
}
100+
91101
private function createRefreshTokenGrant(RepositoryFactory $factory) : RefreshTokenGrant
92102
{
93103
return new RefreshTokenGrant(

src/Repository/AccessToken.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ public function getNewToken(
2525
array $scopes,
2626
$userIdentifier = null
2727
) : AccessTokenEntityInterface {
28-
return new AccessTokenEntity($clientEntity);
28+
29+
$accessToken = new AccessTokenEntity($clientEntity);
30+
$accessToken->setUserIdentifier($userIdentifier);
31+
foreach ($scopes as $scope) {
32+
$accessToken->addScope(new \Pdsinterop\Solid\Auth\Entity\Scope($scope));
33+
}
34+
return $accessToken;
2935
}
3036

3137
/**

src/TokenGenerator.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Pdsinterop\Solid\Auth;
4+
5+
use Pdsinterop\Solid\Auth\Utils\Jwks;
6+
use Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata as OidcMeta;
7+
8+
class TokenGenerator
9+
{
10+
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\
11+
12+
/** @var Config */
13+
public $config;
14+
15+
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
16+
17+
final public function __construct(
18+
Config $config
19+
) {
20+
$this->config = $config;
21+
}
22+
23+
public function generateRegistrationAccessToken($clientId, $privateKey) {
24+
$issuer = $this->config->getServer()->get(OidcMeta::ISSUER);
25+
26+
// Create JWT
27+
$signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
28+
$keychain = new \Lcobucci\JWT\Signer\Keychain();
29+
$builder = new \Lcobucci\JWT\Builder();
30+
$token = $builder
31+
->setIssuer($issuer)
32+
->permittedFor($clientId)
33+
->set("sub", $clientId)
34+
->sign($signer, $keychain->getPrivateKey($privateKey))
35+
->getToken();
36+
return $token->__toString();
37+
}
38+
39+
public function generateIdToken($accessToken, $clientId, $subject, $nonce, $privateKey) {
40+
$issuer = $this->config->getServer()->get(OidcMeta::ISSUER);
41+
42+
$jwks = $this->getJwks();
43+
$tokenHash = $this->generateTokenHash($accessToken);
44+
45+
// Create JWT
46+
$signer = new \Lcobucci\JWT\Signer\Rsa\Sha256();
47+
$keychain = new \Lcobucci\JWT\Signer\Keychain();
48+
$builder = new \Lcobucci\JWT\Builder();
49+
$token = $builder
50+
->setIssuer($issuer)
51+
->permittedFor($clientId)
52+
->setIssuedAt(time())
53+
->setNotBefore(time() - 1)
54+
->setExpiration(time() + 14*24*60*60)
55+
->set("azp", $clientId)
56+
->set("sub", $subject)
57+
->set("jti", $this->generateJti())
58+
->set("nonce", $nonce)
59+
->set("at_hash", $tokenHash) //FIXME: at_hash should only be added if the response_type is a token
60+
->set("c_hash", $tokenHash) // FIXME: c_hash should only be added if the response_type is a code
61+
->set("cnf", array(
62+
"jwk" => $jwks['keys'][0]
63+
))
64+
->withHeader('kid', $jwks['keys'][0]['kid'])
65+
->sign($signer, $keychain->getPrivateKey($privateKey))
66+
->getToken();
67+
return $token->__toString();
68+
}
69+
70+
public function respondToRegistration($registration, $privateKey) {
71+
/*
72+
Expects in $registration:
73+
client_id
74+
client_id_issued_at
75+
redirect_uris
76+
registration_client_uri
77+
*/
78+
$registration_access_token = $this->generateRegistrationAccessToken($registration['client_id'], $privateKey);
79+
80+
$registrationBase = array(
81+
'response_types' => array("id_token token"),
82+
'grant_types' => array("implicit"),
83+
'application_type' => 'web',
84+
'id_token_signed_response_alg' => "RS256",
85+
'token_endpoint_auth_method' => 'client_secret_basic',
86+
'registration_access_token' => $registration_access_token,
87+
);
88+
89+
return array_merge($registrationBase, $registration);
90+
}
91+
92+
public function addIdTokenToResponse($response, $clientId, $subject, $nonce, $privateKey) {
93+
if ($response->hasHeader("Location")) {
94+
$value = $response->getHeaderLine("Location");
95+
if (preg_match("/#access_token=(.*?)&/", $value, $matches)) {
96+
$idToken = $this->generateIdToken(
97+
$matches[1],
98+
$clientId,
99+
$subject,
100+
$nonce,
101+
$privateKey
102+
);
103+
$value = preg_replace("/#access_token=(.*?)&/", "#access_token=\$1&id_token=$idToken&", $value);
104+
$response = $response->withHeader("Location", $value);
105+
} else if (preg_match("/code=(.*?)&/", $value, $matches)) {
106+
$idToken = $this->generateIdToken(
107+
$matches[1],
108+
$clientId,
109+
$subject,
110+
$nonce,
111+
$privateKey
112+
);
113+
$value = preg_replace("/code=(.*?)&/", "code=\$1&id_token=$idToken&", $value);
114+
$response = $response->withHeader("Location", value);
115+
}
116+
}
117+
return $response;
118+
}
119+
///////////////////////////// HELPER FUNCTIONS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
120+
121+
private function generateJti() {
122+
return substr(md5((string)time()), 12); // FIXME: generate unique jti values
123+
}
124+
125+
private function generateTokenHash($accessToken) {
126+
$atHash = hash('sha256', $accessToken);
127+
$atHash = substr($atHash, 0, 32);
128+
$atHash = hex2bin($atHash);
129+
$atHash = base64_encode($atHash);
130+
$atHash = rtrim($atHash, '=');
131+
$atHash = str_replace('/', '_', $atHash);
132+
$atHash = str_replace('+', '-', $atHash);
133+
134+
return $atHash;
135+
}
136+
137+
private function getJwks() {
138+
$key = $this->config->getKeys()->getPublicKey();
139+
$jwks = new Jwks($key);
140+
return json_decode($jwks->__toString(), true);
141+
}
142+
}

src/Utils/Jwks.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ private function createKey(string $certificate, $subject) : array
4747
JwkParameter::KEY_TYPE => 'RSA',
4848
RsaParameter::PUBLIC_EXPONENT => 'AQAB', // Hard-coded as `Base64Url::encode($keyInfo['rsa']['e'])` tends to be empty...
4949
RsaParameter::PUBLIC_MODULUS => Base64Url::encode($subject),
50+
JwkParameter::KEY_OPERATIONS => array("verify"),
5051
];
5152
}
5253

0 commit comments

Comments
 (0)