|
| 1 | +<?php |
| 2 | + |
| 3 | +// ============================================================================= |
| 4 | +// The majority of this part of the code is usually handled by your framework |
| 5 | +// ----------------------------------------------------------------------------- |
| 6 | +session_start(); |
| 7 | +ob_start(); |
| 8 | + |
| 9 | +require_once __DIR__ . '/../vendor/autoload.php'; |
| 10 | + |
| 11 | +/*/ The PSR Request and Response objects are usually provided by your framework /*/ |
| 12 | +$request = \Laminas\Diactoros\ServerRequestFactory::fromGlobals($_SERVER, $_GET, $_POST, $_COOKIE, $_FILES); |
| 13 | +$response = new \Laminas\Diactoros\Response(); |
| 14 | + |
| 15 | +/*/ The User (ID) is usually also provided by an entity in your framework /*/ |
| 16 | +$userId = $_SESSION['user_id'] ?? ''; |
| 17 | + |
| 18 | +/*/ An identifier for the requesting client is needed to ask your framework for information /*/ |
| 19 | +$clientIdentifier = \array_key_exists(\Pdsinterop\Solid\Auth\Enum\OAuth2\Parameter::CLIENT_ID, |
| 20 | + $request->getQueryParams()) |
| 21 | + ? $request->getQueryParams()[\Pdsinterop\Solid\Auth\Enum\OAuth2\Parameter::CLIENT_ID] |
| 22 | + : ''; |
| 23 | + |
| 24 | +/*/ These should come from a database, based on $clientIdentifier |
| 25 | + * |
| 26 | + * They have previously been provided to or by the Client, using a Dynamic |
| 27 | + * Registration request. |
| 28 | +/*/ |
| 29 | +$clientName = 'Example Client Name'; |
| 30 | +$clientRedirectUris = [ |
| 31 | + 'https://server/client/redirect-url', |
| 32 | + 'https://server/client/another-redirect-url', |
| 33 | +]; |
| 34 | +$clientSecret = 'client secret'; |
| 35 | +// ============================================================================= |
| 36 | + |
| 37 | + |
| 38 | +// ============================================================================= |
| 39 | +// Create the Authorization Server |
| 40 | +// ----------------------------------------------------------------------------- |
| 41 | +$keyPath = dirname(__DIR__) . '/tests/fixtures/keys'; |
| 42 | +$encryptionKey = file_get_contents($keyPath . '/encryption.key'); |
| 43 | +$privateKey = file_get_contents($keyPath . '/private.key'); |
| 44 | +$publicKey = file_get_contents($keyPath . '/public.key'); |
| 45 | + |
| 46 | +$config = (new \Pdsinterop\Solid\Auth\Factory\ConfigFactory( |
| 47 | + new \Pdsinterop\Solid\Auth\Config\Client( |
| 48 | + $clientIdentifier, |
| 49 | + $clientSecret, |
| 50 | + $clientRedirectUris, |
| 51 | + $clientName |
| 52 | + ), |
| 53 | + $encryptionKey,$privateKey, $publicKey, |
| 54 | + [ |
| 55 | + /* URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core]. */ |
| 56 | + \Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::AUTHORIZATION_ENDPOINT => 'https://server/authorize', |
| 57 | + |
| 58 | + /* URL using the https scheme with no query or fragment component that |
| 59 | + * the OP asserts as its Issuer Identifier. If Issuer discovery is |
| 60 | + * supported, this value MUST be identical to the issuer value returned |
| 61 | + * by WebFinger. This also MUST be identical to the iss Claim value in |
| 62 | + * ID Tokens issued from this Issuer. |
| 63 | + */ |
| 64 | + \Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::ISSUER => 'https://server/identifier', |
| 65 | + |
| 66 | + /* URL of the OP's JSON Web Key Set [JWK] document. This contains the |
| 67 | + * signing key(s) the RP uses to validate signatures from the OP. The |
| 68 | + * JWK Set MAY also contain the Server's encryption key(s), which are |
| 69 | + * used by RPs to encrypt requests to the Server. |
| 70 | + * |
| 71 | + * When both signing and encryption keys are made available, a use |
| 72 | + * (Key Use) parameter value is REQUIRED for all keys in the referenced |
| 73 | + * JWK Set to indicate each key's intended usage. Although some |
| 74 | + * algorithms allow the same key to be used for both signatures and |
| 75 | + * encryption, doing so is NOT RECOMMENDED, as it is less secure. |
| 76 | + * |
| 77 | + * The JWK x5c parameter MAY be used to provide X.509 representations |
| 78 | + * of keys provided. When used, the bare key values MUST still be |
| 79 | + * present and MUST match those in the certificate. |
| 80 | + */ |
| 81 | + \Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::JWKS_URI => 'https://server/.well-known/jwks.json' |
| 82 | + ] |
| 83 | +))->create(); |
| 84 | + |
| 85 | +$authorizationServer = (new \Pdsinterop\Solid\Auth\Factory\AuthorizationServerFactory($config))->create(); |
| 86 | + |
| 87 | +$user = null; |
| 88 | +if ($userId !== '') { |
| 89 | + $user = new \Pdsinterop\Solid\Auth\Entity\User(); |
| 90 | + $user->setIdentifier($userId); |
| 91 | +} |
| 92 | + |
| 93 | +$server = new \Pdsinterop\Solid\Auth\Server($authorizationServer, $config, $response); |
| 94 | +// ============================================================================= |
| 95 | + |
| 96 | + |
| 97 | +// ============================================================================= |
| 98 | +// Handle requests |
| 99 | +// ----------------------------------------------------------------------------- |
| 100 | +switch ($request->getMethod() . $request->getRequestTarget()) { |
| 101 | + case 'GET/.well-known/jwks.json': |
| 102 | + $response = $server->respondToJwksMetadataRequest(); |
| 103 | + break; |
| 104 | + |
| 105 | + case 'GET/.well-known/openid-configuration': |
| 106 | + $response = $server->respondToOpenIdMetadataRequest(); |
| 107 | + break; |
| 108 | + |
| 109 | + case 'POST/access_token': |
| 110 | + $response = $server->respondToAccessTokenRequest($request); |
| 111 | + break; |
| 112 | + |
| 113 | + case 'GET/authorize': |
| 114 | + case 'POST/authorize': |
| 115 | + /*/ |
| 116 | + * The HTTP request is validate on every call. |
| 117 | + * |
| 118 | + * There are three steps to the Authorization request/response cycle: |
| 119 | + * |
| 120 | + * 1. Redirect the user to a login endpoint |
| 121 | + * - The user logs in |
| 122 | + * |
| 123 | + * 2. Redirect the user to an authorization page |
| 124 | + * - The user gives authorization to a client for certain scopes |
| 125 | + * |
| 126 | + * 3. Redirect the user to the URL provided by the Client |
| 127 | + * - The user is returned to the client |
| 128 | + * |
| 129 | + * The returned response depends on the given parameters. |
| 130 | + * |
| 131 | + * A callback can be given to receive the AuthorizationRequest, for |
| 132 | + * instance to saves the serialized object into the user's session or |
| 133 | + * to read/compare state, scope, or other values. |
| 134 | + * |
| 135 | + * Please note that this callback is called _after_ any logic that runs |
| 136 | + * to create the response |
| 137 | + /*/ |
| 138 | + $callback = static function (\League\OAuth2\Server\RequestTypes\AuthorizationRequest $authRequest) { |
| 139 | + if (empty($_SESSION['authRequest'])) { |
| 140 | + $_SESSION['authRequest'] = serialize($authRequest); |
| 141 | + } |
| 142 | + |
| 143 | + /** @var \League\OAuth2\Server\RequestTypes\AuthorizationRequest $sessionAuthRequest */ |
| 144 | + $sessionAuthRequest = unserialize($_SESSION['authRequest'], \League\OAuth2\Server\RequestTypes\AuthorizationRequest::class); |
| 145 | + |
| 146 | + if ($authRequest->getState() !== $sessionAuthRequest->getState()) { |
| 147 | + throw new \UnexpectedValueException('Auth state does not match session state!'); |
| 148 | + } |
| 149 | + }; |
| 150 | + |
| 151 | + /*/ Step 1: The user is redirected to a login endpoint |
| 152 | + * |
| 153 | + * As the user is not yet logged in, no $user Entity object is provided. |
| 154 | + * |
| 155 | + * As the user has not yet approved (or denied) the Authorization |
| 156 | + * request, no approval status is given. |
| 157 | + * |
| 158 | + * $response = $server->respondToAuthorizationRequest($request, null, null, $callback); |
| 159 | + /*/ |
| 160 | + |
| 161 | + /*/ Step 2: The user is redirected to an authorization page |
| 162 | + * |
| 163 | + * As the user is now logged in, a $user Entity object is provided. |
| 164 | + * |
| 165 | + * As the user has not yet approved (or denied) the Authorization |
| 166 | + * request, no approval status is given. |
| 167 | + * |
| 168 | + * $response = $server->respondToAuthorizationRequest($request, $user, null, $callback); |
| 169 | + /*/ |
| 170 | + |
| 171 | + /*/ Step 3: The user is redirected to the URL provided by the Client |
| 172 | + * |
| 173 | + * As the user is now logged in, a $user Entity object _can_ be provided. |
| 174 | + * |
| 175 | + * As the user has now approved (or denied) the Authorization request, |
| 176 | + * an approval status is given. |
| 177 | + * |
| 178 | + * This is usually the user's response to a form asking them to approve |
| 179 | + * scopes requested by the client. If previous consent has been given, |
| 180 | + * this response _may_ come from the database without asking the user |
| 181 | + * again, in which case the form can be skipped completely. |
| 182 | + * |
| 183 | + * If other scopes are requested than those that have been stored, the |
| 184 | + * user will have to be asked to expand their permission to include the |
| 185 | + * new scopes. |
| 186 | + * |
| 187 | + * $approval = Pdsinterop\Solid\Auth\Enum\Authorization::DENIED || $approval = Pdsinterop\Solid\Auth\Enum\Authorization::APPROVED; |
| 188 | + * $response = $server->respondToAuthorizationRequest($request, null, $approval, $callback); |
| 189 | + /*/ |
| 190 | + $approval = null; // <-- Change this in this example to emulate a user approving (or denying) the request |
| 191 | + $response = $server->respondToAuthorizationRequest($request, $user, $approval, $callback); |
| 192 | + break; |
| 193 | + |
| 194 | + case 'GET/.well-known/jwks.json': |
| 195 | + $response = $server->respondToJwksRequest(); |
| 196 | + break; |
| 197 | + |
| 198 | + default: |
| 199 | + $response->getBody()->write('404'); |
| 200 | + $response = $response->withStatus(404); |
| 201 | + break; |
| 202 | +} |
| 203 | +// ============================================================================= |
| 204 | + |
| 205 | + |
| 206 | +// ============================================================================= |
| 207 | +// Handling the response is usually also handled by your framework |
| 208 | +// ----------------------------------------------------------------------------- |
| 209 | +foreach ($response->getHeaders() as $name => $values) { |
| 210 | + foreach ($values as $value) { |
| 211 | + header(sprintf('%s: %s', $name, $value), false); |
| 212 | + } |
| 213 | +} |
| 214 | + |
| 215 | +echo (string) $response->getBody(); |
| 216 | +exit; |
| 217 | +// ============================================================================= |
0 commit comments