Skip to content
Snippets Groups Projects
Commit 55852ef4 authored by Marcus Eibrink-Lunzenauer's avatar Marcus Eibrink-Lunzenauer
Browse files

StEP-366: Add OAuth2 support to Stud.IP

Closes #1035 and #1198

Merge request studip/studip!635
parent a9585dad
No related branches found
No related tags found
No related merge requests found
Showing
with 1075 additions and 5 deletions
......@@ -46,3 +46,5 @@ tests/_helpers/_generated
tests/_output/
.idea
/config/oauth2/*.key
/config/oauth2/encryption_key.php
<?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');
}
}
<?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;
},
[]
)
];
}
}
<?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;
}
}
<?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];
}
}
<?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);
}
}
<?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);
}
}
<?
$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>
<? } ?>
<? } ?>
<? 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')
)
) ?>
<? } ?>
<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>
<?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>
<?= $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') ?>
......@@ -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>
<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>
<? 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.') ]) ?>
<? } ?>
<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>
<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>
<?php
namespace Studip\Cli\Commands\OAuth2;
use phpseclib\Crypt\RSA;
use Studip\OAuth2\Container;
use Studip\OAuth2\KeyInformation;
use Studip\OAuth2\SetupInformation;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class Keys extends Command
{
protected static $defaultName = 'oauth2:keys';
protected function configure(): void
{
$this->setDescription(
'Erstelle alle kryptografischen Schlüssel, um Stud.IP als OAuth2-Authorization-Server zu verwenden.'
);
$this->addOption('force', null, InputOption::VALUE_NONE, 'Überschreibe ggf. vorhandene Schlüssel');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$container = new Container();
$setup = $container->get(SetupInformation::class);
$encryptionKey = $setup->encryptionKey();
$publicKey = $setup->publicKey();
$privateKey = $setup->privateKey();
$force = $input->getOption('force');
if (($encryptionKey->exists() || $publicKey->exists() || $privateKey->exists()) && !$force) {
$io->error(
'Schlüsseldateien liegen bereits vor. Verwenden Sie die Option --force, um diese zu überschreiben.'
);
return Command::FAILURE;
}
$this->storeKeyContentsToFile($encryptionKey, $this->generateEncryptionKey());
$keys = (new RSA())->createKey(4096);
$this->storeKeyContentsToFile($publicKey, $keys['publickey']);
$this->storeKeyContentsToFile($privateKey, $keys['privatekey']);
$io->info('Schlüsseldateien erfolgreich angelegt.');
return Command::SUCCESS;
}
private function storeKeyContentsToFile(KeyInformation $key, string $contents)
{
file_put_contents($key->filename(), $contents);
chmod($key->filename(), 0660);
}
private function generateEncryptionKey(): string
{
return "<?php return '" . randomString(48) . "';";
}
}
<?php
namespace Studip\Cli\Commands\OAuth2;
use Studip\OAuth2\Models\AccessToken;
use Studip\OAuth2\Models\AuthCode;
use Studip\OAuth2\Models\RefreshToken;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class Purge extends Command
{
protected static $defaultName = 'oauth2:purge';
protected function configure(): void
{
$this->setDescription('Bereinige die OAuth2-Datenbanktabellen von widerrufenen und/oder abgelaufenen Token');
$this->addOption('revoked', null, InputOption::VALUE_NONE, 'Entferne widerrufene Token');
$this->addOption('expired', null, InputOption::VALUE_NONE, 'Entferne abgelaufene Token');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$expiryDate = strtotime('-7days midnight');
$revoked = $input->getOption('revoked');
$expired = $input->getOption('expired');
if (($revoked && $expired) || (!$revoked && !$expired)) {
AccessToken::deleteBySQL('revoked = ? AND expires_at < ?', [1, $expiryDate]);
AuthCode::deleteBySQL('revoked = ? AND expires_at < ?', [1, $expiryDate]);
RefreshToken::deleteBySQL('revoked = ? AND expires_at < ?', [1, $expiryDate]);
$io->info(
'Alle Token, die widerrufen wurden oder vor mindestens 7 Tagen abgelaufen sind, wurden entfernt.'
);
} elseif ($revoked) {
AccessToken::deleteBySQL('revoked = ?', [1]);
AuthCode::deleteBySQL('revoked = ?', [1]);
RefreshToken::deleteBySQL('revoked = ?', [1]);
$io->info('Alle widerrufenen Token wurden entfernt.');
} elseif ($expired) {
AccessToken::deleteBySQL('expires_at < ?', [$expiryDate]);
AuthCode::deleteBySQL('expires_at < ?', [$expiryDate]);
RefreshToken::deleteBySQL('expires_at < ?', [$expiryDate]);
$io->info('Alle Token, die vor mindestens 7 Tagen abgelaufen sind, wurden entfernt.');
}
return Command::SUCCESS;
}
}
......@@ -37,6 +37,8 @@ $commands = [
Commands\Migrate\MigrateList::class,
Commands\Migrate\MigrateStatus::class,
Commands\Migrate\Migrate::class,
Commands\OAuth2\Keys::class,
Commands\OAuth2\Purge::class,
Commands\Plugins\PluginActivate::class,
Commands\Plugins\PluginDeactivate::class,
Commands\Plugins\PluginInfo::class,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment