Skip to content

Commit

Permalink
Launch v0.2.1
Browse files Browse the repository at this point in the history
  • Loading branch information
gRegorLove committed Aug 6, 2022
2 parents d629585 + f3129cf commit ecec99d
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ProcessIndieAuth/vendor/mf2/mf2/tests/
vendor/mf2/mf2/tests/

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Added missing token-revocation-endpoint template
- Added `profile_name` and `profile_photo_url` fields to user template and editable profile
- Added support for clients requesting profile information

## [0.2.0] - 2022-07-04
### Changed
Expand Down
176 changes: 153 additions & 23 deletions ProcessIndieAuth.module.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static function getModuleInfo(): array
{
return [
'title' => 'IndieAuth',
'version' => '020',
'version' => '021',
'author' => 'gRegor Morrill, https://gregorlove.com/',
'summary' => 'Use your domain name as an IndieAuth provider',
'href' => 'https://indieauth.com/',
Expand Down Expand Up @@ -88,6 +88,11 @@ public function ___install(): void
$templates = file_get_contents(__DIR__ . '/data/templates.json');
$this->importTemplates($templates);

$fields = file_get_contents(__DIR__ . '/data/fields.json');
$this->importFields($fields);

$this->addProfileFields();

# attempt to set up the indieauth-metadata page
$endpoint = $this->pages->get('template=indieauth-metadata-endpoint');
if ($endpoint instanceof NullPage) {
Expand Down Expand Up @@ -145,7 +150,7 @@ public function ___install(): void

$this->regenerateTokenSecret();

$this->message('To complete installation, ensure the template files indieauth-metadata-endpoint.php, authorization-endpoint.php, and token-endpoint.php are put in the /site/templates/ directory.');
$this->message('To complete installation, please follow the Setup directions in the readme file.');
}

public function ___uninstall(): void
Expand Down Expand Up @@ -180,6 +185,15 @@ public function ___uninstall(): void
}
}

# attempt to un-publish the token-revocation-endpoint page
$endpoint = $this->pages->get('template=token-revocation-endpoint');
if (!($endpoint instanceof NullPage)) {
$endpoint->addStatus(Page::statusUnpublished);
if ($endpoint->save()) {
$this->message(sprintf('Unpublished page: %s', $endpoint->url));
}
}

$this->uninstallPage();
}

Expand Down Expand Up @@ -297,7 +311,7 @@ public function ___execute(): array
public function ___executeTokenSecret()
{
$this->regenerateTokenSecret();
$this->session->redirect($this->page->url, false);
$this->session->redirect($this->page->url, 302);
}

public function ___executeAuthorization(): array
Expand All @@ -308,7 +322,12 @@ public function ___executeAuthorization(): array

# missing part of the IndieAuth session
if (!($request && $client)) {
$this->session->redirect($this->wire('config')->urls->admin, false);
$this->session->redirect($this->wire('config')->urls->admin, 302);
}

if (!$this->user->hasRole('indieauth')) {
$this->message('Sorry, your account does not have the IndieAuth access role on this site.');
$this->session->redirect($this->config->urls->admin, 302);
}

if ($input->requestMethod('GET')) {
Expand All @@ -325,15 +344,7 @@ public function ___executeAuthorization(): array
);
}

$scopes = [];
if (array_key_exists('scope', $request)) {
$scopes = array_filter(
array_map(
'trim',
explode(' ', $request['scope'])
)
);
}
$scopes = $this->spaceSeparatedToArray($request['scope'] ?? '');

if (!$scopes) {
$this->headline('Authenticate');
Expand Down Expand Up @@ -370,8 +381,7 @@ public function ___executeAuthorization(): array
'iss' => $this->urls->httpRoot,
]);

$this->session->removeAllFor('IndieAuth');
$this->session->redirect($url, false);
$this->session->redirect($url, 302);
}
}

Expand Down Expand Up @@ -427,7 +437,7 @@ public function ___executeRevoke(): array
}

$this->message('Token has been revoked');
$this->session->redirect($this->page->url, false);
$this->session->redirect($this->page->url, 302);
}
}

