Add Login check

This commit is contained in:
philipp lang 2020-06-27 23:45:49 +02:00
parent ac5c544c32
commit c9d6ffa7a9
13 changed files with 6595 additions and 20 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "zoomyboy/laravel-nami-guard", "name": "zoomyboy/laravel-nami",
"description": "Authentication provider against NaMi for Laravel", "description": "Authentication provider against NaMi for Laravel",
"type": "library", "type": "library",
"authors": [ "authors": [
@ -11,14 +11,16 @@
"require": {}, "require": {},
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Zoomyboy\\LaravelNamiGuard\\": "./src" "Zoomyboy\\LaravelNami\\": "src/"
} }
}, },
"extra": { "autoload-dev": {
"laravel": { "psr-4": {
"providers": [ "Zoomyboy\\LaravelNami\\Tests\\": "tests/"
"Zoomyboy\\LaravelNamiGuard\\ServiceProvider"
]
} }
},
"require-dev": {
"orchestra/testbench": "^5.3",
"guzzlehttp/guzzle": "^6.3.1|^7.0"
} }
} }

5965
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
phpunit.xml Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
</php>
</phpunit>

232
src/Api.php Normal file
View File

@ -0,0 +1,232 @@
<?php
namespace Zoomyboy\LaravelNami;
use App\Conf;
use App\Nami\Exceptions\TooManyLoginAttemptsException;
use App\Nami\Interfaces\UserResolver;
use Illuminate\Support\Facades\Http;
class Api {
public $cookie;
public $loggedIn = null;
public static $url = 'https://nami.dpsg.de';
public function __construct() {
$this->cookie = new \GuzzleHttp\Cookie\CookieJar();
}
private function http() {
return Http::withOptions(['cookies' => $this->cookie]);
}
public function setUser(NamiUser $user) {
$this->user = $user;
return $this;
}
protected function loggedInAlready(): bool {
return $this->loggedIn !== null;
}
public function login($mglnr = null, $password = null, $groupid = null): self {
if ($this->loggedIn) { return $this; }
$mglnr = $mglnr ?: config('nami.auth.mglnr');
$password = $password ?: config('nami.auth.password');
$groupid = $groupid ?: config('nami.auth.groupid');
Http::withOptions(['cookies' => $this->cookie])->get(self::$url.'/ica/pages/login.jsp');
$response = Http::asForm()->withOptions(['cookies' => $this->cookie])->post(self::$url.'/ica/rest/nami/auth/manual/sessionStartup', [
'Login' => 'API',
'redirectTo' => './app.jsp',
'username' => $mglnr,
'password' => $password
])->json();
if ($response['statusCode'] !== 0) {
$e = new LoginException();
$e->setResponse($response);
throw $e;
}
$this->loggedIn = $mglnr;
return $this;
}
public function groups() {
$r = $this->http()->get(self::$url.'/ica/rest/nami/gruppierungen/filtered-for-navigation/gruppierung/node/root')->body();
}
public function fees() {
$response = $this->client->get("/ica/rest/namiBeitrag/beitragsartmgl/gruppierung/{$this->user->getNamiGroupId()}", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function nationalities() {
$response = $this->client->get("/ica/rest/baseadmin/staatsangehoerigkeit", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function confessions() {
$response = $this->client->get("/ica/rest/baseadmin/konfession", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function genders() {
$response = $this->client->get("/ica/rest/baseadmin/geschlecht", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function regions() {
$response = $this->client->get("/ica/rest/baseadmin/region", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function countries() {
$response = $this->client->get("/ica/rest/baseadmin/land", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function activities() {
$response = $this->client->get("/ica/rest/nami/taetigkeitaufgruppierung/filtered/gruppierung/gruppierung/{$this->user->getNamiGroupId()}", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function groupForActivity($activityId) {
$response = $this->client->get("/ica/rest//nami/untergliederungauftaetigkeit/filtered/untergliederung/taetigkeit/{$activityId}", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function allMembers() {
$response = $this->client->get("/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/{$this->user->getNamiGroupId()}/flist", [
'cookies' => $this->cookie
]);
return json_decode((string)$response->getBody());
}
public function getMember($memberId) {
$response = $this->client->get('/ica/rest/nami/mitglied/filtered-for-navigation/gruppierung/gruppierung/'.$this->user->getNamiGroupId().'/'.$memberId, [
'cookies' => $this->cookie,
'http_errors' => false
]
);
return json_decode((string)$response->getBody());
}
/** @todo testen mit guzzle fake */
public function isSuccess($response) {
return isset ($response->success) && $response->success === true
&& isset ($response->responseType) && $response->responseType == 'OK';
}
public function checkCredentials() {
try {
$this->login();
} catch (LoginException $e) {
return false;
}
return true;
}
public function get($url) {
$this->login();
$response = $this->client->request('GET', $this->baseUrl.$url, [
'http_errors' => false,
'cookies' => $this->cookie
]);
$json = json_decode((string) $response->getBody());
return collect($json);
}
public function post($url, $fields) {
$this->login();
$response = $this->client->request('POST', $this->baseUrl.$url, [
'http_errors' => false,
'cookies' => $this->cookie,
'headers' => [
'Accept' => '*/*',
'Content-Type' => 'application/x-www-form-urlencoded; charset=UTF-8'
],
'form_params' => $fields
]);
$json = json_decode((string) $response->getBody());
return collect($json);
}
public function put($url, $fields) {
$this->login();
$response = $this->client->request('PUT', $this->baseUrl.$url, [
'http_errors' => false,
'cookies' => $this->cookie,
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
'json' => $fields
]);
$json = json_decode((string) $response->getBody());
if (is_null($json)) {
\Log::critical('Api gibt kein JSON zurück', [
'response' => (string) $response->getBody(),
'fields' => $fields,
'url' => $url
]);
return null;
}
if (!$json->success || $json->success == false) {
\Log::critical('Fehler beim Update', [
'response' => (string) $response->getBody(),
'fields' => $fields,
'url' => $url
]);
return null;
}
return collect($json);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Zoomyboy\LaravelNami;
use Illuminate\Http\Request;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
trait AuthenticatesNamiUsers {
use AuthenticatesUsers;
protected function validateLogin(Request $request)
{
$request->validate([
$this->username() => 'required|numeric',
'password' => 'required|string',
'groupid' => 'required|numeric'
]);
}
public function username()
{
return 'mglnr';
}
protected function credentials(Request $request)
{
return $request->only($this->username(), 'password', 'groupid');
}
}

30
src/LoginException.php Normal file
View File

@ -0,0 +1,30 @@
<?php
namespace Zoomyboy\LaravelNami;
use Illuminate\Support\Str;
class LoginException extends \Exception {
const TOO_MANY_FAILED_LOGINS = 1;
const WRONG_CREDENTIALS = 2;
public $response;
public $reason = null;
public function setResponse($response) {
if (Str::startsWith($response['statusMessage'], 'Die höchste Anzahl von Login-Versuchen wurde erreicht')) {
$this->setReason(self::TOO_MANY_FAILED_LOGINS);
}
if (Str::startsWith($response['statusMessage'], 'Benutzer nicht gefunden oder Passwort falsch')) {
$this->setReason(self::WRONG_CREDENTIALS);
}
$this->response = $response;
}
public function setReason($reason) {
$this->reason = $reason;
}
}

9
src/Nami.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace Zoomyboy\LaravelNami;
use Illuminate\Support\Facades\Facade;
class Nami extends Facade {
protected static function getFacadeAccessor() { return 'nami.api'; }
}

View File

@ -0,0 +1,23 @@
<?php
namespace Zoomyboy\LaravelNami;
use Illuminate\Support\Facades\Auth;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Support\ServiceProvider;
class NamiServiceProvider extends ServiceProvider
{
public function boot()
{
Auth::provider('nami', function ($app, array $config) {
return new NamiUserProvider($config['model']);
});
}
public function register() {
$this->app->bind('nami.api', function() {
return new Api();
});
}
}

69
src/NamiUser.php Normal file
View File

@ -0,0 +1,69 @@
<?php
namespace Zoomyboy\LaravelNami;
use Illuminate\Contracts\Auth\Authenticatable;
class NamiUser implements Authenticatable {
private $mglnr;
private $groupid;
public $name = 'DDD';
public $email = 'III';
public static function fromCredentials(array $credentials): ?self {
$user = new static();
$user->mglnr = $credentials['mglnr'];
$user->groupid = $credentials['groupid'];
return $user;
}
public function getNamiApi() {
return app(Api::class)->setUser($this)->login(
$this->mglnr,
cache('member.'.$this->mglnr)['credentials']['password'],
$this->groupid
);
}
public function attemptNamiLogin($password) {
return app(Api::class)->setUser($this)->login($this->mglnr, $password, $this->groupid);
}
public function getNamiGroupId() {
return $this->groupid;
}
public static function fromId($id) {
list($mglnr, $groupid) = explode('-', $id);
$user = new static();
$user->mglnr = $mglnr;
$user->groupid = $groupid;
return $user;
}
public function getAuthIdentifierName() {
return 'mglnr';
}
public function getAuthIdentifier() {
return $this->{$this->getAuthIdentifierName()}.'-'.$this->groupid;
}
public function getAuthPassword() {
return null;
}
public function getRememberToken() {
return null;
}
public function setRememberToken($value) {}
public function getRememberTokenName() {
return null;
}
}

51
src/NamiUserProvider.php Normal file
View File

@ -0,0 +1,51 @@
<?php
namespace Zoomyboy\LaravelNami;
use Cache;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Auth\Authenticatable;
class NamiUserProvider implements UserProvider {
private $model;
public function __construct($model) {
$this->model = $model;
}
public function retrieveById($identifier) {
return $this->model::fromId($identifier);
}
public function retrieveByToken($identifier, $token) {
}
public function updateRememberToken(Authenticatable $user, $token) {
}
public function retrieveByCredentials(array $credentials) {
return $this->model::fromCredentials($credentials);
}
public function validateCredentials(Authenticatable $user, array $credentials) {
try {
$api = $user->attemptNamiLogin($credentials['password']);
$data = collect($api->allMembers()->data)->first(function($member) use ($credentials) {
return $member->entries_mitgliedsNummer == $credentials['mglnr'];
});
Cache::forever('member.'.$credentials['mglnr'], [
'data' => $api->getMember($data->id)->data,
'credentials' => $credentials
]);
return true;
} catch (NamiException $e) {
return false;
}
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace Zoomyboy\LaravelNamiGuard;
use Illuminate\Support\ServiceProvider;
use Riak\Connection;
class NamiGuardServiceProvider extends ServiceProvider
{
public function register()
{
}
}

10
tests/TestCase.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Zoomyboy\LaravelNami\Tests;
class TestCase extends \Orchestra\Testbench\TestCase
{
public function test_aaa() {
$this->assertTrue(true);
}
}

144
tests/Unit/LoginTest.php Normal file
View File

@ -0,0 +1,144 @@
<?php
namespace Zoomyboy\LaravelNami\Tests\Unit;
use Zoomyboy\LaravelNami\Nami;
use Zoomyboy\LaravelNami\Tests\TestCase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Config;
use Zoomyboy\LaravelNami\NamiServiceProvider;
use Zoomyboy\LaravelNami\LoginException;
class Login extends TestCase
{
public $successJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":0,"statusMessage":"","apiSessionName":"JSESSIONID","apiSessionToken":"ILBY--L4pZEjSKa39tCemens","minorNumber":2,"majorNumber":1}';
public $bruteJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":3000,"statusMessage":"Die höchste Anzahl von Login-Versuchen wurde erreicht. Ihr Konto ist für 15 Minuten gesperrt worden. Nach Ablauf dieser Zeitspanne wird ihr Zugang wieder freigegeben.","apiSessionName":"JSESSIONID","apiSessionToken":"tGlSpMMij9ruHfeiUYjO7SD2","minorNumber":0,"majorNumber":0}';
public $wrongCredentialsJson = '{"servicePrefix":null,"methodCall":null,"response":null,"statusCode":3000,"statusMessage":"Benutzer nicht gefunden oder Passwort falsch.","apiSessionName":"JSESSIONID","apiSessionToken":"v7lrjgPBbXInJR57qJzVIJ05","minorNumber":0,"majorNumber":0}';
protected function getPackageProviders($app)
{
return [ NamiServiceProvider::class ];
}
/**
* A basic unit test example.
*
* @return void
*/
public function test_first_successful_login()
{
Http::fake([
'https://nami.dpsg.de/ica/pages/login.jsp' => Http::response('<html></html>', 200),
'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup' => Http::response($this->successJson, 200)
]);
Config::set('nami.auth.mglnr', '11223');
Config::set('nami.auth.password', 'secret');
Config::set('nami.auth.groupid', '55555');
Nami::login();
Http::assertSent(function($request) {
return $request->url() == 'https://nami.dpsg.de/ica/pages/login.jsp';
});
Http::assertSent(function($request) {
return $request->url() == 'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup'
&& $request['username'] == '11223' && $request['password'] == 'secret' && $request['redirectTo'] == './app.jsp' && $request['Login'] == 'API';
});
Http::assertSentCount(2);
}
public function test_first_login_fails_because_of_bruteforce_protection()
{
Http::fake([
'https://nami.dpsg.de/ica/pages/login.jsp' => Http::response('<html></html>', 200),
'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup' => Http::response($this->bruteJson, 200)
]);
Config::set('nami.auth.mglnr', '11223');
Config::set('nami.auth.password', 'secret');
Config::set('nami.auth.groupid', '55555');
try {
Nami::login();
} catch(LoginException $e) {
$this->assertEquals(LoginException::TOO_MANY_FAILED_LOGINS, $e->reason);
}
}
public function test_login_once_on_second_login()
{
Http::fake([
'https://nami.dpsg.de/ica/pages/login.jsp' => Http::response('<html></html>', 200),
'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup' => Http::response($this->successJson, 200)
]);
Config::set('nami.auth.mglnr', '11223');
Config::set('nami.auth.password', 'secret');
Config::set('nami.auth.groupid', '55555');
Nami::login();
Nami::login();
Http::assertSent(function($request) {
return $request->url() == 'https://nami.dpsg.de/ica/pages/login.jsp';
});
Http::assertSent(function($request) {
return $request->url() == 'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup'
&& $request['username'] == '11223' && $request['password'] == 'secret' && $request['redirectTo'] == './app.jsp' && $request['Login'] == 'API';
});
Http::assertSentCount(2);
}
public function test_login_check()
{
Http::fake([
'https://nami.dpsg.de/ica/pages/login.jsp' => Http::response('<html></html>', 200),
'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup' => Http::sequence()->push($this->wrongCredentialsJson, 200)
]);
Config::set('nami.auth.mglnr', '11223');
Config::set('nami.auth.password', 'secret');
Config::set('nami.auth.groupid', '55555');
try {
Nami::login();
} catch(LoginException $e) {
$this->assertEquals(LoginException::WRONG_CREDENTIALS, $e->reason);
}
Http::assertSentCount(2);
}
/*
public function test_login_again_if_login_has_expired()
{
Http::fake([
'https://nami.dpsg.de/*' => Http::sequence()
->push('<html></html>')
->push($this->successJson, 200)
->push($this->expiredJson, 200)
->push('<html></html>')
->push($this->successJson, 200)
->push('me', 200)
]);
Config::set('nami.auth.mglnr', '11223');
Config::set('nami.auth.password', 'secret');
Config::set('nami.auth.groupid', '55555');
Nami::login();
Nami::me();
Http::assertSent(function($request) {
return $request->url() == 'https://nami.dpsg.de/ica/pages/login.jsp';
});
Http::assertSent(function($request) {
return $request->url() == 'https://nami.dpsg.de/ica/rest/nami/auth/manual/sessionStartup'
&& $request['username'] == '11223' && $request['password'] == 'secret' && $request['redirectTo'] == './app.jsp' && $request['Login'] == 'API';
});
Http::assertSentCount(6);
}
*/
}