aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2022-07-15 11:47:35 +0000
committerMarcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de>2022-07-15 11:47:35 +0000
commit55852ef4819e5eafce9ae53dc4de2d84cdad1778 (patch)
tree9aedcdf89f416a7936f7df80da339a537082b5d5 /app
parenta9585dad3547a4ebbadd00f44065f95017d18684 (diff)
StEP-366: Add OAuth2 support to Stud.IP
Closes #1035 and #1198 Merge request studip/studip!635
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/oauth2.php46
-rw-r--r--app/controllers/api/oauth2/applications.php101
-rw-r--r--app/controllers/api/oauth2/authorize.php175
-rw-r--r--app/controllers/api/oauth2/clients.php166
-rw-r--r--app/controllers/api/oauth2/oauth2_controller.php52
-rw-r--r--app/controllers/api/oauth2/token.php29
-rw-r--r--app/views/admin/oauth2/_clients.php79
-rw-r--r--app/views/admin/oauth2/_notices.php10
-rw-r--r--app/views/admin/oauth2/_setup.php17
-rw-r--r--app/views/admin/oauth2/_setup_key.php28
-rw-r--r--app/views/admin/oauth2/index.php14
-rw-r--r--app/views/api/oauth/authorize.php11
-rw-r--r--app/views/api/oauth2/applications/details.php24
-rw-r--r--app/views/api/oauth2/applications/index.php52
-rw-r--r--app/views/api/oauth2/authorize.php60
-rw-r--r--app/views/api/oauth2/clients/add.php86
16 files changed, 945 insertions, 5 deletions
diff --git a/app/controllers/admin/oauth2.php b/app/controllers/admin/oauth2.php
new file mode 100644
index 0000000..ae4f5f9
--- /dev/null
+++ b/app/controllers/admin/oauth2.php
@@ -0,0 +1,46 @@
+<?php
+
+use Studip\OAuth2\Container;
+use Studip\OAuth2\Models\Client;
+use Studip\OAuth2\SetupInformation;
+
+class Admin_Oauth2Controller extends AuthenticatedController
+{
+ /**
+ * @param string $action
+ * @param string[] $args
+ *
+ * @return void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ $GLOBALS['perm']->check('root');
+
+ Navigation::activateItem('/admin/config/oauth2');
+ PageLayout::setTitle(_('OAuth2 Verwaltung'));
+
+ $this->types = [
+ 'website' => _('Website'),
+ 'desktop' => _('Herkömmliches Desktopprogramm'),
+ 'mobile' => _('Mobile App'),
+ ];
+
+ // Sidebar
+ $views = new ViewsWidget();
+ $views->addLink(
+ _('Übersicht'),
+ $this->indexURL()
+ )->setActive($action === 'index');
+ Sidebar::get()->addWidget($views);
+
+ $this->container = new Container();
+ }
+
+ public function index_action(): void
+ {
+ $this->setup = $this->container->get(SetupInformation::class);
+ $this->clients = Client::findBySql('1 ORDER BY chdate DESC');
+ }
+}
diff --git a/app/controllers/api/oauth2/applications.php b/app/controllers/api/oauth2/applications.php
new file mode 100644
index 0000000..d08ec1e
--- /dev/null
+++ b/app/controllers/api/oauth2/applications.php
@@ -0,0 +1,101 @@
+<?php
+use Studip\OAuth2\Models\AccessToken;
+use Studip\OAuth2\Models\Scope;
+
+/**
+ * @property array $applications
+ */
+class Api_Oauth2_ApplicationsController extends AuthenticatedController
+{
+ public function index_action(): void
+ {
+ Navigation::activateItem('/profile/settings/oauth2');
+ PageLayout::setTitle(_('Autorisierte Drittanwendungen'));
+ Helpbar::get()->addPlainText(
+ _('Autorisierte Drittanwendungen'),
+ _("Sie können Ihren Stud.IP-Zugang über OAuth mit Anwendungen von Drittanbietern verbinden.\n\nWenn Sie eine OAuth-App autorisieren, sollten Sie sicherstellen, dass Sie der Anwendung vertrauen, überprüfen, wer sie entwickelt hat, und die Art der Informationen überprüfen, auf die die Anwendung zugreifen möchte.")
+ );
+
+ $user = User::findCurrent();
+ $this->applications = $this->getApplications($user);
+ }
+
+ public function details_action(AccessToken $accessToken): void
+ {
+ $user = User::findCurrent();
+ if ($accessToken['user_id'] !== $user->id) {
+ throw new AccessDeniedException();
+ }
+
+ PageLayout::setTitle(_('Autorisierte OAuth2-Drittanwendung'));
+ $this->application = $this->formatApplication($accessToken);
+
+ if (!$this->application) {
+ throw new Trails_Exception(500, 'Error finding client.');
+ }
+ }
+
+ public function revoke_action(): void
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $user = User::findCurrent();
+ $accessToken = AccessToken::find(Request::option('application'));
+ if (!$accessToken) {
+ throw new Trails_Exception(404);
+ }
+ if ($accessToken['user_id'] !== $user->id) {
+ throw new AccessDeniedException();
+ }
+
+ $accessToken->revoke();
+
+ $this->redirect('api/oauth2/applications');
+ }
+
+ private function getApplications(User $user): array
+ {
+ return array_reduce(
+ AccessToken::findValidTokens($user),
+ function ($applications, $accessToken) {
+ $application = $this->formatApplication($accessToken);
+ if ($application) {
+ $applications[] = $application;
+ }
+
+ return $applications;
+ },
+ []
+ );
+ }
+
+ private function formatApplication(AccessToken $accessToken): ?array
+ {
+ $allScopes = Scope::scopes();
+
+ if (!$accessToken->client) {
+ return null;
+ }
+
+ return [
+ 'id' => $accessToken['id'],
+ 'name' => $accessToken->client['name'],
+ 'description' => $accessToken->client['description'],
+ 'owner' => $accessToken->client['owner'],
+ 'homepage' => $accessToken->client['homepage'],
+ 'created' => new DateTime('@' . $accessToken->client['mkdate']),
+
+ 'scopes' => array_reduce(
+ json_decode($accessToken['scopes']),
+ function ($scopes, $scopeIdentifier) use ($allScopes) {
+ if (isset($allScopes[$scopeIdentifier])) {
+ $scopes[] = $allScopes[$scopeIdentifier];
+ }
+
+ return $scopes;
+ },
+ []
+ )
+ ];
+ }
+}
diff --git a/app/controllers/api/oauth2/authorize.php b/app/controllers/api/oauth2/authorize.php
new file mode 100644
index 0000000..5628d49
--- /dev/null
+++ b/app/controllers/api/oauth2/authorize.php
@@ -0,0 +1,175 @@
+<?php
+require_once __DIR__ . '/oauth2_controller.php';
+
+use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
+use Studip\OAuth2\Bridge\UserEntity;
+use Studip\OAuth2\Exceptions\InvalidAuthTokenException;
+use Studip\OAuth2\Models\Scope;
+
+class Api_Oauth2_AuthorizeController extends OAuth2Controller
+{
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ if ('index' !== $action) {
+ throw new Trails_Exception(404);
+ }
+
+ $action = $this->determineAction();
+ }
+
+ private function determineAction(): string
+ {
+ $method = $this->getMethod();
+
+ if (Request::submitted('auth_token')) {
+ $GLOBALS['auth']->login_if('nobody' === $GLOBALS['user']->id);
+ CSRFProtection::verifyUnsafeRequest();
+
+ switch ($method) {
+ case 'POST':
+ return 'approved';
+
+ case 'DELETE':
+ return 'denied';
+ }
+ }
+
+ return 'authorize';
+ }
+
+ public function authorize_action(): void
+ {
+ $psrRequest = $this->getPsrRequest();
+ $authRequest = $this->server->validateAuthorizationRequest($psrRequest);
+
+ $scopes = $authRequest->getScopes();
+ $client = $authRequest->getClient();
+
+ $authToken = randomString(32);
+ $this->freezeSessionVars($authRequest, $authToken);
+
+ // show login form if not logged in
+ $authPlugin = Config::get()->getValue('API_OAUTH_AUTH_PLUGIN');
+ if ('nobody' === $GLOBALS['user']->id && 'Standard' !== $authPlugin && !Request::option('sso')) {
+ $queryParams = $psrRequest->getQueryParams();
+ $queryParams['sso'] = strtolower($authPlugin);
+ $this->redirect($this->authorizeURL($queryParams));
+
+ return;
+ } else {
+ $GLOBALS['auth']->login_if('nobody' === $GLOBALS['user']->id);
+ }
+
+ $this->client = $client;
+ $this->user = $GLOBALS['user'];
+ $this->scopes = $this->scopesFor($scopes);
+ $this->authToken = $authToken;
+ $this->state = $authRequest->getState();
+
+ PageLayout::disableHeader();
+ $this->render_template(
+ 'api/oauth2/authorize.php',
+ $GLOBALS['template_factory']->open('layouts/base.php')
+ );
+ }
+
+ public function approved_action(): void
+ {
+ [$authRequest, $authToken] = $this->thawSessionVars();
+ $this->assertValidAuthToken($authToken);
+
+ $authRequest->setUser(new UserEntity($GLOBALS['user']->id));
+ $authRequest->setAuthorizationApproved(true);
+
+ $response = $this->server->completeAuthorizationRequest($authRequest, $this->getPsrResponse());
+
+ $this->renderPsrResponse($response);
+ }
+
+ public function denied_action(): void
+ {
+ [$authRequest, $authToken] = $this->thawSessionVars();
+ $this->assertValidAuthToken($authToken);
+
+ $authRequest->setUser(new UserEntity($GLOBALS['user']->id));
+ $authRequest->setAuthorizationApproved(false);
+
+ $clientUris = $authRequest->getClient()->getRedirectUri();
+
+ $uri = $authRequest->getRedirectUri();
+ if (!in_array($uri, $clientUris)) {
+ $uri = current($clientUris);
+ }
+
+ $uri = URLHelper::getURL($uri, [
+ 'error' => 'access_denied',
+ 'state' => Request::get('state'),
+ ], true);
+ $this->redirect($uri);
+ }
+
+ private function getMethod(): string
+ {
+ $method = Request::method();
+ if ('POST' === $method && Request::submitted('_method')) {
+ $_method = strtoupper(Request::get('_method'));
+ if (in_array($_method, ['DELETE', 'PATCH', 'PUT'])) {
+ $method = $_method;
+ }
+ }
+
+ return $method;
+ }
+
+ /**
+ * Make sure the auth token matches the one in the session.
+ *
+ * @throws InvalidAuthTokenException
+ */
+ private function assertValidAuthToken(string $authToken): void
+ {
+ if (Request::submitted('auth_token') && $authToken !== Request::get('auth_token')) {
+ throw InvalidAuthTokenException::different();
+ }
+ }
+
+ private function freezeSessionVars(AuthorizationRequest $authRequest, string $authToken): void
+ {
+ $_SESSION['oauth2'] = [
+ 'authRequest' => serialize($authRequest),
+ 'authToken' => $authToken,
+ ];
+ }
+
+ private function thawSessionVars(): array
+ {
+ $authRequest = null;
+ $authToken = null;
+ if (
+ isset($_SESSION['oauth2']) &&
+ is_array($_SESSION['oauth2']) &&
+ isset($_SESSION['oauth2']['authRequest']) &&
+ isset($_SESSION['oauth2']['authToken'])
+ ) {
+ $authRequest = unserialize($_SESSION['oauth2']['authRequest']);
+ $authToken = $_SESSION['oauth2']['authToken'];
+ }
+
+ return [$authRequest, $authToken];
+ }
+
+ private function scopesFor(array $scopeEntities): array
+ {
+ $scopes = Scope::scopes();
+ $scopeModels = [];
+ foreach ($scopeEntities as $scopeEntity) {
+ if (isset($scopes[$scopeEntity->getIdentifier()])) {
+ $scopeModels[] = $scopes[$scopeEntity->getIdentifier()];
+ }
+ }
+
+ return $scopeModels;
+ }
+}
diff --git a/app/controllers/api/oauth2/clients.php b/app/controllers/api/oauth2/clients.php
new file mode 100644
index 0000000..c16b67d
--- /dev/null
+++ b/app/controllers/api/oauth2/clients.php
@@ -0,0 +1,166 @@
+<?php
+
+use Studip\OAuth2\Models\Client;
+
+class Api_Oauth2_ClientsController extends AuthenticatedController
+{
+ /**
+ * @param string $action
+ * @param string[] $args
+ *
+ * @return void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ $GLOBALS['perm']->check('root');
+ }
+
+ public function add_action(): void
+ {
+ Navigation::activateItem('/admin/config/oauth2');
+ PageLayout::setTitle(_('OAuth2-Client hinzufügen'));
+ }
+
+ public function store_action(): void
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $this->redirect('admin/oauth2');
+
+ list($valid, $data, $errors) = $this->validateCreateClientRequest();
+
+ if (!$valid) {
+ PageLayout::postError(_('Das Erstellen eines OAuth2-Clients war nicht erfolgreich.'), $errors);
+ return;
+ }
+
+ $client = $this->createAuthCodeClient($data);
+ $this->outputClientCredentials($client);
+ }
+
+ public function delete_action(Client $client): void
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $clientId = $client['id'];
+ $clientName = $client['name'];
+ $client->delete();
+
+ PageLayout::postSuccess(sprintf(_('Der OAuth2-Client #%d ("%s") wurde gelöscht.'), $clientId, $clientName));
+ $this->redirect('admin/oauth2');
+ }
+
+ /**
+ * Create a authorization code client.
+ *
+ * @param array<string, mixed>
+ */
+ private function createAuthCodeClient(array $data): Client
+ {
+ return Client::createClient(
+ $data['name'],
+ $data['redirect'],
+ $data['confidential'],
+ $data['owner'],
+ $data['homepage'],
+ $data['description'],
+ $data['admin_notes']
+ );
+ }
+
+ /**
+ * Show feedback to the user depending on the confidentiality of the `$client`.
+ */
+ private function outputClientCredentials(Client $client): void
+ {
+ if ($client->confidential()) {
+ PageLayout::postWarning(_('Der OAuth2-Client wurde erstellt.'), [
+ sprintf(_('Die <em lang="en"> client_id </em> lautet: <pre>%s</pre>'), $client['id']),
+ sprintf(_('Das <em lang="en"> client_secret </em> lautet: <pre>%s</pre>'), $client->plainsecret),
+ _(
+ 'Notieren Sie sich bitte das <em lang="en"> client_secret </em>. Es wird Ihnen nur <strong> dieses eine Mal </strong> angezeigt.'
+ ),
+ ]);
+ } else {
+ PageLayout::postSuccess(_('Der OAuth2-Client wurde erstellt.'), [
+ sprintf(_('Die <em lang="en"> client_id </em> lautet: <pre>%s</pre>'), $client['id']),
+ ]);
+ }
+ }
+
+ /**
+ * Validate the request parameters when creating a new client.
+ *
+ * @return array{0: bool, 1: array<string, mixed>, 2: string[]}
+ */
+ private function validateCreateClientRequest()
+ {
+ $valid = true;
+ $data = [];
+ $errors = [];
+
+ // required
+ $name = Request::get('name');
+ $redirectURIs = Request::get('redirect');
+ $confidentiality = Request::get('confidentiality');
+ $owner = Request::get('owner');
+ $homepage = Request::get('homepage');
+
+ // optional
+ $data['description'] = Request::get('description');
+ $data['admin_notes'] = Request::get('admin_notes');
+
+ foreach (compact('name', 'redirectURIs', 'confidentiality', 'owner', 'homepage') as $key => $value) {
+ if (!isset($value)) {
+ $errors[] = sprintf(_('Parameter "%s" fehlt.'), $key);
+ $valid = false;
+ }
+ }
+
+ // validate $name
+ $data['name'] = trim($name);
+ if ($name === '') {
+ $errors[] = _('Der Parameter "name" darf nicht leer sein.');
+ $valid = false;
+ }
+
+ // validate $redirectURIS
+ $redirect = [];
+ $redirectLines = preg_split("/[\n\r]/", $redirectURIs, -1, PREG_SPLIT_NO_EMPTY);
+ foreach ($redirectLines as $line) {
+ $url = filter_var($line, FILTER_SANITIZE_URL);
+ if (false === filter_var($url, FILTER_VALIDATE_URL)) {
+ $errors = _('Der Parameter "redirect" darf nur gültige URLs enthalten.');
+ $valid = false;
+ break;
+ }
+ $redirect[] = $url;
+ }
+ $data['redirect'] = join(',', $redirect);
+
+ // validate $confidentiality
+ if (!in_array($confidentiality, ['public', 'confidential'])) {
+ $errors[] = _('Der Parameter "confidentiality" darf nur gültige URLs enthalten.');
+ $valid = false;
+ }
+ $data['confidential'] = $confidentiality === 'confidential';
+
+ // validate $owner
+ $data['owner'] = trim($owner);
+ if ($owner === '') {
+ $errors[] = _('Der Parameter "owner" darf nicht leer sein.');
+ $valid = false;
+ }
+
+ // validate $homepage
+ $data['homepage'] = filter_var($homepage, FILTER_SANITIZE_URL);
+ if (false === filter_var($homepage, FILTER_VALIDATE_URL)) {
+ $errors = _('Der Parameter "homepage" muss eine gültige URL enthalten.');
+ $valid = false;
+ }
+
+ return [$valid, $data, $errors];
+ }
+}
diff --git a/app/controllers/api/oauth2/oauth2_controller.php b/app/controllers/api/oauth2/oauth2_controller.php
new file mode 100644
index 0000000..fd02ea9
--- /dev/null
+++ b/app/controllers/api/oauth2/oauth2_controller.php
@@ -0,0 +1,52 @@
+<?php
+
+use League\OAuth2\Server\AuthorizationServer;
+use League\OAuth2\Server\Exception\OAuthServerException;
+use Studip\OAuth2\NegotiatesWithPsr7;
+
+abstract class OAuth2Controller extends StudipController
+{
+ use NegotiatesWithPsr7;
+
+ /**
+ * @return void
+ */
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ page_open([
+ 'sess' => 'Seminar_Session',
+ 'auth' => 'Seminar_Default_Auth',
+ 'perm' => 'Seminar_Perm',
+ 'user' => 'Seminar_User',
+ ]);
+
+ $this->set_layout(null);
+
+ $this->container = new Studip\OAuth2\Container();
+ $this->server = $this->getAuthorizationServer();
+ }
+
+ /**
+ * Exception handler called when the performance of an action raises an
+ * exception.
+ *
+ * @param Exception $exception the thrown exception
+ */
+ public function rescue($exception)
+ {
+ if ($exception instanceof OAuthServerException) {
+ $psrResponse = $exception->generateHttpResponse($this->getPsrResponse());
+
+ return $this->convertPsrResponse($psrResponse);
+ }
+
+ return new Trails_Response($exception->getMessage(), [], 500);
+ }
+
+ protected function getAuthorizationServer(): AuthorizationServer
+ {
+ return $this->container->get(AuthorizationServer::class);
+ }
+}
diff --git a/app/controllers/api/oauth2/token.php b/app/controllers/api/oauth2/token.php
new file mode 100644
index 0000000..0ae7ffb
--- /dev/null
+++ b/app/controllers/api/oauth2/token.php
@@ -0,0 +1,29 @@
+<?php
+require_once __DIR__ . '/oauth2_controller.php';
+
+class Api_Oauth2_TokenController extends OAuth2Controller
+{
+ public function before_filter(&$action, &$args)
+ {
+ parent::before_filter($action, $args);
+
+ if ('index' !== $action) {
+ throw new Trails_Exception(404);
+ }
+
+ if (!Request::isPost()) {
+ throw new Trails_Exception(405);
+ }
+
+ $action = 'issue_token';
+ }
+
+ public function issue_token_action(): void
+ {
+ $psrRequest = $this->getPsrRequest();
+ $psrResponse = $this->getPsrResponse();
+ $response = $this->server->respondToAccessTokenRequest($psrRequest, $psrResponse);
+
+ $this->renderPsrResponse($response);
+ }
+}
diff --git a/app/views/admin/oauth2/_clients.php b/app/views/admin/oauth2/_clients.php
new file mode 100644
index 0000000..0e3db8a
--- /dev/null
+++ b/app/views/admin/oauth2/_clients.php
@@ -0,0 +1,79 @@
+<?
+ $sidebar = Sidebar::get();
+ $actions = new ActionsWidget();
+ $actions->addLink(
+ _('OAuth2-Client hinzufügen'),
+ $controller->url_for('api/oauth2/clients/add'),
+ Icon::create('add')
+ );
+ $sidebar->addWidget($actions);
+?>
+
+<? if (isset($clients) && count($clients)) { ?>
+ <h2>
+ <?= _('Registrierte OAuth2-Clients') ?>
+ </h2>
+
+ <? foreach ($clients as $client) { ?>
+ <article class="studip">
+ <header>
+ <h1>
+ <b><?= htmlReady($client['name']) ?></b>
+ </h1>
+ <nav>
+ <form
+ action ="<?= $controller->link_for('api/oauth2/clients/delete', $client) ?>"
+ method="post">
+ <?= CSRFProtection::tokenTag() ?>
+ <?= ActionMenu::get()
+ ->addButton(
+ sprintf(_('OAuth2-Client "%s" löschen'), $client['name']),
+ 'delete_client',
+ Icon::create('trash'),
+ [
+ 'data-confirm' => _('Wollen Sie den OAuth2-Client wirklich löschen?'),
+ 'title' => sprintf(_('OAuth2-Client "%s" löschen'), $client['name']),
+ ]
+ )
+ ->render() ?>
+ </form>
+ </nav>
+ </header>
+
+ <div>
+ <dl>
+ <dt><?= _('Beschreibung') ?></dt>
+ <dd><?= htmlReady($client['description']) ?></dd>
+
+ <dt><?= _('Entwickelt durch') ?></dt>
+ <dd>
+ <a rel="noreferrer noopener" target="_blank"
+ href="<?= htmlReady($client['homepage']) ?>">
+ <?= htmlReady($client['owner']) ?>
+ </a>
+ </dd>
+
+ <dt><?= _('client_id') ?></dt>
+ <dd> <?= htmlReady($client['id']) ?> </dd>
+
+ <dt><?= _('Redirect-URIs') ?></dt>
+ <dd>
+ <ul>
+ <? foreach ($client->redirectUris() as $uri) { ?>
+ <li><?= htmlReady($uri) ?></li>
+ <? } ?>
+ </ul>
+ </dd>
+
+ <dt><?= _('Kann kryptographische Geheimnisse bewahren?') ?></dt>
+ <dd><?= $client->confidential() ? _('Ja') : _('Nein') ?></dd>
+
+ <dt><?= _('Notizen (nur für Root-Accounts sichtbar)') ?></dt>
+ <dd>
+ <?= htmlReady($client['admin_notes']) ?>
+ </dd>
+ </dl>
+ </div>
+ </article>
+ <? } ?>
+<? } ?>
diff --git a/app/views/admin/oauth2/_notices.php b/app/views/admin/oauth2/_notices.php
new file mode 100644
index 0000000..a67539c
--- /dev/null
+++ b/app/views/admin/oauth2/_notices.php
@@ -0,0 +1,10 @@
+<? if (!isset($clients) || !count($clients)) { ?>
+ <?= MessageBox::info(
+ _('Es wurde noch kein OAuth2-Client erstellt.') .
+ '<br/>' .
+ \Studip\LinkButton::createAdd(
+ _('OAuth2-Client hinzufügen'),
+ $controller->link_for('api/oauth2/clients/add')
+ )
+ ) ?>
+<? } ?>
diff --git a/app/views/admin/oauth2/_setup.php b/app/views/admin/oauth2/_setup.php
new file mode 100644
index 0000000..2469a1b
--- /dev/null
+++ b/app/views/admin/oauth2/_setup.php
@@ -0,0 +1,17 @@
+<ul>
+ <li>
+ <? $privateKey = $setup->privateKey(); ?>
+ <b lang="en">Private Key</b> (<?= htmlReady($privateKey->filename()) ?>)
+ <?= $this->render_partial('admin/oauth2/_setup_key.php', ['key' => $privateKey]) ?>
+ </li>
+ <li>
+ <? $publicKey = $setup->publicKey(); ?>
+ <b lang="en">Public Key</b> (<?= htmlReady($publicKey->filename()) ?>)
+ <?= $this->render_partial('admin/oauth2/_setup_key.php', ['key' => $publicKey]) ?>
+ </li>
+ <li>
+ <? $encryptionKey = $setup->encryptionKey(); ?>
+ <b lang="en">Encryption Key</b> (<?= htmlReady($encryptionKey->filename()) ?>)
+ <?= $this->render_partial('admin/oauth2/_setup_key.php', ['key' => $encryptionKey]) ?>
+ </li>
+</ul>
diff --git a/app/views/admin/oauth2/_setup_key.php b/app/views/admin/oauth2/_setup_key.php
new file mode 100644
index 0000000..d1ee322
--- /dev/null
+++ b/app/views/admin/oauth2/_setup_key.php
@@ -0,0 +1,28 @@
+<?php
+ $checkmark = function (bool $checked): Icon {
+ return $checked
+ ? Icon::create('accept', Icon::ROLE_STATUS_GREEN)
+ : Icon::create('decline', Icon::ROLE_STATUS_RED);
+ };
+
+ $predicate = function ($checked, $positive, $negative) {
+ return $checked ? $positive : $negative;
+ };
+?>
+<ul>
+ <li style="list-style-image: url(<?= $checkmark($key->exists())->asImagePath() ?>)">
+ <?= $predicate($key->exists(), _('Datei existiert.'), _('Datei existiert nicht.')) ?>
+ </li>
+ <li style="list-style-image: url(<?= $checkmark($key->isReadable())->asImagePath() ?>)">
+ <?= $predicate($key->isReadable(), _('Datei ist lesbar.'), _('Datei ist nicht lesbar.')) ?>
+ </li>
+ <? if ($key->isReadable()) { ?>
+ <li style="list-style-image: url(<?= $checkmark($key->hasProperMode())->asImagePath() ?>)">
+ <?= $predicate(
+ $key->hasProperMode(),
+ sprintf(_('Korrekte Zugriffsberechtigung: %s'), $key->mode()),
+ sprintf(_('Falsche Zugriffsberechtigung: %s'), $key->mode())
+ ) ?>
+ </li>
+ <? } ?>
+</ul>
diff --git a/app/views/admin/oauth2/index.php b/app/views/admin/oauth2/index.php
new file mode 100644
index 0000000..8ac0f18
--- /dev/null
+++ b/app/views/admin/oauth2/index.php
@@ -0,0 +1,14 @@
+<?= $this->render_partial('admin/oauth2/_notices') ?>
+
+<article class="studip admin-oauth2--setup">
+ <header>
+ <h1>
+ <a name="setup">
+ <?= _('OAuth2-Setup') ?>
+ </a>
+ </h1>
+ </header>
+ <?= $this->render_partial('admin/oauth2/_setup') ?>
+</article>
+
+<?= $this->render_partial('admin/oauth2/_clients') ?>
diff --git a/app/views/api/oauth/authorize.php b/app/views/api/oauth/authorize.php
index 8330e9f..6c66532 100644
--- a/app/views/api/oauth/authorize.php
+++ b/app/views/api/oauth/authorize.php
@@ -23,11 +23,12 @@
htmlReady($GLOBALS['user']->username)
) ?><br>
<small>
- <?= sprintf(
- _('Sind sie nicht <strong>%s</strong>, so <a href="%s">melden Sie sich bitte ab</a> und versuchen es erneut.'),
- htmlReady($GLOBALS['user']->getFullName()),
- URLHelper::getLink('logout.php')
- ) ?>
+ <a href="<?= URLHelper::getLink('logout.php') ?>">
+ <?= sprintf(
+ _('Sind sie nicht <strong>%s</strong>, so melden Sie sich bitte ab und versuchen es erneut.'),
+ htmlReady($GLOBALS['user']->getFullName())
+ ) ?>
+ </a>
</small>
</p>
</section>
diff --git a/app/views/api/oauth2/applications/details.php b/app/views/api/oauth2/applications/details.php
new file mode 100644
index 0000000..12533a0
--- /dev/null
+++ b/app/views/api/oauth2/applications/details.php
@@ -0,0 +1,24 @@
+<dl>
+ <dt><?= _('Name') ?></dt>
+ <dl><?= htmlReady($application['name']) ?></dl>
+
+ <dt><?= _('Beschreibung') ?></dt>
+ <dl><?= htmlReady($application['description']) ?></dl>
+
+ <dt><?= _('Von wem wird der OAuth2-Client entwickelt?') ?></dt>
+ <dl>
+ <a rel="noreferrer noopener" target="_blank"
+ href="<?= htmlReady($application['homepage']) ?>">
+ <?= htmlReady($application['owner']) ?>
+ </a>
+ </dl>
+
+ <dt><?= _('Berechtigungen') ?></dt>
+ <dd>
+ <ul>
+ <? foreach ($application['scopes'] as $scope) { ?>
+ <li><?= htmlReady($scope->description) ?></li>
+ <? } ?>
+ </ul>
+ </dd>
+</dl>
diff --git a/app/views/api/oauth2/applications/index.php b/app/views/api/oauth2/applications/index.php
new file mode 100644
index 0000000..6a1462a
--- /dev/null
+++ b/app/views/api/oauth2/applications/index.php
@@ -0,0 +1,52 @@
+<? if (isset($applications) && count($applications)) { ?>
+ <? foreach ($applications as $application) { ?>
+ <article class="studip">
+ <header>
+ <h1>
+ <a href="<?= $controller->link_for('api/oauth2/applications/details/' . $application['id']) ?>" data-dialog="size=auto">
+ <?= htmlReady($application['name']) ?>
+ </a>
+ </h1>
+ <nav>
+ <form
+ action ="<?= $controller->link_for('api/oauth2/applications/revoke') ?>"
+ method="post">
+ <?= CSRFProtection::tokenTag() ?>
+ <input type="hidden" name="application" value="<?= htmlReady($application['id']) ?>">
+ <?= ActionMenu::get()
+ ->addButton(
+ _('Autorisierung widerrufen'),
+ 'revoke_authorisation',
+ Icon::create('trash'),
+ [
+ 'data-confirm' => _('Wollen Sie die OAuth2-Autorisierung wirklich widerrufen?'),
+ 'title' => _('Autorisierung widerrufen'),
+ ]
+ )
+ ->render() ?>
+ </form>
+ </nav>
+ </header>
+
+ <div>
+ <span class="oauth2-application--owned-by">
+ <?= _('Entwickelt durch:') ?>
+ <a rel="noreferrer noopener" target="_blank"
+ href="<?= htmlReady($application['homepage']) ?>">
+ <?= htmlReady($application['owner']) ?>
+ </a>
+ </span>
+ </div>
+
+ <ul>
+ <? foreach ($application['scopes'] as $scope) { ?>
+ <li><?= htmlReady($scope->description) ?></li>
+ <? } ?>
+ </ul>
+ </article>
+ <? } ?>
+<? } else { ?>
+ <?= \MessageBox::info(
+ _('Keine autorisierten Drittanwendungen'),
+ [ _('Sie haben keine Anwendungen, die zum Zugriff auf Ihr Konto berechtigt sind.') ]) ?>
+<? } ?>
diff --git a/app/views/api/oauth2/authorize.php b/app/views/api/oauth2/authorize.php
new file mode 100644
index 0000000..693968a
--- /dev/null
+++ b/app/views/api/oauth2/authorize.php
@@ -0,0 +1,60 @@
+<section class="oauth authorize">
+ <header>
+ <h1><?= _('Autorisierungsanfrage') ?></h1>
+ </header>
+
+ <p>
+ <?= sprintf(
+ _('Die Applikation <strong>"%s"</strong> möchte auf Ihre Daten zugreifen.'),
+ htmlReady($client->getName())
+ ) ?>
+ </p>
+
+ <? if (count($scopes) > 0) { ?>
+ <div class="scopes">
+ <p><strong><?= _('Diese Applikation hat Zugriff auf:') ?></strong></p>
+ <ul>
+ <? foreach ($scopes as $scope) { ?>
+ <li><?= htmlReady($scope->description) ?></li>
+ <? } ?>
+ </ul>
+ </div>
+ <? } ?>
+
+ <div class="buttons">
+ <form action="<?= $controller->url_for('api/oauth2/authorize') ?>" method="post">
+ <?= \CSRFProtection::tokenTag() ?>
+ <input type="hidden" name="_method" value="delete">
+ <input type="hidden" name="state" value="<?= htmlReady($state) ?>">
+ <input type="hidden" name="client_id" value="<?= htmlReady($client->id) ?>">
+ <input type="hidden" name="auth_token" value="<?= htmlReady($authToken) ?>">
+ <?= Studip\Button::create(_('Verweigern'), 'deny') ?>
+ </form>
+
+ <form action="<?= $controller->url_for('api/oauth2/authorize') ?>" method="post">
+ <?= \CSRFProtection::tokenTag() ?>
+ <input type="hidden" name="state" value="<?= htmlReady($state) ?>">
+ <input type="hidden" name="client_id" value="<?= htmlReady($client->id) ?>">
+ <input type="hidden" name="auth_token" value="<?= htmlReady($authToken) ?>">
+ <?= Studip\Button::create(_('Erlauben'), 'allow') ?>
+ </form>
+ </div>
+
+ <p>
+ <?= Avatar::getAvatar($GLOBALS['user']->id)->getImageTag(Avatar::SMALL) ?>
+
+ <?= sprintf(
+ _('Angemeldet als <strong>%s</strong> (%s)'),
+ htmlReady($GLOBALS['user']->getFullName()),
+ htmlReady($GLOBALS['user']->username)
+ ) ?><br>
+ <small>
+ <a href="<?= URLHelper::getLink('logout.php') ?>">
+ <?= sprintf(
+ _('Sind sie nicht <strong>%s</strong>, so melden Sie sich bitte ab und versuchen es erneut.'),
+ htmlReady($GLOBALS['user']->getFullName())
+ ) ?>
+ </a>
+ </small>
+ </p>
+</section>
diff --git a/app/views/api/oauth2/clients/add.php b/app/views/api/oauth2/clients/add.php
new file mode 100644
index 0000000..855f446
--- /dev/null
+++ b/app/views/api/oauth2/clients/add.php
@@ -0,0 +1,86 @@
+<form class="default" action="<?= $controller->url_for('api/oauth2/clients/store') ?>" method="post">
+ <?= CSRFProtection::tokenTag() ?>
+
+ <fieldset>
+ <legend>
+ <?= _('Basisdaten des OAuth2-Clients') ?>
+ </legend>
+ <label>
+ <span class="required">
+ <?= _('Name') ?>
+ </span>
+ <input required type="text" name="name">
+ </label>
+
+ <label>
+ <span class="required">
+ <?= _('Redirect-URIs') ?>
+ </span>
+ <textarea required name="redirect" placeholder="<?= _('schema://<redirect-uri-1>\nschema://<redirect-uri-2>') ?>" maxlength="1000"></textarea>
+ </label>
+
+ <label>
+ <span>
+ <?= _('Beschreibung') ?>
+ </span>
+ <textarea name="description" maxlength="1000"></textarea>
+ </label>
+ </fieldset>
+
+ <fieldset class="oauth2-clients--confidentiality">
+ <legend class="required">
+ <?= _('Kann der OAuth2-Client kryptographische Geheimnisse bewahren?') ?>
+ </legend>
+
+ <div>
+ <input type="radio" name="confidentiality" value="public" id="oauth2-clients-confidentiality--public" required>
+ <label for="oauth2-clients-confidentiality--public">
+ <?= _('Nein. Es handelt sich zum Beispiel um eine <span lang="en">Mobile App</span> oder <span lang="en">Single Page App</span>.') ?>
+ </label>
+ </div>
+
+ <div>
+ <input type="radio" name="confidentiality" value="confidential" id="oauth2-clients-confidentiality--confidential">
+ <label for="oauth2-clients-confidentiality--confidential">
+ <?= _('Ja, dieser OAuth2-Client kann ein kryptographisches Geheimnis bewahren.') ?>
+ </label>
+ </div>
+ </fieldset>
+
+ <fieldset>
+ <legend>
+ <?= _('Meta-Informationen') ?>
+ </legend>
+
+ <label>
+ <span class="required">
+ <?= _('Von wem wird der OAuth2-Client entwickelt?') ?>
+ </span>
+ <input required type="text" name="owner" maxlength="100">
+ </label>
+
+ <label>
+ <span class="required">
+ <?= _('Homepage der Entwickelnden des OAuth2-Clients') ?>
+ </span>
+ <input required type="url" name="homepage" maxlength="200">
+ </label>
+
+ <label>
+ <span>
+ <?= _('Notizen (nur für Root-Accounts sichtbar)') ?>
+ </span>
+ <textarea name="admin_notes"></textarea>
+ </label>
+
+ </fieldset>
+
+ <footer data-dialog-button>
+ <?= Studip\Button::createAccept(_('Erstellen'), 'create_client', [
+ 'title' => _('Neuen OAuth2-Client erstellen'),
+ ]) ?>
+ <?= Studip\LinkButton::createCancel(_('Abbrechen'), $controller->url_for('admin/oauth2'), [
+ 'title' => _('Zurück zur Übersicht'),
+ ]) ?>
+ </footer>
+</form>