Expand Down Expand Up @@ -578,15 +588,16 @@ public function authorizationEndpoint(): void

$this->session->setFor('IndieAuth', 'request', $request);
$this->session->setFor('IndieAuth', 'client', $client);
$this->session->setFor('IndieAuth', 'user_id', $this->user->id);

if ($user->isLoggedIn()) {
$moduleID = $this->modules->getModuleID($this);
$admin = $this->pages->get('process=' . $moduleID);
$this->session->redirect($admin->url . 'authorization', false);
$this->session->redirect($admin->url . 'authorization', 302);
}

# redirct to ProcessWire login
$this->session->redirect($this->wire('config')->urls->admin, false);
$this->session->redirect($this->wire('config')->urls->admin, 302);

} elseif ($input->requestMethod('POST')) {
$request = $input->post()->getArray();
Expand Down Expand Up @@ -797,6 +808,8 @@ private function redeemAuthorizationCode(array $request): void
$client_id = $decoded['client_id'] ?? '';
$scope = $decoded['scope'] ?? '';

$scopes = $this->spaceSeparatedToArray($decoded['scope'] ?? '');

# authorization_code has scope(s), generate an access token
if ($scope) {
$token_data = $this->addToken($decoded);
Expand All @@ -815,13 +828,24 @@ private function redeemAuthorizationCode(array $request): void
$token_data
);

if (in_array('profile', $scopes)) {
$response = $this->addProfileToResponse($response);
}

$this->session->removeAllFor('IndieAuth');
$this->log->save('indieauth', sprintf('Granted access token to %s with scope "%s"', $client_id, $scope));
$this->httpResponse($response, 200);
}

# authentication-only response
$me = $this->wire('urls')->httpRoot;
$response = compact('me');

if (in_array('profile', $scopes)) {
$response = $this->addProfileToResponse($response);
}

$this->session->removeAllFor('IndieAuth');
$this->log->save('indieauth', sprintf('Signed in to %s as %s', $request['client_id'], $me));
$this->httpResponse($response, 200);
}
Expand Down Expand Up @@ -970,6 +994,10 @@ private function verifyCode(array $request): array
}
}

# canonize URLs
$request['client_id'] = Server::canonizeUrl($request['client_id']);
$request['redirect_uri'] = Server::canonizeUrl($request['redirect_uri']);

# verify client_id
$original_client_id = $decoded['client_id'] ?? '';
if ($request['client_id'] !== $original_client_id) {
Expand Down Expand Up @@ -1007,7 +1035,7 @@ protected function loginSuccess(HookEvent $event): void

$moduleID = $this->modules->getModuleID($this);
$admin = $this->pages->get('process=' . $moduleID);
$this->session->redirect($admin->url . 'authorization', false);
$this->session->redirect($admin->url . 'authorization', 302);
}
}

Expand Down Expand Up @@ -1370,13 +1398,43 @@ private function cancelRequest(): void
'error_description' => 'authorization request cancelled by the user',
'state' => $request['state'],
]);
$this->session->redirect($url, false);
$this->session->redirect($url, 302);
}

/**
* When `profile` scope is requested, add profile to the response body
* If the user's `profile_name` is not set, profile will not be included
* in the response body.
*
* TODO: optionally add profile photo
* @see https://indieauth.spec.indieweb.org/#profile-information
*/
private function addProfileToResponse(array $response): array
{
$user_id = $this->session->getFor('IndieAuth', 'user_id');
$user = $this->users->get($user_id);

if (!$user->get('profile_name')) {
$this->log->save('indieauth', 'Missing profile name in user account');
return $response;
}

return array_merge(
$response,
[
'profile' => [
'name' => $user->get('profile_name'),
'url' => $this->wire('urls')->httpRoot,
'photo' => $user->get('profile_photo_url'),
],
]
);
}

private function redirectHttpResponse(string $redirect_uri, array $queryParams): void
{
$url = Server::buildUrlWithQueryString($redirect_uri, $queryParams);
$this->session->redirect($url, false);
$this->session->redirect($url, 302);
}

