From 2a5ba8e8ad78b302f2174c444443badb22044dad Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 16 Jun 2021 18:46:26 +0300 Subject: [PATCH] Add Apple OAuth2 provider. --- .../oauth2_config_templates/apple_config.json | 24 +++++ .../main/data/upgrade/3.2.2/schema_update.sql | 2 +- .../auth/oauth2/AppleOAuth2ClientMapper.java | 101 ++++++++++++++++++ .../auth/oauth2/BasicOAuth2ClientMapper.java | 3 +- .../auth/oauth2/CustomOAuth2ClientMapper.java | 4 +- .../auth/oauth2/GithubOAuth2ClientMapper.java | 3 +- .../auth/oauth2/OAuth2ClientMapper.java | 4 +- .../oauth2/OAuth2ClientMapperProvider.java | 6 ++ .../Oauth2AuthenticationFailureHandler.java | 1 - .../Oauth2AuthenticationSuccessHandler.java | 2 +- .../server/common/data/oauth2/MapperType.java | 2 +- .../model/sql/OAuth2RegistrationEntity.java | 2 +- .../server/dao/oauth2/OAuth2ServiceImpl.java | 3 - .../resources/sql/schema-entities-hsql.sql | 2 +- .../main/resources/sql/schema-entities.sql | 2 +- ui-ngx/src/app/app.component.ts | 7 ++ .../admin/oauth2-settings.component.html | 5 +- .../pages/admin/oauth2-settings.component.ts | 3 +- ui-ngx/src/app/shared/models/oauth2.models.ts | 3 +- 19 files changed, 158 insertions(+), 21 deletions(-) create mode 100644 application/src/main/data/json/system/oauth2_config_templates/apple_config.json create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java diff --git a/application/src/main/data/json/system/oauth2_config_templates/apple_config.json b/application/src/main/data/json/system/oauth2_config_templates/apple_config.json new file mode 100644 index 0000000000..a956920b6b --- /dev/null +++ b/application/src/main/data/json/system/oauth2_config_templates/apple_config.json @@ -0,0 +1,24 @@ +{ + "providerId": "Apple", + "additionalInfo": null, + "accessTokenUri": "https://appleid.apple.com/auth/token", + "authorizationUri": "https://appleid.apple.com/auth/authorize?response_mode=form_post", + "scope": ["email","openid","name"], + "jwkSetUri": "https://appleid.apple.com/auth/keys", + "userInfoUri": null, + "clientAuthenticationMethod": "POST", + "userNameAttributeName": "email", + "mapperConfig": { + "type": "APPLE", + "basic": { + "emailAttributeKey": "email", + "firstNameAttributeKey": "firstName", + "lastNameAttributeKey": "lastName", + "tenantNameStrategy": "DOMAIN" + } + }, + "comment": null, + "loginButtonIcon": "apple-logo", + "loginButtonLabel": "Apple", + "helpLink": "https://developer.apple.com/sign-in-with-apple/get-started/" +} diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index d57668de0d..8020046a7b 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration ( created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java new file mode 100644 index 0000000000..93da71169c --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2021 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 com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.dao.oauth2.OAuth2User; +import org.thingsboard.server.service.security.model.SecurityUser; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +@Service(value = "appleOAuth2ClientMapper") +@Slf4j +public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { + + private static final String USER = "user"; + private static final String NAME = "name"; + private static final String FIRST_NAME = "firstName"; + private static final String LAST_NAME = "lastName"; + private static final String EMAIL = "email"; + + @Override + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + OAuth2MapperConfig config = registration.getMapperConfig(); + Map attributes = updateAttributesFromRequestParams(request, token.getPrincipal().getAttributes()); + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config); + + return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); + } + + private static Map updateAttributesFromRequestParams(HttpServletRequest request, Map attributes) { + Map updated = attributes; + MultiValueMap params = toMultiMap(request.getParameterMap()); + String userValue = params.getFirst(USER); + if (StringUtils.hasText(userValue)) { + JsonNode user = null; + try { + user = JacksonUtil.toJsonNode(userValue); + } catch (Exception e) {} + if (user != null) { + updated = new HashMap<>(attributes); + if (user.has(NAME)) { + JsonNode name = user.get(NAME); + if (name.isObject()) { + JsonNode firstName = name.get(FIRST_NAME); + if (firstName != null && firstName.isTextual()) { + updated.put(FIRST_NAME, firstName.asText()); + } + JsonNode lastName = name.get(LAST_NAME); + if (lastName != null && lastName.isTextual()) { + updated.put(LAST_NAME, lastName.asText()); + } + } + } + if (user.has(EMAIL)) { + JsonNode email = user.get(EMAIL); + if (email != null && email.isTextual()) { + updated.put(EMAIL, email.asText()); + } + } + } + } + return updated; + } + + private static MultiValueMap toMultiMap(Map map) { + MultiValueMap params = new LinkedMultiValueMap<>(map.size()); + map.forEach((key, values) -> { + if (values.length > 0) { + for (String value : values) { + params.add(key, value); + } + } + }); + return params; + } +} 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 f5172b64e7..d2532d0240 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 @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; import java.util.Map; @Service(value = "basicOAuth2ClientMapper") @@ -30,7 +31,7 @@ import java.util.Map; public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); Map attributes = token.getPrincipal().getAttributes(); String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey()); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java index 65ebb384de..778f7416ff 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; + @Service(value = "customOAuth2ClientMapper") @Slf4j public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper { @@ -39,7 +41,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom()); return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration); 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 index d6260fe482..3810f36757 100644 --- 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 @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2User; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Map; import java.util.Optional; @@ -46,7 +47,7 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme private OAuth2Configuration oAuth2Configuration; @Override - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { OAuth2MapperConfig config = registration.getMapperConfig(); Map githubMapperConfig = oAuth2Configuration.getGithubMapper(); String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java index 280418d066..39957602d4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapper.java @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo; import org.thingsboard.server.service.security.model.SecurityUser; +import javax.servlet.http.HttpServletRequest; + public interface OAuth2ClientMapper { - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); + SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration); } 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 606b4f9b1d..df9e5e05ad 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 @@ -37,6 +37,10 @@ public class OAuth2ClientMapperProvider { @Qualifier("githubOAuth2ClientMapper") private OAuth2ClientMapper githubOAuth2ClientMapper; + @Autowired + @Qualifier("appleOAuth2ClientMapper") + private OAuth2ClientMapper appleOAuth2ClientMapper; + public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) { switch (oauth2MapperType) { case CUSTOM: @@ -45,6 +49,8 @@ public class OAuth2ClientMapperProvider { return basicOAuth2ClientMapper; case GITHUB: return githubOAuth2ClientMapper; + case APPLE: + return appleOAuth2ClientMapper; default: throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java index 0e413b4f22..95b4643f4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java @@ -36,7 +36,6 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @Component(value = "oauth2AuthenticationFailureHandler") -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true") public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java index 227d733ebd..303a430e77 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java @@ -90,7 +90,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS token.getAuthorizedClientRegistrationId(), token.getPrincipal().getName()); OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType()); - SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(), registration); JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser); 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 3f91e14bd9..4811e153d9 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, GITHUB; + BASIC, CUSTOM, GITHUB, APPLE; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java index edcaa794a6..ca32bad322 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java @@ -178,7 +178,7 @@ public class OAuth2RegistrationEntity extends BaseSqlEntity .activateUser(activateUser) .type(type) .basic( - (type == MapperType.BASIC || type == MapperType.GITHUB) ? + (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ? OAuth2BasicMapperConfig.builder() .emailAttributeKey(emailAttributeKey) .firstNameAttributeKey(firstNameAttributeKey) 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 83a134ea36..6d7dde1f21 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 @@ -377,9 +377,6 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se if (StringUtils.isEmpty(clientRegistration.getScope())) { throw new DataValidationException("Scope should be specified!"); } - if (StringUtils.isEmpty(clientRegistration.getUserInfoUri())) { - throw new DataValidationException("User info uri should be specified!"); - } if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) { throw new DataValidationException("User name attribute name should be specified!"); } diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index 16522becde..ca7cd73604 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -387,7 +387,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration ( created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 88386a11dd..303bf6f0f9 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -424,7 +424,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration ( created_time bigint NOT NULL, additional_info varchar, client_id varchar(255), - client_secret varchar(255), + client_secret varchar(2048), authorization_uri varchar(255), token_uri varchar(255), scope varchar(255), diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 3ad4038066..39d51ced92 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -89,6 +89,13 @@ export class AppComponent implements OnInit { ) ); + this.matIconRegistry.addSvgIconLiteral( + 'apple-logo', + this.domSanitizer.bypassSecurityTrustHtml( + '' + ) + ); + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index a983266434..43989d907a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -321,16 +321,13 @@ admin.oauth2.user-info-uri - + - - {{ 'admin.oauth2.user-info-uri-required' | translate }} - {{ 'admin.oauth2.uri-pattern-error' | translate }} diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index 4a123c642b..bf5dc75023 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -311,8 +311,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope), jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)], userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '', - [Validators.required, - Validators.pattern(this.URL_REGEXP)]], + [Validators.pattern(this.URL_REGEXP)]], clientAuthenticationMethod: [ registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST, Validators.required], diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index b434302a9a..3d2c125299 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -54,7 +54,8 @@ export const domainSchemaTranslations = new Map( export enum MapperConfigType{ BASIC = 'BASIC', CUSTOM = 'CUSTOM', - GITHUB = 'GITHUB' + GITHUB = 'GITHUB', + APPLE = 'APPLE' } export enum TenantNameStrategy{