diff options
| author | Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de> | 2022-07-15 11:47:35 +0000 |
|---|---|---|
| committer | Marcus Eibrink-Lunzenauer <lunzenauer@elan-ev.de> | 2022-07-15 11:47:35 +0000 |
| commit | 55852ef4819e5eafce9ae53dc4de2d84cdad1778 (patch) | |
| tree | 9aedcdf89f416a7936f7df80da339a537082b5d5 /app | |
| parent | a9585dad3547a4ebbadd00f44065f95017d18684 (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.php | 46 | ||||
| -rw-r--r-- | app/controllers/api/oauth2/applications.php | 101 | ||||
| -rw-r--r-- | app/controllers/api/oauth2/authorize.php | 175 | ||||
| -rw-r--r-- | app/controllers/api/oauth2/clients.php | 166 | ||||
| -rw-r--r-- | app/controllers/api/oauth2/oauth2_controller.php | 52 | ||||
| -rw-r--r-- | app/controllers/api/oauth2/token.php | 29 | ||||
| -rw-r--r-- | app/views/admin/oauth2/_clients.php | 79 | ||||
| -rw-r--r-- | app/views/admin/oauth2/_notices.php | 10 | ||||
| -rw-r--r-- | app/views/admin/oauth2/_setup.php | 17 | ||||
| -rw-r--r-- | app/views/admin/oauth2/_setup_key.php | 28 | ||||
| -rw-r--r-- | app/views/admin/oauth2/index.php | 14 | ||||
| -rw-r--r-- | app/views/api/oauth/authorize.php | 11 | ||||
| -rw-r--r-- | app/views/api/oauth2/applications/details.php | 24 | ||||
| -rw-r--r-- | app/views/api/oauth2/applications/index.php | 52 | ||||
| -rw-r--r-- | app/views/api/oauth2/authorize.php | 60 | ||||
| -rw-r--r-- | app/views/api/oauth2/clients/add.php | 86 |
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> |
