summaryrefslogblamecommitdiffstats
path: root/inc/shibauth.inc.php
blob: 6ae3a895f48d9ecac23716933004aa65bd759a7d (plain) (tree)









































































































































































































                                                                                                                                                                                                 
<?php

class ShibAuth
{

	/**
	 * Log user into master-server using the data provided by the current shibboleth session
	 * @param ?string $accessCode optional one-time access code to retreive session data via thrift
	 * @return array{status: string, firstName: string, lastName: string, mail: string, token: string, sessionId: string, userId: string, organizationId: string, url: string, error: string}
	 */
	private static function loginInternal(?string $accessCode = null): array
	{
		if ($accessCode !== null) {
			$entrop = strlen(count_chars($accessCode, 3));
			if (strlen($accessCode) < 32 || strlen($accessCode) > 64 || $entrop < 10) {
				return ['status' => 'error', 'error' => 'Malformed accessCode'];
			}
		}
		// Handle
		if (empty($_SERVER['persistent-id'])) {
			// No persistent id given, should not happen!
			file_put_contents('/tmp/shib-nopid-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
			return ['status' => 'error', 'error' => 'Shibboleth metadata missing!'];
		}
		// Query database for user
		// First, use persistent-id as-is
		$shibId = [ md5($_SERVER['persistent-id']) ];
		// ... but we might have multiple persistent IDs, split
		if (strpos($_SERVER['persistent-id'], ';') !== false) {
			foreach (explode(';', $_SERVER['persistent-id']) as $s) {
				if (empty($s))
					continue;
				$shibId[] = md5($s);
			}
		}
		// Figure out role
		if (strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) {
			$role = 'TUTOR';
		} else if (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';employee@') !== false
			|| strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';staff@') !== false
			|| strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';faculty@') !== false) {
			$role = 'TUTOR';
		} else {
			file_put_contents('/tmp/shib-student-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
			$role = 'STUDENT';
			// NEW: Ignore students for now
			return [
				'status' => 'error',
				'error' => "Sie wurden als Student eingestuft und können sich daher nicht an der " . CONFIG_SUITE . "-Suite anmelden."
					. "\nFalls Ihr Nutzerkonto kein Studentenkonto ist stellen Sie sicher, dass Ihr IdP für berechtigte"
					. "\nAccounts entweder das " . CONFIG_SUITE . "-Entitlement ausliefert, oder das Attribut " . CONFIG_SCOPED_AFFILIATION
					. "\nausgeliefert wird, und es entweder 'employee@..', 'staff@..' oder 'faculty@..' enthält."
					. "\n\nMehr Informationen finden Sie unter " . CONFIG_HELPURL
			];
			// end IGNORE STUDENTS
		}
		// Now we have an array of all persistent-ids, plus the raw string; see if any of those match
		$user = Database::queryFirst("SELECT user.userid, user.organizationid, user.firstname, user.lastname, user.email "
			. " FROM user "
			. " INNER JOIN organization USING (organizationid) "
			. " WHERE user.shibid IN (:shibid) LIMIT 1", array('shibid' => $shibId));
		if ($user === false) {
			// Not found, so we don't know which satellite to use
			if ($role === 'STUDENT') {
				$response['status'] = 'ok';
				$response['firstName'] = $_SERVER['givenName'] ?? 'Anonymous';
				$response['lastName'] = $_SERVER[CONFIG_SURNAME] ?? 'Student';
				$response['mail'] = $_SERVER['mail'] ?? 'void@none.invalid';
				$response['userId'] = $shibId;
				// Try to figure out orgId
				if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_EPPN])) {
					if (preg_match('/@(.+)$/', $_SERVER[CONFIG_EPPN], $out)) {
						$out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", [
							'suffix' => $out[1]
						]);
						if ($out !== false) {
							$response['organizationId'] = $out['organizationid'];
						}
					}
				}
				if (!isset($response['organizationId']) && isset($_SERVER[CONFIG_SCOPED_AFFILIATION])) {
					if (preg_match('/(^|;)[^@]+@([^;]+)/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) {
						$out = Database::queryFirst("SELECT organizationid FROM organization_suffix WHERE suffix = :suffix", [
							'suffix' => $out[2]
						]);
						if ($out !== false) {
							$response['organizationId'] = $out['organizationid'];
						}
					}
				}
				// This one we send to the running master server handler
				$rpc = $response;
				$rpc['role'] = $role;
				if (isset($response['organizationId']) && $accessCode === null) {
					$response['satellites2'] = self::getSatelliteList($response['organizationId']);
				}
			} else {
				$response['status'] = 'unregistered';
				$response['error'] = 'Sie müssen sich erst für die Nutzung von ' . CONFIG_SUITE . ' registrieren';
			}
			$response['id'] = $shibId;
			$response['url'] = CONFIG_MASTERWEBIF;
			file_put_contents('/tmp/shib-unreg-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true));
		} else {
			/*
			if (in_array($shibId, unserialize(CONFIG_ADMINS), true) || $shibId === '2fa5c3e020a5aca0cbf9a562268d5173-') {
				$role = 'STUDENT';
			}
			*/
			// Found, see if we got personal information, either temporarily through metadata, or from database
			$firstName = $user['firstname'];
			$lastName = $user['lastname'];
			$mail = $user['email'];
			if (empty($firstName) && isset($_SERVER['givenName']))
				$firstName = trim($_SERVER['givenName']);
			if (empty($lastName) && isset($_SERVER[CONFIG_SURNAME]))
				$lastName = trim($_SERVER[CONFIG_SURNAME]);
			if (empty($mail) && isset($_SERVER['mail']))
				$mail = trim($_SERVER['mail']);
			//
			$login = (empty($user['userid']) ? $shibId : $user['userid'] );
			if (empty($firstName) || empty($lastName) || empty($login)) {
				// This means the user did not provide personal information on signup, nor does the IdP send them
				$response['status'] = 'anonymous';
				$response['error'] = "Ihr IdP liefert nicht die erforderlichen Attribute\n"
					. CONFIG_SURNAME . ', ' . 'givenName' . ', ' . 'email';
			} else {
				// Seems ok!
				//
				$response['status'] = 'ok';
				$response['firstName'] = $firstName;
				$response['lastName'] = $lastName;
				$response['mail'] = $mail;
				$response['userId'] = $user['userid'];
				$response['organizationId'] = $user['organizationid'];
				// This one we send to the running master server handler
				$rpc = $response;
				$rpc['userId'] = $login;
				$rpc['role'] = $role;
				// This one we only send to the user
				$response['satellites2'] = self::getSatelliteList($user['organizationid']);
			}
		}

		if (isset($rpc)) {
			if ($accessCode !== null) {
				$rpc['accessCode'] = $accessCode;
			}
			$reply = RPC::submit($rpc);
			if (preg_match('/^TOKEN:(\w+) SESSIONID:(\w+)$/', $reply, $out)) {
				// For talking to the sat server, also referred to as userToken in Java
				$response['token'] = $out[1];
				// For talking to the master server - not known by satellite server, referred to as masterToken in Java
				$response['sessionId'] = $out[2];
			} else {
				if (empty($rpc['mail'])) {
					$reply .= ' (No email given)';
				}
				if (empty($rpc['firstName'])) {
					$reply .= ' (No first name given)';
				}
				if (empty($rpc['lastName'])) {
					$reply .= ' (No last name given)';
				}
				if (empty($rpc['organizationId'])) {
					$reply .= ' (No organization id found)';
				}
				$response['error'] = $reply;
				$response['status'] = 'error';
			}
		}
		return $response;
	}

	public static function login(?string $accessCode = null): array
	{
		$res = self::loginInternal($accessCode);
		if ($res['status'] !== 'ok' && isset($res['error']) && $accessCode !== null) {
			RPC::submit(['status' => 'error', 'error' => $res['error'], 'accessCode' => $accessCode]);
		}
		return $res;
	}

	private static function getSatelliteList($orgId)
	{
		// Determine satellite(s)
		$res = Database::simpleQuery("SELECT satellitename, addresses, certsha256 FROM satellite"
			. " WHERE organizationid = :organizationid AND userid IS NULL", array('organizationid' => $orgId));
		$sat2 = array();
		while ($row = $res->fetch(PDO::FETCH_ASSOC)) {
			$addrs = json_decode($row['addresses'], true);
			if (!is_array($addrs) || empty($addrs))
				continue;
			$sat2[$row['satellitename']] = array(
				'addresses' => $addrs,
				'certHash' => $row['certsha256']
			);
		}
		return $sat2;
	}

}