diff --git a/application/src/main/data/json/system/oauth2_config_templates/github_config.json b/application/src/main/data/json/system/oauth2_config_templates/github_config.json
index 58812fdc1e..fff9fcf3a1 100644
--- a/application/src/main/data/json/system/oauth2_config_templates/github_config.json
+++ b/application/src/main/data/json/system/oauth2_config_templates/github_config.json
@@ -7,7 +7,10 @@
"userInfoUri": "https://api.github.com/user",
"clientAuthenticationMethod": "BASIC",
"userNameAttributeName": "login",
- "basic": {},
+ "basic": {
+ "lastNameAttributeKey": "name",
+ "tenantNameStrategy": "DOMAIN"
+ },
"comment": "In order to log into ThingsBoard you need to have user's email. You may configure and use Custom OAuth2 Mapper to get email information. Please refer to Github Documentation",
"loginButtonIcon": "mdi:github",
"loginButtonLabel": "Github",
diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql
index fe8ee4531b..871f55d694 100644
--- a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql
+++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql
@@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
user_name_attribute_name varchar(255),
jwk_set_uri varchar(255),
client_authentication_method varchar(255),
+ type varchar(31),
basic_email_attribute_key varchar(31),
basic_first_name_attribute_key varchar(31),
basic_last_name_attribute_key varchar(31),
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java
new file mode 100644
index 0000000000..246e74245d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicMapperUtils.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.text.StrSubstitutor;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
+import org.thingsboard.server.dao.oauth2.OAuth2User;
+
+import java.util.Map;
+
+@Slf4j
+public class BasicMapperUtils {
+ private static final String START_PLACEHOLDER_PREFIX = "%{";
+ private static final String END_PLACEHOLDER_PREFIX = "}";
+
+ public static OAuth2User getOAuth2User(String email, Map attributes, OAuth2MapperConfig config) {
+ OAuth2User oauth2User = new OAuth2User();
+ oauth2User.setEmail(email);
+ oauth2User.setTenantName(getTenantName(email, attributes, config));
+ if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
+ String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
+ oauth2User.setLastName(lastName);
+ }
+ if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
+ String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
+ oauth2User.setFirstName(firstName);
+ }
+ if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
+ StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
+ String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
+ oauth2User.setCustomerName(customerName);
+ }
+ oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen());
+ if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) {
+ oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName());
+ }
+ return oauth2User;
+ }
+
+ public static String getTenantName(String email, Map attributes, OAuth2MapperConfig config) {
+ switch (config.getBasic().getTenantNameStrategy()) {
+ case EMAIL:
+ return email;
+ case DOMAIN:
+ return email.substring(email .indexOf("@") + 1);
+ case CUSTOM:
+ StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
+ return sub.replace(config.getBasic().getTenantNamePattern());
+ default:
+ throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
+ }
+ }
+
+ public static String getStringAttributeByKey(Map attributes, String key) {
+ String result = null;
+ try {
+ result = (String) attributes.get(key);
+ } catch (Exception e) {
+ log.warn("Can't convert attribute to String by key " + key);
+ }
+ return result;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
index 7412f2199c..73da9e539f 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
@@ -16,10 +16,8 @@
package org.thingsboard.server.service.security.auth.oauth2;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.text.StrSubstitutor;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.service.security.model.SecurityUser;
@@ -30,59 +28,12 @@ import java.util.Map;
@Slf4j
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
- private static final String START_PLACEHOLDER_PREFIX = "%{";
- private static final String END_PLACEHOLDER_PREFIX = "}";
-
@Override
public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
- OAuth2User oauth2User = new OAuth2User();
Map attributes = token.getPrincipal().getAttributes();
- String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
- oauth2User.setEmail(email);
- oauth2User.setTenantName(getTenantName(attributes, config));
- if (!StringUtils.isEmpty(config.getBasic().getLastNameAttributeKey())) {
- String lastName = getStringAttributeByKey(attributes, config.getBasic().getLastNameAttributeKey());
- oauth2User.setLastName(lastName);
- }
- if (!StringUtils.isEmpty(config.getBasic().getFirstNameAttributeKey())) {
- String firstName = getStringAttributeByKey(attributes, config.getBasic().getFirstNameAttributeKey());
- oauth2User.setFirstName(firstName);
- }
- if (!StringUtils.isEmpty(config.getBasic().getCustomerNamePattern())) {
- StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
- String customerName = sub.replace(config.getBasic().getCustomerNamePattern());
- oauth2User.setCustomerName(customerName);
- }
- oauth2User.setAlwaysFullScreen(config.getBasic().isAlwaysFullScreen());
- if (!StringUtils.isEmpty(config.getBasic().getDefaultDashboardName())) {
- oauth2User.setDefaultDashboardName(config.getBasic().getDefaultDashboardName());
- }
+ String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
+ OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
return getOrCreateSecurityUserFromOAuth2User(oauth2User, config.isAllowUserCreation(), config.isActivateUser());
}
-
- private String getTenantName(Map attributes, OAuth2MapperConfig config) {
- switch (config.getBasic().getTenantNameStrategy()) {
- case EMAIL:
- return getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
- case DOMAIN:
- String email = getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
- return email.substring(email .indexOf("@") + 1);
- case CUSTOM:
- StrSubstitutor sub = new StrSubstitutor(attributes, START_PLACEHOLDER_PREFIX, END_PLACEHOLDER_PREFIX);
- return sub.replace(config.getBasic().getTenantNamePattern());
- default:
- throw new RuntimeException("Tenant Name Strategy with type " + config.getBasic().getTenantNameStrategy() + " is not supported!");
- }
- }
-
- private String getStringAttributeByKey(Map attributes, String key) {
- String result = null;
- try {
- result = (String) attributes.get(key);
- } catch (Exception e) {
- log.warn("Can't convert attribute to String by key " + key);
- }
- return result;
- }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
new file mode 100644
index 0000000000..dcca2b71a1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016-2020 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 lombok.Data;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
+import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
+import org.thingsboard.server.dao.oauth2.OAuth2User;
+import org.thingsboard.server.service.security.model.SecurityUser;
+
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Optional;
+
+@Service(value = "githubOAuth2ClientMapper")
+@Slf4j
+public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
+ private static final String EMAIL_URL_KEY = "emailUrl";
+
+ private static final String AUTHORIZATION = "Authorization";
+
+ private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
+
+ @Autowired
+ private OAuth2Configuration oAuth2Configuration;
+
+ @Override
+ public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2MapperConfig config) {
+ Map githubMapperConfig = oAuth2Configuration.getGithubMapper();
+ String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
+ Map attributes = token.getPrincipal().getAttributes();
+ OAuth2User oAuth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
+ return getOrCreateSecurityUserFromOAuth2User(oAuth2User, config.isAllowUserCreation(), config.isActivateUser());
+ }
+
+ private synchronized String getEmail(String emailUrl, String oauth2Token) {
+ restTemplateBuilder = restTemplateBuilder.defaultHeader(AUTHORIZATION, "token " + oauth2Token);
+
+ RestTemplate restTemplate = restTemplateBuilder.build();
+ GithubEmailsResponse githubEmailsResponse;
+ try {
+ githubEmailsResponse = restTemplate.getForEntity(emailUrl, GithubEmailsResponse.class).getBody();
+ if (githubEmailsResponse == null){
+ throw new RuntimeException("Empty Github response!");
+ }
+ } catch (Exception e) {
+ log.error("There was an error during connection to Github API", e);
+ throw new RuntimeException("Unable to login. Please contact your Administrator!");
+ }
+ Optional emailOpt = githubEmailsResponse.stream()
+ .filter(GithubEmailResponse::isPrimary)
+ .map(GithubEmailResponse::getEmail)
+ .findAny();
+ if (emailOpt.isPresent()){
+ return emailOpt.get();
+ } else {
+ log.error("Could not find primary email from {}.", githubEmailsResponse);
+ throw new RuntimeException("Unable to login. Please contact your Administrator!");
+ }
+ }
+ private static class GithubEmailsResponse extends ArrayList {}
+
+ @Data
+ @ToString
+ private static class GithubEmailResponse {
+ private String email;
+ private boolean verified;
+ private boolean primary;
+ private String visibility;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
index 64bdf40970..2e764fe223 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
@@ -33,12 +33,18 @@ public class OAuth2ClientMapperProvider {
@Qualifier("customOAuth2ClientMapper")
private OAuth2ClientMapper customOAuth2ClientMapper;
+ @Autowired
+ @Qualifier("githubOAuth2ClientMapper")
+ private OAuth2ClientMapper githubOAuth2ClientMapper;
+
public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) {
switch (oauth2MapperType) {
case CUSTOM:
return customOAuth2ClientMapper;
case BASIC:
return basicOAuth2ClientMapper;
+ case GITHUB:
+ return githubOAuth2ClientMapper;
default:
throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!");
}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index c81782a0a5..d0ddeb611a 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -115,6 +115,8 @@ security:
oauth2:
# Redirect URL where access code from external user management system will be processed
loginProcessingUrl: "${SECURITY_OAUTH2_LOGIN_PROCESSING_URL:/login/oauth2/code/}"
+ githubMapper:
+ emailUrl: "${SECURITY_OAUTH2_GITHUB_MAPPER_EMAIL_URL_KEY:https://api.github.com/user/emails}"
# Dashboard parameters
dashboard:
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java
index 48f0a77a41..25faad187e 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/MapperType.java
@@ -16,5 +16,5 @@
package org.thingsboard.server.common.data.oauth2;
public enum MapperType {
- BASIC, CUSTOM;
+ BASIC, CUSTOM, GITHUB;
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java
index 5c2f70874a..897472f154 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2ClientRegistrationTemplate.java
@@ -34,6 +34,7 @@ import java.util.List;
public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo implements HasName {
private String providerId;
+ private MapperType mapperType;
private OAuth2BasicMapperConfig basic;
private String authorizationUri;
private String accessTokenUri;
@@ -50,6 +51,7 @@ public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditio
public OAuth2ClientRegistrationTemplate(OAuth2ClientRegistrationTemplate clientRegistrationTemplate) {
super(clientRegistrationTemplate);
this.providerId = clientRegistrationTemplate.providerId;
+ this.mapperType = clientRegistrationTemplate.mapperType;
this.basic = clientRegistrationTemplate.basic;
this.authorizationUri = clientRegistrationTemplate.authorizationUri;
this.accessTokenUri = clientRegistrationTemplate.accessTokenUri;
diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java
index efa9a563ec..6e9ed4822a 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractOAuth2ClientRegistrationInfoEntity.java
@@ -190,7 +190,7 @@ public abstract class AbstractOAuth2ClientRegistrationInfoEntity githubMapper;
}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
index 2ad0ad4f20..9fda0e3c2f 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java
@@ -184,6 +184,22 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
throw new DataValidationException("Tenant name pattern should be specified!");
}
}
+ if (mapperConfig.getType() == MapperType.GITHUB) {
+ OAuth2BasicMapperConfig basicConfig = mapperConfig.getBasic();
+ if (basicConfig == null) {
+ throw new DataValidationException("Basic config should be specified!");
+ }
+ if (!StringUtils.isEmpty(basicConfig.getEmailAttributeKey())) {
+ throw new DataValidationException("Email attribute key cannot be configured for GITHUB mapper type!");
+ }
+ if (basicConfig.getTenantNameStrategy() == null) {
+ throw new DataValidationException("Tenant name strategy should be specified!");
+ }
+ if (basicConfig.getTenantNameStrategy() == TenantNameStrategyType.CUSTOM
+ && StringUtils.isEmpty(basicConfig.getTenantNamePattern())) {
+ throw new DataValidationException("Tenant name pattern should be specified!");
+ }
+ }
if (mapperConfig.getType() == MapperType.CUSTOM) {
OAuth2CustomMapperConfig customConfig = mapperConfig.getCustom();
if (customConfig == null) {
diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql
index 8c78d3392a..74f9679b11 100644
--- a/dao/src/main/resources/sql/schema-entities-hsql.sql
+++ b/dao/src/main/resources/sql/schema-entities-hsql.sql
@@ -386,6 +386,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
user_name_attribute_name varchar(255),
jwk_set_uri varchar(255),
client_authentication_method varchar(255),
+ type varchar(31),
basic_email_attribute_key varchar(31),
basic_first_name_attribute_key varchar(31),
basic_last_name_attribute_key varchar(31),
diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql
index 5e1a7dbddc..6056ba8895 100644
--- a/dao/src/main/resources/sql/schema-entities.sql
+++ b/dao/src/main/resources/sql/schema-entities.sql
@@ -412,6 +412,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client_registration_template (
user_name_attribute_name varchar(255),
jwk_set_uri varchar(255),
client_authentication_method varchar(255),
+ type varchar(31),
basic_email_attribute_key varchar(31),
basic_first_name_attribute_key varchar(31),
basic_last_name_attribute_key varchar(31),
diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java
index 3d828d1ac5..3c32af73a3 100644
--- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java
+++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java
@@ -21,6 +21,7 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.dao.exception.DataValidationException;
@@ -105,6 +106,7 @@ public class BaseOAuth2ConfigTemplateServiceTest extends AbstractServiceTest {
OAuth2ClientRegistrationTemplate clientRegistrationTemplate = new OAuth2ClientRegistrationTemplate();
clientRegistrationTemplate.setProviderId(providerId);
clientRegistrationTemplate.setAdditionalInfo(mapper.createObjectNode().put(UUID.randomUUID().toString(), UUID.randomUUID().toString()));
+ clientRegistrationTemplate.setMapperType(MapperType.BASIC);
clientRegistrationTemplate.setBasic(
OAuth2BasicMapperConfig.builder()
.firstNameAttributeKey("firstName")