Browse Source

Resolve OAuth2 user within the tenant of the OAuth2 client

The user was looked up by email across the whole platform, so a client
belonging to one tenant could resolve a user of another. New users were also
placed in the tenant named by the provider response rather than in the tenant
that owns the client.

Both now follow the client's own tenant. System clients keep the platform-wide
behaviour they rely on.
fix/oauth2-user-tenant-lts-4.2
Viacheslav Klimov 6 days ago
parent
commit
0730387640
Failed to extract signature
  1. 28
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AbstractOAuth2ClientMapper.java
  2. 115
      application/src/test/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperTest.java

28
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) {

115
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();
}
}
Loading…
Cancel
Save