diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java index 9ac31a8813..5af6eed34f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java @@ -95,7 +95,7 @@ public abstract class AbstractOAuth2ClientMapper { UserPrincipal principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, oauth2User.getEmail()); - User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail()); + User user = findUserForClient(oauth2User.getEmail(), oAuth2Client); if (user == null && !config.isAllowUserCreation()) { throw new UsernameNotFoundException("User not found: " + oauth2User.getEmail()); @@ -104,7 +104,7 @@ public abstract class AbstractOAuth2ClientMapper { if (user == null) { userCreationLock.lock(); try { - user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, oauth2User.getEmail()); + user = findUserForClient(oauth2User.getEmail(), oAuth2Client); if (user == null) { user = new User(); if (oauth2User.getCustomerId() == null && StringUtils.isEmpty(oauth2User.getCustomerName())) { @@ -112,7 +112,12 @@ public abstract class AbstractOAuth2ClientMapper { } else { user.setAuthority(Authority.CUSTOMER_USER); } - TenantId tenantId = oauth2User.getTenantId() != null ? oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName()); + TenantId tenantId; + if (oAuth2Client.getTenantId().isSysTenantId()) { + tenantId = oauth2User.getTenantId() != null ? oauth2User.getTenantId() : getTenantId(oauth2User.getTenantName()); + } else { + tenantId = oAuth2Client.getTenantId(); + } user.setTenantId(tenantId); CustomerId customerId = oauth2User.getCustomerId() != null ? oauth2User.getCustomerId() : getCustomerId(user.getTenantId(), oauth2User.getCustomerName()); @@ -164,6 +169,23 @@ public abstract class AbstractOAuth2ClientMapper { } } + /** + * The user is matched by email alone, and the email is whatever the provider chose to send. A client registered by a + * tenant must therefore only ever resolve users of that same tenant; a system client is platform-wide by design. + */ + private User findUserForClient(String email, OAuth2Client oAuth2Client) { + User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, email); + if (user == null || oAuth2Client.getTenantId().isSysTenantId()) { + return user; + } + if (user.getTenantId().equals(oAuth2Client.getTenantId())) { + return user; + } + log.warn("OAuth2 client [{}] of tenant [{}] attempted to log in as user [{}] of tenant [{}]", + oAuth2Client.getId(), oAuth2Client.getTenantId(), user.getId(), user.getTenantId()); + throw new UsernameNotFoundException("User not found: " + email); + } + private TenantId getTenantId(String name) throws Exception { Tenant tenant = tenantService.findTenantByName(name); if (tenant != null) { diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperTest.java new file mode 100644 index 0000000000..775ed5945e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperTest.java @@ -0,0 +1,115 @@ +/** + * Copyright © 2016-2026 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth.oauth2; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.oauth2.OAuth2Client; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.service.security.model.SecurityUser; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DaoSqlTest +public class OAuth2ClientMapperTest extends AbstractControllerTest { + + @Autowired + private BasicOAuth2ClientMapper basicOAuth2ClientMapper; + @Autowired + private UserService userService; + + @Test + public void testShouldFindUserOfOwnTenant() throws Exception { + loginTenantAdmin(); + OAuth2Client tenantClient = doPost("/api/oauth2/client", createOauth2Client(tenantId, "tenant client"), OAuth2Client.class); + + OAuth2User oAuth2User = new OAuth2User(); + oAuth2User.setEmail(TENANT_ADMIN_EMAIL); + + SecurityUser securityUser = basicOAuth2ClientMapper.getOrCreateSecurityUserFromOAuth2User(oAuth2User, tenantClient); + assertThat(securityUser.getTenantId()).isEqualTo(tenantId); + assertThat(securityUser.getAuthority()).isEqualTo(Authority.TENANT_ADMIN); + } + + @Test + public void testShouldNotFindUserOfAnotherTenant() throws Exception { + loginDifferentTenant(); + loginTenantAdmin(); + OAuth2Client tenantClient = doPost("/api/oauth2/client", createOauth2Client(tenantId, "tenant client"), OAuth2Client.class); + + // the email attribute is controlled by the identity provider behind the client + OAuth2User oAuth2User = new OAuth2User(); + oAuth2User.setEmail(DIFFERENT_TENANT_ADMIN_EMAIL); + + UsernameNotFoundException exception = assertThrows( + UsernameNotFoundException.class, + () -> basicOAuth2ClientMapper.getOrCreateSecurityUserFromOAuth2User(oAuth2User, tenantClient)); + assertThat(exception.getMessage()).isEqualTo("User not found: " + DIFFERENT_TENANT_ADMIN_EMAIL); + + User differentTenantAdmin = userService.findUserByEmail(TenantId.SYS_TENANT_ID, DIFFERENT_TENANT_ADMIN_EMAIL); + assertThat(differentTenantAdmin.getTenantId()).isEqualTo(differentTenantId); + + loginSysAdmin(); + deleteDifferentTenant(); + } + + @Test + public void testShouldNotFindSysAdmin() throws Exception { + loginTenantAdmin(); + OAuth2Client tenantClient = doPost("/api/oauth2/client", createOauth2Client(tenantId, "tenant client"), OAuth2Client.class); + + OAuth2User oAuth2User = new OAuth2User(); + oAuth2User.setEmail(SYS_ADMIN_EMAIL); + + UsernameNotFoundException exception = assertThrows( + UsernameNotFoundException.class, + () -> basicOAuth2ClientMapper.getOrCreateSecurityUserFromOAuth2User(oAuth2User, tenantClient)); + assertThat(exception.getMessage()).isEqualTo("User not found: " + SYS_ADMIN_EMAIL); + + User sysAdmin = userService.findUserByEmail(TenantId.SYS_TENANT_ID, SYS_ADMIN_EMAIL); + assertThat(sysAdmin.getAuthority()).isEqualTo(Authority.SYS_ADMIN); + } + + @Test + public void testShouldCreateUserInClientTenant() throws Exception { + loginDifferentTenant(); + loginTenantAdmin(); + OAuth2Client tenantClient = doPost("/api/oauth2/client", createOauth2Client(tenantId, "tenant client"), OAuth2Client.class); + + // a custom mapper endpoint may return any tenant id; the client's own tenant must win + String email = "userA@corporation.gmail.com"; + OAuth2User oAuth2User = new OAuth2User(); + oAuth2User.setEmail(email); + oAuth2User.setTenantId(differentTenantId); + + basicOAuth2ClientMapper.getOrCreateSecurityUserFromOAuth2User(oAuth2User, tenantClient); + + User created = userService.findUserByEmail(TenantId.SYS_TENANT_ID, email); + assertThat(created.getTenantId()).isEqualTo(tenantId); + + loginSysAdmin(); + deleteDifferentTenant(); + } + +}