diff --git a/docs/Basics/Configuration-settings.md b/docs/Basics/Configuration-settings.md index 279798d..01a575e 100644 --- a/docs/Basics/Configuration-settings.md +++ b/docs/Basics/Configuration-settings.md @@ -27,7 +27,7 @@ Whether to use encryption for the file storage ## storageEncryptionKey -The encryption key to use +The encryption key (hexadecimal) to use **See also:** diff --git a/docs/Development/Additional-functionality.md b/docs/Development/Additional-functionality.md new file mode 100644 index 0000000..b9273c4 --- /dev/null +++ b/docs/Development/Additional-functionality.md @@ -0,0 +1,364 @@ +# Additional functionality + +Services may support additional features, such as token refresh or invalidation, among other things. You can add these features +by implementing one or more of the feature interfaces. Some of the methods for these interfaces are already implemented in the +abstract providers, so that you only rarely need to re-implement them. + + +## `UserInfo` + +The `UserInfo` interface implements a method `me()` that returns basic information about the currently authenticated user from a `/me`, +`/tokeninfo` or similar endpoint in a `AuthenticatedUser` instance. To ease implementation, the endpoint request including error handling +has been unified and condensed into a single ugly method that returns an array with the information available. +You only need to assign the available values and hand them over to `AuthenticatedUser`, which takes an array with the following elements: + +- `data`: the full response data array +- `handle`: a unique user handle +- `displayName`: the user's display- or full name +- `id`: a unique identifier, e.g. numeric or UUID - not to be confused with the handle +- `email`: the user's e-mail address +- `avatar`: a link to the avatar image +- `url`: a link to the public user profile + +All elements except for `data` are nullable. + +```php +class MyOAuth2Provider extends OAuth2Provider implements UserInfo{ + + /* + * ... + */ + + public function me():AuthenticatedUser{ + + // the endpoint can either be an absolute URL or a path relative to $apiURL + // additional request parameters can be supplied as an array + $params = ['param' => 'value']; + $data = $this->getMeResponseData('/v1/accounts/verify_credentials', $params); + + // assign the fields + $userdata = [ + 'data' => $data, + 'avatar' => $data['avatar_url'], + 'displayName' => $data['display_name'], + 'email' => $data['email'], + 'handle' => $data['username'], + 'id' => $data['id'], + 'url' => $data['profile_url'], + ]; + + // the values for AuthenticatedUser can only be assigned via constructor + return new AuthenticatedUser($userdata); + } + +} +``` + +Sometimes, the unified request method might not work, for example in case your extended provider class overrides the +`OAuthProvider::request()` method to add functionality that cannot be handled otherwise. +In that case you can override the method `OAuthProvider::sendMeRequest()`: + +```php +class MyOAuth2Provider extends OAuth2Provider implements UserInfo{ + + /* + * ... + */ + + protected function sendMeRequest( + string $endpoint, + array|null $params = null + ):ResponseInterface{ + return $this->request(path: $endpoint, params: $params); + } + +} +``` + + +## `ClientCredentials` (OAuth2) + +The `ClientCredentials` interface indicates that the provider supports the OAuth2 *Client Credentials Grant* as described in [RFC-6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). +This allows the creation of access tokens without user context via the method `ClientCredentials::getClientCredentialsToken()` that is already implemented in `OAuth2Provider`. +Similar to the user authorization request, an optional set of scopes can be supplied via the `$scopes` parameter. + + +```php +class MyOAuth2Provider extends OAuth2Provider implements ClientCredentials{ + + /* + * ... + */ + +} +``` + + +## `CSRFToken` (OAuth2) + +The `CSRFToken` interface indicates that the provider supports CSRF protection during the authorization request via the `state` query parameter, +as defined in [RFC-6749, section 10.12](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12). + +The (`final`) methods `CSRFToken::setState()` and `CSRFToken::checkState()` are implemented in `OAuth2Provider` and called in `getAuthorizationURL()` and `getAccessToken()`, respectively (user interaction in between). +If you need to re-implement one of the latter methods, don't forget to add the set/check! + +```php +class MyOAuth2Provider extends OAuth2Provider implements CSRFToken{ + + /* + * ... + */ + + public function getAccessToken(string $code, string|null $state = null):AccessToken{ + // we're an instance of CSRFToken, no instance check needed + $this->checkState($state); + + $body = $this->getAccessTokenRequestBodyParams($code); + $response = $this->sendAccessTokenRequest($this->accessTokenURL, $body); + $token = $this->parseTokenResponse($response); + + // do stuff... + $token->expires = (time() + 2592000); // set expiry to 30 days + + $this->storage->storeAccessToken($token, $this->name); + + return $token; + } + +} +``` + + +## `TokenRefresh` (OAuth2) + +This interface indicates that the provider class is capable of the OAuth2 token refresh, as described in [RFC-6749, section 6](https://datatracker.ietf.org/doc/html/rfc6749#section-6). +It shouldn't be necessary to re-implement the method `OAuth2Provider::refreshAccessToken()` unless the service you're about to implement interprets the RFC in very strange ways... + +The method is usually called in `OAuthInterface::getRequestAuthorization()`, which you might need to re-implement only in rare cases: + +```php +class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{ + + /* + * ... + */ + + public function getRequestAuthorization( + RequestInterface $request, + AccessToken|null $token = null, + ):RequestInterface{ + + // fetch the token from storage if none was given + $token ??= $this->storage->getAccessToken($this->name); + + // check whether the token is expired + if($token->isExpired()){ + + // throw if the token cannot be refreshed + if($this->options->tokenAutoRefresh !== true){ + throw new InvalidAccessTokenException; + } + + // call the token refresh + $token = $this->refreshAccessToken($token); + } + + $header = sprintf('%s %s', $this::AUTH_PREFIX_HEADER, $token->accessToken); + + return $request->withHeader('Authorization', $header); + } + +} +``` + + +## `PKCE` (OAuth2) + +The `PKCE` interface can be implemented when the service supports *"Proof Key for Code Exchange"* as described in [RFC-7636](https://datatracker.ietf.org/doc/html/rfc7636). +It implements the methods `PKCE::setCodeChallenge()` and `PKCE::setCodeVerifier()` that are called during the *authorization* and *access token* requests, respectively. + +If you need to override either of the aforementioned request methods, don't forget to add the PKCE parameters: + +```php +class MyOAuth2Provider extends OAuth2Provider implements PKCE{ + + /* + * ... + */ + + // the query parameters for the authorization URL + protected function getAuthorizationURLRequestParams(array $params, array $scopes):array{ + + $params = array_merge($params, [ + 'client_id' => $this->options->key, + 'redirect_uri' => $this->options->callbackURL, + 'response_type' => 'code', + 'type' => 'web_server', + // ... + ]); + + if(!empty($scopes)){ + $params['scope'] = implode($this::SCOPES_DELIMITER, $scopes); + } + + // set the CSRF token + $params = $this->setState($params); + + // set the PKCE "code_challenge" and "code_challenge_method" parameters + $params = $this->setCodeChallenge($params, PKCE::CHALLENGE_METHOD_S256); + + return $params; + } + + // the body for the access token exchange + protected function getAccessTokenRequestBodyParams(string $code):array{ + + $params = [ + 'client_id' => $this->options->key, + 'client_secret' => $this->options->secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->options->callbackURL, + // ... + ]; + + // sets the "code_verifier" parameter + $params = $this->setCodeVerifier($params); + + return $params; + } + +} +``` + + +## `PAR` (OAuth2) + +The `PAR` interface indicates support for *"Pushed Authorization Requests"* as described in [RFC-9126](https://datatracker.ietf.org/doc/html/rfc9126). +When this interface is implemented, the method `OAuth2Provider::getAuthorizationURL()` calls `PAR::getParRequestUri()` with the set of parameters from +`OAuth2Provider::getAuthorizationURLRequestParams()` and returns its result. The method `PAR::getParRequestUri()` sends the authorization parameters +to the PAR endpoint of the service and gets a temporary request URI in return, which is then used in the actual authorization URL to redirect the user. + +In case the service needs additional parameters in the final authorization URL, you can override the method `OAuth2Provider::getParAuthorizationURLRequestParams()`: + +```php +class MyOAuth2Provider extends OAuth2Provider implements PAR{ + + /* + * ... + */ + + protected function getParAuthorizationURLRequestParams(array $response):array{ + + if(!isset($response['request_uri'])){ + throw new ProviderException('PAR response error: "request_uri" missing'); + } + + return [ + 'client_id' => $this->options->key, + 'request_uri' => $response['request_uri'], + ]; + } + +} +``` + + +## `TokenInvalidate` + +This is interface is *not* implemented in the abstract providers, as it may differ drastically between services or is not supported at all. +The method `TokenInvalidate::invalidateAccessToken()` takes an `AccessToken` as optional parameter, in which case this token should be invalidated, +otherwise the token for the current user should be fetched from the storage and be used in the invalidation request. + +The more common implementation looks as follows: the access token along with client-id is sent with a `POST` request as url-encoded +form-data in the body, and the server responds with either a HTTP 200 and (often) an empty body or a HTTP 204. +On a successful response, the token should be deleted from the storage. + +```php +class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{ + + /* + * ... + */ + + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + $tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name)); + + // the body may vary between services + $bodyParams = [ + 'client_id' => $this->options->key, + 'token' => $tokenToInvalidate->accessToken, + ]; + + // prepare the request + $request = $this->requestFactory + ->createRequest('POST', $this->revokeURL) + ->withHeader('Content-Type', 'application/x-www-form-urlencoded') + ; + + // encode the body according to the content-type given in the request header + $request = $this->setRequestBody($bodyParams, $request); + + // bypass the host check and request authorization + $response = $this->http->sendRequest($request); + + if($response->getStatusCode() === 200){ + // delete the token on success (only if it wasn't given via param) + if($token === null){ + $this->storage->clearAccessToken($this->name); + } + + return true; + } + + return false; + } + +} +``` + +Other services may just expect a `POST` or `DELETE` request to the invalidation endpoint with the `Authorization: Bearer ` header set. +The problem here is that a token given via parameter can't just be revoked that easily without overwriting the currently stored token (if any). +This can be solved by simply cloning the current provider instance, feed the given token to the clone and call `invalidateAccessToken()` on it: + +```php +class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{ + + /* + * ... + */ + + public function invalidateAccessToken(AccessToken|null $token = null):bool{ + + // a token was given + if($token !== null){ + // clone the current provider instance + return (clone $this) + // replace the storage instance + ->setStorage(new MemoryStorage) + // store the given token in the clone + ->storeAccessToken($token) + // call this method on the clone without token parameter + ->invalidateAccessToken() + ; + } + + // prepare the request + $request = $this->requestFactory->createRequest('DELETE', $this->revokeURL); + $request = $this->getRequestAuthorization($request); + + // bypass the host check and request authorization + $response = $this->http->sendRequest($request); + + if($response->getStatusCode() === 204){ + // delete the token on success + $this->storage->clearAccessToken($this->name); + + return true; + } + + return false; + } + +} +``` diff --git a/docs/Development/Create-provider.md b/docs/Development/Create-provider.md index 8bfe882..fff09f5 100644 --- a/docs/Development/Create-provider.md +++ b/docs/Development/Create-provider.md @@ -1,4 +1,4 @@ -# Create your own Provider +# Create your own Provider class Thanks to clever abstraction, creating a new provider class is almost trivial; often it's only a few lines of code. You start by extending one of the abstract classes, depending on which OAuth version the service supports. @@ -8,14 +8,19 @@ e.g. [last.fm](https://www.last.fm/api/authentication), however, that requires i ## Minimal implementation -Absolutely necessary are the several endpoints for *authorization* and *access token* (as well as *request token* in case of OAuth1), the *API base URL* - -all of which you can find in the documentation of the service you're about to implement. With that, the bare minimum for a provider class looks as follows: +Absolutely necessary are the several endpoints for *authorization* and *access token* (as well as *request token* in case of OAuth1), +the *API base URL* - all of which you can find in the documentation of the service you're about to implement. +Further, an `IDENTIFIER` constant for use in tests, fetching environment variables etc., is required. + +With that, the bare minimum for a provider class looks as follows: **OAuth1** ```php class MyOAuth1Provider extends OAuth1Provider{ + public const IDENTIFIER = 'MYOAUTH1PROVIDER'; + protected string $requestTokenURL = 'https://example.com/oauth/request_token'; protected string $authorizationURL = 'https://example.com/oauth/authorize'; protected string $accessTokenURL = 'https://example.com/oauth/access_token'; @@ -29,6 +34,8 @@ class MyOAuth1Provider extends OAuth1Provider{ ```php class MyOAuth2Provider extends OAuth2Provider{ + public const IDENTIFIER = 'MYOAUTH2PROVIDER'; + protected string $authorizationURL = 'https://example.com/oauth2/authorize'; protected string $accessTokenURL = 'https://example.com/oauth2/token'; protected string $apiURL = 'https://example.com/api'; @@ -45,7 +52,7 @@ so let's continue expanding the class. ### Dynamic URLs Before you ask *"why aren't these constants?"* or *"why don't you implement abstract getter methods"*: some providers require dynamic URLs. -Take `Mastodon` for example: the URLs and API endpoints change with the host of the Mastodon instance - this would quickly become a mess with getter methods. +Take `Mastodon` for example: the URLs and API endpoints change with the host of the Mastodon instance - this would quickly become a mess with setter/getter methods. The `Mastodon` provider class implements a method `setInstance()` that takes the hostname of the instance and changes the internal URLs. ```php @@ -101,6 +108,7 @@ class MyOAuth2Provider extends OAuth2Provider{ /* * ... */ + public const USER_AGENT = 'myCoolOAuthClient/1.0.0 +https://github.com/my/oauth'; protected string|null $apiDocs = 'https://example.com/docs/api/reference'; @@ -138,42 +146,16 @@ class MyOAuth2Provider extends OAuth2Provider{ ``` -## Request authorization method (OAuth2) - -Unlike OAuth1 with the header `Authorization: OAuth `, OAuth2 allows for several different authorization methods for requests, -most commonly `Authorization: Bearer `, sometimes an URL query parameter `?access_token=` or something entirely different. - -The `OAuth2Interface` offers several constants that allow you to alter the behaviour of the request authorization: - -- `AUTH_METHOD`: specifies whether the token is passed via header (`AUTH_METHOD_HEADER`, default) or in the URL query (`AUTH_METHOD_QUERY`) -- `AUTH_PREFIX_HEADER`: the prefix for the value of the `Authorization` header, e.g. `Bearer` (default), `OAuth` or whatever the service accepts -- `AUTH_PREFIX_QUERY`: the name of the query parameter, e.g. `access_token` - -The following example would send the token via the URL query parameters, which would result in an URL similar to -`https://example.com/api/endpoint?param=value&here_is_the_access_token=` - - -```php -class MyOAuth2Provider extends OAuth2Provider{ - - /* - * ... - */ - - public const AUTH_METHOD = self::AUTH_METHOD_QUERY; - public const AUTH_PREFIX_QUERY = 'here_is_the_access_token'; - -} -``` - - -## Scopes (OAuth2) +## Scopes The scopes as described in [RFC-6749, section 3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3) can be added to an authorization request, here as an array via the optional `$scopes` parameter of `OAuthInterface::getAuthorizationURL()`. -The `OAuth2Interface` offers a constant `DEFAULT_SCOPES` that can be defined for cases when no scopes are given otherwise. +This interface also offers a constant `DEFAULT_SCOPES` that can be defined for cases when no scopes are given otherwise. Further, a `SCOPES_DELIMITER` can be defined in case it deviates from the `space` specified in the RFC. +Since some OAuth1-based services offer a similar granular permission system, the scopes implementation is not limited to OAuth2 provider classes. +All basic functionality is implemented with the `OAuthInterface` and can be used with any service. + You can and should add any scopes that the service supports as public constants in the form `SCOPE_` so that they can be accessed easily: ```php @@ -198,357 +180,52 @@ class MyOAuth2Provider extends OAuth2Provider{ ``` -## Overriding methods - -The abstract providers are implemented close to the RFCs, however, some services deviate from the proposals (e.g. different parameter names or content encodings), -so you may need to adjust your class. The respective methods are chopped up into small bits that make it easy to re-implement them. - -### OAuth1 - -- `OAuth1Provider::getRequestTokenRequestParams()` -- `OAuth1Provider::sendRequestTokenRequest()` -- `OAuth1Provider::sendAccessTokenRequest()` - -### OAuth2 - -- `OAuth2Provider::getAuthorizationURLRequestParams()` -- `OAuth2Provider::getAccessTokenRequestBodyParams()` -- `OAuth2Provider::sendAccessTokenRequest()` -- `OAuth2Provider::getTokenResponseData()` -- `OAuth2Provider::getClientCredentialsTokenRequestBodyParams()` -- `OAuth2Provider::sendClientCredentialsTokenRequest()` -- `OAuth2Provider::getRefreshAccessTokenRequestBodyParams()` - - -## Additional functionality - -Services may support additional features, such as token refresh or invalidation, among other things. You can add these features -by implementing one or more of the feature interfaces. Some of the methods for these interfaces are already implemented in the -abstract providers, so that you only rarely need to re-implement them. - - -### `UserInfo` - -The `UserInfo` interface implements a method `me()` that returns basic information about the currently authenticated user from a `/me`, -`/tokeninfo` or similar endpoint in a `AuthenticatedUser` instance. To ease implementation, the endpoint request including error handling -has been unified and condensed into a single ugly method that returns an array with the information available. -You only need to assign the available values and hand them over to `AuthenticatedUser`, which takes an array with the following elements: - -- `data`: the full response data array -- `handle`: a unique user handle -- `displayName`: the user's display- or full name -- `id`: a unique identifier, e.g. numeric or UUID - not to be confused with the handle -- `email`: the user's e-mail address -- `avatar`: a link to the avatar image -- `url`: a link to the public user profile - -All elements except for `data` are nullable. - -```php -class MyOAuth2Provider extends OAuth2Provider implements UserInfo{ - - /* - * ... - */ - - public function me():AuthenticatedUser{ - - // the endpoint can either be an absolute URL or a path relative to $apiURL - // additional request parameters can be supplied as an array - $params = ['param' => 'value']; - $data = $this->getMeResponseData('/v1/accounts/verify_credentials', $params); - - // assign the fields - $userdata = [ - 'data' => $data, - 'avatar' => $data['avatar_url'], - 'displayName' => $data['display_name'], - 'email' => $data['email'], - 'handle' => $data['username'], - 'id' => $data['id'], - 'url' => $data['profile_url'], - ]; - - // the values for AuthenticatedUser can only be assigned via constructor - return new AuthenticatedUser($userdata); - } - -} -``` - -Sometimes, the unified request method might not work, for example in case your extended provider class overrides the -`OAuthProvider::request()` method to add functionality that cannot be handled otherwise. -In that case you can override the method `OAuthProvider::sendMeRequest()`: - -```php -class MyOAuth2Provider extends OAuth2Provider implements UserInfo{ - - /* - * ... - */ - - protected function sendMeRequest( - string $endpoint, - array|null $params = null - ):ResponseInterface{ - return $this->request(path: $endpoint, params: $params); - } - -} -``` - - -### `ClientCredentials` (OAuth2) - -The `ClientCredentials` interface indicates that the provider supports the OAuth2 *Client Credentials Grant* as described in [RFC-6749, section 4.4](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4). -This allows the creation of access tokens without user context via the method `ClientCredentials::getClientCredentialsToken()` that is already implemented in `OAuth2Provider`. -Similar to the user authorization request, an optional set of scopes can be supplied via the `$scopes` parameter. - - -```php -class MyOAuth2Provider extends OAuth2Provider implements ClientCredentials{ - - /* - * ... - */ - -} -``` - - -### `CSRFToken` (OAuth2) - -The `CSRFToken` interface indicates that the provider supports CSRF protection during the authorization request via the `state` query parameter, -as defined in [RFC-6749, section 10.12](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12). - -The (`final`) methods `CSRFToken::setState()` and `CSRFToken::checkState()` are implemented in `OAuth2Provider` and called in `getAuthorizationURL()` and `getAccessToken()`, respectively (user interaction in between). -If you need to re-implement one of the latter methods, don't forget to add the set/check! - -```php -class MyOAuth2Provider extends OAuth2Provider implements CSRFToken{ - - /* - * ... - */ - - public function getAccessToken(string $code, string|null $state = null):AccessToken{ - // we're an instance of CSRFToken, no instance check needed - $this->checkState($state); - - $body = $this->getAccessTokenRequestBodyParams($code); - $response = $this->sendAccessTokenRequest($this->accessTokenURL, $body); - $token = $this->parseTokenResponse($response); - - // do stuff... - $token->expires = (time() + 2592000); // set expiry to 30 days - - $this->storage->storeAccessToken($token, $this->name); - - return $token; - } - -} -``` - - -### `TokenRefresh` (OAuth2) - -This interface indicates that the provider class is capable of the OAuth2 token refresh, as described in [RFC-6749, section 6](https://datatracker.ietf.org/doc/html/rfc6749#section-6). -It shouldn't be necessary to re-implement the method `OAuth2Provider::refreshAccessToken()` unless the service you're about to implement interprets the RFC in very strange ways... - -The method is usually called in `OAuthInterface::getRequestAuthorization()`, which you might need to re-implement only in rare cases: - -```php -class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{ - - /* - * ... - */ - - public function getRequestAuthorization( - RequestInterface $request, - AccessToken|null $token = null, - ):RequestInterface{ - - // fetch the token from storage if none was given - $token ??= $this->storage->getAccessToken($this->name); - - // check whether the token is expired - if($token->isExpired()){ - - // throw if the token cannot be refreshed - if($this->options->tokenAutoRefresh !== true){ - throw new InvalidAccessTokenException; - } - - // call the token refresh - $token = $this->refreshAccessToken($token); - } - - $header = sprintf('%s %s', $this::AUTH_PREFIX_HEADER, $token->accessToken); - - return $request->withHeader('Authorization', $header); - } - -} -``` - - -### `PKCE` (OAuth2) - -The `PKCE` interface can be implemented when the service supports *"Proof Key for Code Exchange"* as described in [RFC-7636](https://datatracker.ietf.org/doc/html/rfc7636). -It implements the methods `PKCE::setCodeChallenge()` and `PKCE::setCodeVerifier()` that are called during the *authorization* and *access token* requests, respectively. - -If you need to override either of the aforementioned request methods, don't forget to add the PKCE parameters: - -```php -class MyOAuth2Provider extends OAuth2Provider implements PKCE{ - - /* - * ... - */ - - // the query parameters for the authorization URL - protected function getAuthorizationURLRequestParams(array $params, array $scopes):array{ - - $params = array_merge($params, [ - 'client_id' => $this->options->key, - 'redirect_uri' => $this->options->callbackURL, - 'response_type' => 'code', - 'type' => 'web_server', - // ... - ]); - - if(!empty($scopes)){ - $params['scope'] = implode($this::SCOPES_DELIMITER, $scopes); - } - - // set the CSRF token - $params = $this->setState($params); - - // set the PKCE "code_challenge" and "code_challenge_method" parameters - $params = $this->setCodeChallenge($params, PKCE::CHALLENGE_METHOD_S256); - - return $params; - } - - // the body for the access token exchange - protected function getAccessTokenRequestBodyParams(string $code):array{ - - $params = [ - 'client_id' => $this->options->key, - 'client_secret' => $this->options->secret, - 'code' => $code, - 'grant_type' => 'authorization_code', - 'redirect_uri' => $this->options->callbackURL, - // ... - ]; - - // sets the "code_verifier" parameter - $params = $this->setCodeVerifier($params); - - return $params; - } +## Request authorization method (OAuth2) -} -``` +Unlike OAuth1 with the header `Authorization: OAuth `, OAuth2 allows for several different authorization methods for requests, +most commonly `Authorization: Bearer `, sometimes an URL query parameter `?access_token=` or something entirely different. +The `OAuth2Interface` offers several constants that allow you to alter the behaviour of the request authorization: -### `TokenInvalidate` +- `AUTH_METHOD`: specifies whether the token is passed via header (`AUTH_METHOD_HEADER`, default) or in the URL query (`AUTH_METHOD_QUERY`) +- `AUTH_PREFIX_HEADER`: the prefix for the value of the `Authorization` header, e.g. `Bearer` (default), `OAuth` or whatever the service accepts +- `AUTH_PREFIX_QUERY`: the name of the query parameter, e.g. `access_token` -This is interface is *not* implemented in the abstract providers, as it may differ drastically between services or is not supported at all. -The method `TokenInvalidate::invalidateAccessToken()` takes an `AccessToken` as optional parameter, in which case this token should be invalidated, -otherwise the token for the current user should be fetched from the storage and be used in the invalidation request. +The following example would send the token via the URL query parameters, which would result in an URL similar to +`https://example.com/api/endpoint?param=value&here_is_the_access_token=` -The more common implementation looks as follows: the access token along with client-id is sent with a `POST` request as url-encoded -form-data in the body, and the server responds with either a HTTP 200 and (often) an empty body or a HTTP 204. -On a successful response, the token should be deleted from the storage. ```php -class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{ +class MyOAuth2Provider extends OAuth2Provider{ /* * ... */ - public function invalidateAccessToken(AccessToken|null $token = null):bool{ - $tokenToInvalidate = ($token ?? $this->storage->getAccessToken($this->name)); - - // the body may vary between services - $bodyParams = [ - 'client_id' => $this->options->key, - 'token' => $tokenToInvalidate->accessToken, - ]; - - // prepare the request - $request = $this->requestFactory - ->createRequest('POST', $this->revokeURL) - ->withHeader('Content-Type', 'application/x-www-form-urlencoded') - ; - - // encode the body according to the content-type given in the request header - $request = $this->setRequestBody($bodyParams, $request); - - // bypass the host check and request authorization - $response = $this->http->sendRequest($request); - - if($response->getStatusCode() === 200){ - // delete the token on success (only if it wasn't given via param) - if($token === null){ - $this->storage->clearAccessToken($this->name); - } - - return true; - } - - return false; - } + public const AUTH_METHOD = self::AUTH_METHOD_QUERY; + public const AUTH_PREFIX_QUERY = 'here_is_the_access_token'; } ``` -Other services may just expect a `POST` or `DELETE` request to the invalidation endpoint with the `Authorization: Bearer ` header set. -The problem here is that a token given via parameter can't just be revoked that easily without overwriting the currently stored token (if any). -This can be solved by simply cloning the current provider instance, feed the given token to the clone and call `invalidateAccessToken()` on it: -```php -class MyOAuth2Provider extends OAuth2Provider implements TokenInvalidate{ - - /* - * ... - */ - - public function invalidateAccessToken(AccessToken|null $token = null):bool{ - - // a token was given - if($token !== null){ - // clone the current provider instance - return (clone $this) - // replace the storage instance - ->setStorage(new MemoryStorage) - // store the given token in the clone - ->storeAccessToken($token) - // call this method on the clone without token parameter - ->invalidateAccessToken() - ; - } - - // prepare the request - $request = $this->requestFactory->createRequest('DELETE', $this->revokeURL); - $request = $this->getRequestAuthorization($request); +## Overriding methods - // bypass the host check and request authorization - $response = $this->http->sendRequest($request); +The abstract providers are implemented close to the RFCs, however, some services deviate from the proposals (e.g. different parameter names or content encodings), +so you may need to adjust your class. The respective methods are chopped up into small bits that make it easy to re-implement them. - if($response->getStatusCode() === 204){ - // delete the token on success - $this->storage->clearAccessToken($this->name); +### OAuth1 - return true; - } +- `OAuth1Provider::getRequestTokenRequestParams()` +- `OAuth1Provider::sendRequestTokenRequest()` +- `OAuth1Provider::sendAccessTokenRequest()` - return false; - } +### OAuth2 -} -``` +- `OAuth2Provider::getAuthorizationURLRequestParams()` +- `OAuth2Provider::getAccessTokenRequestBodyParams()` +- `OAuth2Provider::sendAccessTokenRequest()` +- `OAuth2Provider::getTokenResponseData()` +- `OAuth2Provider::getClientCredentialsTokenRequestBodyParams()` +- `OAuth2Provider::sendClientCredentialsTokenRequest()` +- `OAuth2Provider::getRefreshAccessTokenRequestBodyParams()` diff --git a/docs/Readme.md b/docs/Readme.md index 6e77860..6879a79 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -66,11 +66,12 @@ The markdown sources for the [Read the Docs online manual](https://php-oauth.rea - [Quickstart](./Usage/Quickstart.md) - [Authorization flow](./Usage/Authorization.md) -- + ### Development - [Create a Provider](./Development/Create-provider.md) +- [Additional functionality](./Development/Additional-functionality.md) - [Running tests](./Development/Test-suite.md) - [Using the examples](./Development/Using-examples.md) diff --git a/docs/Usage/Authorization.md b/docs/Usage/Authorization.md index 21d9367..3bac4bd 100644 --- a/docs/Usage/Authorization.md +++ b/docs/Usage/Authorization.md @@ -21,7 +21,7 @@ echo 'connect with GitHub!'; The method `OAuthInterface::getAuthorizationURL()` takes two (optional) parameters: - `$params`: this array contains additional query parameters that will be added to the URL query (provider dependent) -- `$scopes`: this array contains all scopes that will be used for this authorization (unused in OAuth1) +- `$scopes`: this array contains all scopes that will be used for this authorization When the user clicks the log-in link, just execute a `header()` to the provider's authorization URL. @@ -47,7 +47,7 @@ while the similar `OAuth2Interface::getAccessToken()` takes two parameters `$cod ### OAuth2 -In our GitHub OAuth2 example we're now receiving the callback to `https://example.com/callback/?code=&state=`. +In our GitHub OAuth2 example we're now receiving the incoming callback to `https://example.com/callback/?code=&state=`. The `getAccessToken()` method initiates a backend request to the provider's server to exchange the temporary credentials for an access token: ```php @@ -63,9 +63,6 @@ if($route === 'oauth2-callback'){ $token = $provider->getAccessToken($_GET['code'], $state); - // some providers may send additional data with the callback query parameters, - // that you may want to save here along with the token - // when everything is done here, you should redirect the user to wherever // they were headed, but also to clear the URL query parameters header('Location: ...'); @@ -91,11 +88,17 @@ if($route === 'oauth1-callback'){ ); // ... + + header('Location: ...'); } } ``` +Some services may send additional data with the callback query parameters, that you might want to save along with the token after calling `getAccessToken()` - +alternatively you can override this method to add functionality. + + ## Use the provider's API After exchanging and saving the access token, you're now ready to access the provider's API on behalf of the authenticated user. diff --git a/docs/index.rst b/docs/index.rst index acf2ba6..9df6246 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ This work is licensed under the Creative Commons Attribution 4.0 International ( :caption: Development Development/Create-provider.md + Development/Additional-functionality.md Development/Test-suite.md Development/Using-examples.md