private function httpResponse($response, int $http_status = 400, array $headers = []): void
Expand Down Expand Up @@ -1452,6 +1510,20 @@ private function getClientInfo(string $url): array
return $info;
}

private function spaceSeparatedToArray(string $input): array
{
if (!$input) {
return [];
}

return array_filter(
array_map(
'trim',
explode(' ', $input)
)
);
}

private function secondsToString(int $input): string
{
$minute = 60;
Expand Down Expand Up @@ -1483,7 +1555,6 @@ private function secondsToString(int $input): string
* Perform a redirect with an error message
* @param string $message
* @param string $url
* @access public
*/
private function handleErrorRedirect(string $message = '', string $url = ''): void
{
Expand All @@ -1496,7 +1567,7 @@ private function handleErrorRedirect(string $message = '', string $url = ''): vo
}

$this->error($message);
$this->session->redirect($url, false);
$this->session->redirect($url, 302);
}

private function regenerateTokenSecret(): void
Expand Down Expand Up @@ -1547,11 +1618,70 @@ private function importTemplates($json): void
}
}

/**
* Import templates from JSON or array
* @param string|array $json
* @see https://processwire.com/talk/topic/9007-utility-to-help-generate-module-install-function/?do=findComment&comment=86995
*/
private function importFields($json): void
{
$data = is_array($json) ? $json : wireDecodeJSON($json);

foreach ($data as $name => $field_data) {
# get rid of the ID so it doesn't conflict
unset($field_data['id']);

# field doesn't exist already; create it
if (!$this->fields->get($name)) {
$field = new Field();
$field->name = $name;

# import the data for the field
$field->setImportData($field_data);
$field->save();

$this->message(sprintf('Added field: %s', $name));
} else {
$this->message(sprintf('Skipped existing field: %s', $name));
}
}
}

private function addProfileFields(): void
{
# attempt to add profile fields to user template
$fieldgroup = $this->templates->get('user')->fieldgroup;
$fields_to_add = [
'profile_name',
'profile_photo_url',
];

foreach ($fields_to_add as $name) {
if (!$fieldgroup->has($name)) {
$fieldgroup->add($name);
if ($fieldgroup->save()) {
$this->message(sprintf('Added field: %s', $name));
}
}
}

# attempt to make new profile fields editable by user
$profileFields = $this->modules->get('ProcessProfile')->profileFields;
if ($missing_fields = array_diff($fields_to_add, $profileFields)) {
foreach ($missing_fields as $name) {
$profileFields[] = $name;
}
$this->modules->saveConfig('ProcessProfile', compact('profileFields'));
$this->message('Updated user profile fields', Notice::debug);
}
}

private function installRole(): void
{
$role = $this->roles->get('indieauth');
if ($role instanceof NullPage) {
$this->roles->add('indieauth');
$this->message('Added role: indieauth');
}
}

Expand Down
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ IndieAuth is an identity layer on top of OAuth 2.0. It can be used to obtain acc
* ProcessWire 3

## Installation
* Create directory `/site/modules/ProcessIndieAuth`
* Upload the plugin files to that directory
* Install the module from the ProcessWire admin
The recommended method is to use the ProcessWire admin area’s module interface. If you prefer to install manually, see below.

Navigate to Modules > New. In the Module Class Name field, enter `ProcessIndieAuth`.

Continue with the [Setup](#setup) steps.

## Setup
* Copy the template files from `/extras/templates` into your `/site/templates` directory
* Verify that the plugin installed pages:
* IndieAuth Metadata Endpoint
Expand All @@ -37,6 +41,15 @@ This should result in three `<link>` elements in the source HTML:
</head>
```

## Installation from Github
If you prefer to manually install:

* Create directory `/site/modules/ProcessIndieAuth`
* Upload the plugin files to that directory
* Install the module from the ProcessWire admin

Continue with the [Setup](#setup) steps.

## Changelog
* [Changelog](CHANGELOG.md)

Expand Down
Loading

0 comments on commit ecec99d

Please sign in to comment.