getRequest(); $returnTo = $request->getVal('returnto'); $returnToQuery = $request->getVal('returntoquery'); $session = SessionManager::getGlobalSession(); $authManager = MediaWikiServices::getInstance()->getAuthManager(); $authManager->setAuthenticationSessionData('AuthManagerSpecialPage:return:IsekaiOIDCCallback', [ 'title' => 1, 'authAction' => 'IsekaiOIDCAuth', 'wpLoginToken' => $session->getToken('AuthManagerSpecialPage:IsekaiOIDCCallback'), 'wpRemember' => 1, 'returnto' => $returnTo, 'returntoquery' => $returnToQuery, ]); $url = $oidc->getAuthorizationUrl(); return AuthenticationResponse::newRedirect([ new IsekaiOIDCAuthBeginAuthenticationRequest(), ], $url); } /** * Continue an authentication flow * @inheritDoc */ public function continuePrimaryAuthentication( array $reqs ) { $services = MediaWikiServices::getInstance(); $config = $services->getMainConfig()->get('IsekaiOIDC'); $oidc = self::getOpenIDConnectClient(); $requestCtx = RequestContext::getMain(); if ($oidc->authenticate()) { $accessToken = $oidc->getAccessToken(); $refreshToken = $oidc->getRefreshToken(); $payload = $oidc->getAccessTokenPayload(); $realname = $oidc->requestUserInfo( 'name' ); $email = $oidc->requestUserInfo( 'email' ); $phone = $oidc->requestUserInfo( 'phone_number' ); //$bio = $oidc->requestUserInfo( 'bio' ); $this->subject = $oidc->requestUserInfo( 'sub' ); // set rememberMe if ( isset( $payload->remember_me ) && $payload->remember_me ) { $GLOBALS['wgIsekaiOIDCRemember'] = true; } $authManager = MediaWikiServices::getInstance()->getAuthManager(); $request = $requestCtx->getRequest(); $session = $request->getSession(); $session->clear('AuthManager::AutoCreateBlacklist'); // 防止缓存检测 wfDebugLog( self::LOG_TAG, 'Real name: ' . $realname . ', Phone: ' . $phone . ', Email: ' . $email . ', Subject: ' . $this->subject . PHP_EOL ); $authManager->setAuthenticationSessionData(self::EMAIL_SESSION_KEY, $email); $authManager->setAuthenticationSessionData(self::PHONE_SESSION_KEY, $phone); $authManager->setAuthenticationSessionData(self::REALNAME_SESSION_KEY, $realname); //$authManager->setAuthenticationSessionData(self::BIO_SESSION_KEY, $bio); $authManager->setAuthenticationSessionData(self::ACCESS_TOKEN_SESSION_KEY, $accessToken); $authManager->setAuthenticationSessionData(self::REFRESH_TOKEN_SESSION_KEY, $refreshToken); list( $id, $username ) = $this->findUser( $this->subject ); if ( $id !== null ) { wfDebugLog( self::LOG_TAG, 'Found user with matching subject.' . PHP_EOL ); $authManager->setAuthenticationSessionData(self::USERNAME_SESSION_KEY, $username); $this->updateUserInfo($username); return AuthenticationResponse::newPass($username); } wfDebugLog( self::LOG_TAG, 'No user found with matching subject.' . PHP_EOL ); if ( isset($config['migrateBy']) && $config['migrateBy'] === 'email' ) { wfDebugLog( self::LOG_TAG, 'Checking for email migration.' . PHP_EOL ); list( $id, $username ) = $this->getMigratedIdByEmail( $email ); if ( $id !== null ) { $this->saveExtraAttributes( $id ); wfDebugLog( self::LOG_TAG, 'Migrated user ' . $username . ' by email: ' . $email . '.' . PHP_EOL ); $authManager->setAuthenticationSessionData(self::USERNAME_SESSION_KEY, $username); $this->updateUserInfo($username); return AuthenticationResponse::newPass($username); } } $preferred_username = $this->getPreferredUsername( $config, $oidc, $realname, $email ); wfDebugLog( self::LOG_TAG, 'Preferred username: ' . $preferred_username . PHP_EOL ); $authManager->setAuthenticationSessionData(self::USERNAME_SESSION_KEY, $preferred_username); if ( !isset($config['migrateBy']) || $config['migrateBy'] === 'username' ) { wfDebugLog( self::LOG_TAG, 'Checking for username migration.' . PHP_EOL ); $id = $this->getMigratedIdByUserName( $preferred_username ); if ( $id !== null ) { $this->saveExtraAttributes( $id ); wfDebugLog( self::LOG_TAG, 'Migrated user by username: ' . $preferred_username . '.' . PHP_EOL ); $username = $preferred_username; $authManager->setAuthenticationSessionData(self::USERNAME_SESSION_KEY, $username); $this->updateUserInfo($username); return AuthenticationResponse::newPass($username); } } $username = self::getAvailableUsername( $preferred_username, $realname, $email ); wfDebugLog( self::LOG_TAG, 'Available username: ' . $username . PHP_EOL ); $authManager->setAuthenticationSessionData( self::OIDC_SUBJECT_SESSION_KEY, $this->subject ); $authManager->setAuthenticationSessionData(self::USERNAME_SESSION_KEY, $username); return AuthenticationResponse::newPass($username); } else { $message = Message::newFromSpecifier('isekaioidc-login-failed'); return AuthenticationResponse::newFail($message); } } /** * Determine whether a property can change * @inheritDoc */ public function providerAllowsPropertyChange( $property ) { if (in_array($property, ['emailaddress', 'realname'])) { return false; } return true; } /** * @inheritDoc */ public function autoCreatedAccount( $user, $source ) { // user created if ($user) { $this->updateUserInfo($user); $this->saveExtraAttributes($user->getId()); } } /** * Test whether the named user exists * @inheritDoc */ public function testUserExists( $username, $flags = User::READ_NORMAL ) { return false; } /** * Validate a change of authentication data (e.g. passwords) * @inheritDoc */ public function providerAllowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) { return StatusValue::newGood( 'ignored' ); } /** * Fetch the account-creation type * @inheritDoc */ public function accountCreationType() { return self::TYPE_LINK; } /** * Start an account creation flow * @inheritDoc */ public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { return AuthenticationResponse::newAbstain(); } /** * Change or remove authentication data (e.g. passwords) * @inheritDoc */ public function providerChangeAuthenticationData( AuthenticationRequest $req ) { } public static function getOpenIDConnectClient() { $services = MediaWikiServices::getInstance(); $config = $services->getMainConfig()->get('IsekaiOIDC'); if (!is_array($config)) { wfDebugLog( self::LOG_TAG, 'wgIsekaiOIDC not set' . PHP_EOL ); throw new Exception('wgIsekaiOIDC not set'); } if (!isset($config['endpoint']) || !isset($config['clientID']) || !isset($config['clientSecret'])) { wfDebugLog( self::LOG_TAG, 'wgIsekaiOIDC not valid' . PHP_EOL ); throw new Exception('wgIsekaiOIDC not valid'); } $endpoint = $config['endpoint']; $clientID = $config['clientID']; $clientSecret = $config['clientSecret']; $oidc = new OpenIDConnectClient($endpoint, $clientID, $clientSecret); $redirectURL = $redirectURL = SpecialPage::getTitleFor('IsekaiOIDCCallback')->getFullURL(); $oidc->setRedirectURL( $redirectURL ); wfDebugLog( self::LOG_TAG, 'Redirect URL: ' . $redirectURL ); if ( isset( $_REQUEST['force'] ) || isset( $_REQUEST['forcelogin'] ) ) { $oidc->addAuthParam( [ 'prompt' => 'login' ] ); } if ( isset( $config['authparam'] ) && is_array( $config['authparam'] ) ) { $oidc->addAuthParam( $config['authparam'] ); } if ( isset( $config['scope'] ) ) { $scope = $config['scope']; if ( ! is_array( $scope ) ) { $scope = [ $scope ]; } $oidc->addScope( $scope ); } if ( isset( $config['proxy'] ) ) { $oidc->setHttpProxy( $config['proxy'] ); } if ( isset( $config['verifyHost'] ) ) { $oidc->setVerifyHost( $config['verifyHost'] ); } if ( isset( $config['verifyPeer'] ) ) { $oidc->setVerifyPeer( $config['verifyPeer'] ); } return $oidc; } /** * @inheritDoc */ public function getAuthenticationRequests( $action, array $options ) { switch ( $action ) { case AuthManager::ACTION_LOGIN: return [ new IsekaiOIDCAuthBeginAuthenticationRequest() ]; default: return []; } } /** * 更新用户的个人信息 * @param User|string $user 用户 * @param array|null $data 用户信息 */ public static function updateUserInfo($user, $data = null) { if (is_string($user)) { $user = MediaWikiServices::getInstance()->getUserFactory()->newFromName($user); } if ($data) { $accessToken = $data['accessToken'] ?? null; $refreshToken = $data['refreshToken'] ?? null; $newEmail = $data['email'] ?? null; $newRealName = $data['realname'] ?? null; $newPhone = $data['phone'] ?? null; } else { $authManager = MediaWikiServices::getInstance()->getAuthManager(); $accessToken = $authManager->getAuthenticationSessionData(self::ACCESS_TOKEN_SESSION_KEY); $refreshToken = $authManager->getAuthenticationSessionData(self::REFRESH_TOKEN_SESSION_KEY); $newEmail = $authManager->getAuthenticationSessionData(self::EMAIL_SESSION_KEY); $newRealName = $authManager->getAuthenticationSessionData(self::REALNAME_SESSION_KEY); $newPhone = $authManager->getAuthenticationSessionData(self::PHONE_SESSION_KEY); } if (!$newEmail && $newPhone) { // 如果只设置了手机号,没有设置邮箱地址,则设置一个虚构的电子邮箱 $newEmail = self::punycodeEnc($user->getName()) . '@' . self::OIDC_FAKE_MAILDOMAIN; } if ($accessToken) { wfDebugLog( self::LOG_TAG, 'update access token for: ' . $user->getId() . '.' . PHP_EOL ); $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY ); $dbw->upsert( self::OIDC_TABLE, [ 'oidc_user' => $user->getId(), 'access_token' => $accessToken, 'refresh_token' => $refreshToken ], [ [ 'oidc_user' ] ], [ 'access_token' => $accessToken, 'refresh_token' => $refreshToken ], __METHOD__ ); } $modified = false; if ($newEmail && $newEmail != $user->mEmail && Sanitizer::validateEmail($newEmail)) { $user->mEmail = $newEmail; $user->confirmEmail(); $modified = true; } if ($newRealName && $newRealName != $user->mRealName) { $user->mRealName = $newRealName; $modified = true; } if ($modified) { $user->saveSettings(); } } /** * @since 1.0 * * @param int $id user id */ public function saveExtraAttributes( $id ) { $authManager = MediaWikiServices::getInstance()->getAuthManager(); if ( $this->subject === null ) { $this->subject = $authManager->getAuthenticationSessionData( self::OIDC_SUBJECT_SESSION_KEY ); $authManager->removeAuthenticationSessionData( self::OIDC_SUBJECT_SESSION_KEY ); } $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_PRIMARY ); $dbw->upsert( self::OIDC_TABLE, [ 'oidc_user' => $id, 'oidc_subject' => $this->subject ], [ [ 'oidc_user' ] ], [ 'oidc_subject' => $this->subject ], __METHOD__ ); } public static function findUser( $subject ) { $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_REPLICA ); $row = $dbr->selectRow( [ 'user', self::OIDC_TABLE ], [ 'user_id', 'user_name', 'access_token', 'refresh_token' ], [ 'oidc_subject' => $subject ], __METHOD__, [], [ self::OIDC_TABLE => [ 'JOIN', [ 'user_id=oidc_user' ] ] ] ); if ( $row === false ) { return [ null, null, null, null ]; } else { return [ $row->user_id, $row->user_name, $row->access_token, $row->refresh_token ]; } } public static function findOidcDataByUserId( $userId ) { $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_REPLICA ); $row = $dbr->selectRow( [ self::OIDC_TABLE ], [ 'oidc_user', 'oidc_subject', 'access_token', 'refresh_token' ], [ 'oidc_user' => $userId ], __METHOD__ ); if ( $row === false ) { return [ null, null, null ]; } else { return [ $row->oidc_subject, $row->access_token, $row->refresh_token ]; } } public static function findOidcSubjectsByUserIds( array $userIds ) { $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_REPLICA ); $rows = $dbr->select( [ self::OIDC_TABLE ], [ 'oidc_user', 'oidc_subject' ], [ 'oidc_user' => $userIds ], __METHOD__ ); $subjects = []; foreach ( $rows as $row ) { $subjects[$row->oidc_user] = $row->oidc_subject; } return $subjects; } private static function getPreferredUsername( $config, $oidc, $realname, $email ) { if ( isset( $config['preferred_username'] ) ) { wfDebugLog( self::LOG_TAG, 'Using ' . $config['preferred_username'] . ' attribute for preferred username.' . PHP_EOL ); $preferred_username = $oidc->requestUserInfo( $config['preferred_username'] ); } else { $preferred_username = $oidc->requestUserInfo( 'preferred_username' ); } if ( strlen( $preferred_username ) > 0 ) { // do nothing } elseif ( strlen( $realname ) > 0 && isset($config['migrateBy']) && $config['migrateBy'] === 'realname' ) { $preferred_username = $realname; } elseif ( strlen( $email ) > 0 && isset($config['migrateBy']) && $config['migrateBy'] === 'email' ) { $pos = strpos( $email, '@' ); if ( $pos !== false && $pos > 0 ) { $preferred_username = substr( $email, 0, $pos ); } else { $preferred_username = $email; } } else { return null; } $nt = Title::makeTitleSafe( NS_USER, $preferred_username ); if ( $nt === null ) { return null; } return $nt->getText(); } private static function getMigratedIdByUserName( $username ) { $nt = Title::makeTitleSafe( NS_USER, $username ); if ( $nt === null ) { wfDebugLog( self::LOG_TAG, 'Invalid preferred username for migration: ' . $username . '.' . PHP_EOL ); return null; } $username = $nt->getText(); $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_REPLICA ); $row = $dbr->selectRow( [ 'user', self::OIDC_TABLE ], [ 'user_id' ], [ 'user_name' => $username, 'oidc_user' => null ], __METHOD__, [], [ self::OIDC_TABLE => [ 'LEFT JOIN', [ 'user_id=oidc_user' ] ] ] ); if ( $row !== false ) { return $row->user_id; } return null; } private static function getMigratedIdByEmail( $email ) { wfDebugLog( self::LOG_TAG, 'Matching user to email ' . $email . '.' . PHP_EOL ); $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getMaintenanceConnectionRef( DB_REPLICA ); $row = $dbr->selectRow( [ 'user', self::OIDC_TABLE ], [ 'user_id', 'user_name', 'oidc_user' ], [ 'user_email' => $email ], __METHOD__, [ // if multiple matching accounts, use the oldest one 'ORDER BY' => 'user_registration' ], [ self::OIDC_TABLE => [ 'LEFT JOIN', [ 'user_id=oidc_user' ] ] ] ); if ( $row !== false && $row->oidc_user === null ) { return [ $row->user_id, $row->user_name ]; } return [ null, null ]; } private static function getAvailableUsername( $preferred_username ) { if ( $preferred_username === null ) { $preferred_username = 'User'; } $userIdentityLookup = MediaWikiServices::getInstance()->getUserIdentityLookup(); $userIdentity = $userIdentityLookup->getUserIdentityByName( $preferred_username ); if ( !$userIdentity || !$userIdentity->isRegistered() ) { return $preferred_username; } $count = 1; while ( true ) { $userIdentity = $userIdentityLookup->getUserIdentityByName( $preferred_username . $count ); if ( !$userIdentity || !$userIdentity->isRegistered() ) { break; } $count ++; } return $preferred_username . $count; } protected static function punycodeEnc( $str ){ $punycode = new PunyCode(); return $punycode->encode( $str ); } }