Commit 2a5ba8e8ad78b302f2174c444443badb22044dad

Authored by Igor Kulikov
1 parent eec4f5a4

Add Apple OAuth2 provider.

Showing 19 changed files with 158 additions and 21 deletions
  1 +{
  2 + "providerId": "Apple",
  3 + "additionalInfo": null,
  4 + "accessTokenUri": "https://appleid.apple.com/auth/token",
  5 + "authorizationUri": "https://appleid.apple.com/auth/authorize?response_mode=form_post",
  6 + "scope": ["email","openid","name"],
  7 + "jwkSetUri": "https://appleid.apple.com/auth/keys",
  8 + "userInfoUri": null,
  9 + "clientAuthenticationMethod": "POST",
  10 + "userNameAttributeName": "email",
  11 + "mapperConfig": {
  12 + "type": "APPLE",
  13 + "basic": {
  14 + "emailAttributeKey": "email",
  15 + "firstNameAttributeKey": "firstName",
  16 + "lastNameAttributeKey": "lastName",
  17 + "tenantNameStrategy": "DOMAIN"
  18 + }
  19 + },
  20 + "comment": null,
  21 + "loginButtonIcon": "apple-logo",
  22 + "loginButtonLabel": "Apple",
  23 + "helpLink": "https://developer.apple.com/sign-in-with-apple/get-started/"
  24 +}
... ...
... ... @@ -92,7 +92,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration (
92 92 created_time bigint NOT NULL,
93 93 additional_info varchar,
94 94 client_id varchar(255),
95   - client_secret varchar(255),
  95 + client_secret varchar(2048),
96 96 authorization_uri varchar(255),
97 97 token_uri varchar(255),
98 98 scope varchar(255),
... ...
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.security.auth.oauth2;
  17 +
  18 +import com.fasterxml.jackson.databind.JsonNode;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
  21 +import org.springframework.stereotype.Service;
  22 +import org.springframework.util.LinkedMultiValueMap;
  23 +import org.springframework.util.MultiValueMap;
  24 +import org.springframework.util.StringUtils;
  25 +import org.thingsboard.common.util.JacksonUtil;
  26 +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
  27 +import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
  28 +import org.thingsboard.server.dao.oauth2.OAuth2User;
  29 +import org.thingsboard.server.service.security.model.SecurityUser;
  30 +
  31 +import javax.servlet.http.HttpServletRequest;
  32 +import java.util.HashMap;
  33 +import java.util.Map;
  34 +
  35 +@Service(value = "appleOAuth2ClientMapper")
  36 +@Slf4j
  37 +public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
  38 +
  39 + private static final String USER = "user";
  40 + private static final String NAME = "name";
  41 + private static final String FIRST_NAME = "firstName";
  42 + private static final String LAST_NAME = "lastName";
  43 + private static final String EMAIL = "email";
  44 +
  45 + @Override
  46 + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
  47 + OAuth2MapperConfig config = registration.getMapperConfig();
  48 + Map<String, Object> attributes = updateAttributesFromRequestParams(request, token.getPrincipal().getAttributes());
  49 + String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
  50 + OAuth2User oauth2User = BasicMapperUtils.getOAuth2User(email, attributes, config);
  51 +
  52 + return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration);
  53 + }
  54 +
  55 + private static Map<String, Object> updateAttributesFromRequestParams(HttpServletRequest request, Map<String, Object> attributes) {
  56 + Map<String, Object> updated = attributes;
  57 + MultiValueMap<String, String> params = toMultiMap(request.getParameterMap());
  58 + String userValue = params.getFirst(USER);
  59 + if (StringUtils.hasText(userValue)) {
  60 + JsonNode user = null;
  61 + try {
  62 + user = JacksonUtil.toJsonNode(userValue);
  63 + } catch (Exception e) {}
  64 + if (user != null) {
  65 + updated = new HashMap<>(attributes);
  66 + if (user.has(NAME)) {
  67 + JsonNode name = user.get(NAME);
  68 + if (name.isObject()) {
  69 + JsonNode firstName = name.get(FIRST_NAME);
  70 + if (firstName != null && firstName.isTextual()) {
  71 + updated.put(FIRST_NAME, firstName.asText());
  72 + }
  73 + JsonNode lastName = name.get(LAST_NAME);
  74 + if (lastName != null && lastName.isTextual()) {
  75 + updated.put(LAST_NAME, lastName.asText());
  76 + }
  77 + }
  78 + }
  79 + if (user.has(EMAIL)) {
  80 + JsonNode email = user.get(EMAIL);
  81 + if (email != null && email.isTextual()) {
  82 + updated.put(EMAIL, email.asText());
  83 + }
  84 + }
  85 + }
  86 + }
  87 + return updated;
  88 + }
  89 +
  90 + private static MultiValueMap<String, String> toMultiMap(Map<String, String[]> map) {
  91 + MultiValueMap<String, String> params = new LinkedMultiValueMap<>(map.size());
  92 + map.forEach((key, values) -> {
  93 + if (values.length > 0) {
  94 + for (String value : values) {
  95 + params.add(key, value);
  96 + }
  97 + }
  98 + });
  99 + return params;
  100 + }
  101 +}
... ...
... ... @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
23 23 import org.thingsboard.server.dao.oauth2.OAuth2User;
24 24 import org.thingsboard.server.service.security.model.SecurityUser;
25 25
  26 +import javax.servlet.http.HttpServletRequest;
26 27 import java.util.Map;
27 28
28 29 @Service(value = "basicOAuth2ClientMapper")
... ... @@ -30,7 +31,7 @@ import java.util.Map;
30 31 public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
31 32
32 33 @Override
33   - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
  34 + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
34 35 OAuth2MapperConfig config = registration.getMapperConfig();
35 36 Map<String, Object> attributes = token.getPrincipal().getAttributes();
36 37 String email = BasicMapperUtils.getStringAttributeByKey(attributes, config.getBasic().getEmailAttributeKey());
... ...
... ... @@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
29 29 import org.thingsboard.server.dao.oauth2.OAuth2User;
30 30 import org.thingsboard.server.service.security.model.SecurityUser;
31 31
  32 +import javax.servlet.http.HttpServletRequest;
  33 +
32 34 @Service(value = "customOAuth2ClientMapper")
33 35 @Slf4j
34 36 public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
... ... @@ -39,7 +41,7 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
39 41 private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
40 42
41 43 @Override
42   - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
  44 + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
43 45 OAuth2MapperConfig config = registration.getMapperConfig();
44 46 OAuth2User oauth2User = getOAuth2User(token, providerAccessToken, config.getCustom());
45 47 return getOrCreateSecurityUserFromOAuth2User(oauth2User, registration);
... ...
... ... @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
29 29 import org.thingsboard.server.dao.oauth2.OAuth2User;
30 30 import org.thingsboard.server.service.security.model.SecurityUser;
31 31
  32 +import javax.servlet.http.HttpServletRequest;
32 33 import java.util.ArrayList;
33 34 import java.util.Map;
34 35 import java.util.Optional;
... ... @@ -46,7 +47,7 @@ public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme
46 47 private OAuth2Configuration oAuth2Configuration;
47 48
48 49 @Override
49   - public SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
  50 + public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) {
50 51 OAuth2MapperConfig config = registration.getMapperConfig();
51 52 Map<String, String> githubMapperConfig = oAuth2Configuration.getGithubMapper();
52 53 String email = getEmail(githubMapperConfig.get(EMAIL_URL_KEY), providerAccessToken);
... ...
... ... @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
20 20 import org.thingsboard.server.common.data.oauth2.deprecated.OAuth2ClientRegistrationInfo;
21 21 import org.thingsboard.server.service.security.model.SecurityUser;
22 22
  23 +import javax.servlet.http.HttpServletRequest;
  24 +
23 25 public interface OAuth2ClientMapper {
24   - SecurityUser getOrCreateUserByClientPrincipal(OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration);
  26 + SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration);
25 27 }
... ...
... ... @@ -37,6 +37,10 @@ public class OAuth2ClientMapperProvider {
37 37 @Qualifier("githubOAuth2ClientMapper")
38 38 private OAuth2ClientMapper githubOAuth2ClientMapper;
39 39
  40 + @Autowired
  41 + @Qualifier("appleOAuth2ClientMapper")
  42 + private OAuth2ClientMapper appleOAuth2ClientMapper;
  43 +
40 44 public OAuth2ClientMapper getOAuth2ClientMapperByType(MapperType oauth2MapperType) {
41 45 switch (oauth2MapperType) {
42 46 case CUSTOM:
... ... @@ -45,6 +49,8 @@ public class OAuth2ClientMapperProvider {
45 49 return basicOAuth2ClientMapper;
46 50 case GITHUB:
47 51 return githubOAuth2ClientMapper;
  52 + case APPLE:
  53 + return appleOAuth2ClientMapper;
48 54 default:
49 55 throw new RuntimeException("OAuth2ClientRegistrationMapper with type " + oauth2MapperType + " is not supported!");
50 56 }
... ...
... ... @@ -36,7 +36,6 @@ import java.net.URLEncoder;
36 36 import java.nio.charset.StandardCharsets;
37 37
38 38 @Component(value = "oauth2AuthenticationFailureHandler")
39   -@ConditionalOnProperty(prefix = "security.oauth2", value = "enabled", havingValue = "true")
40 39 public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
41 40
42 41 private final HttpCookieOAuth2AuthorizationRequestRepository httpCookieOAuth2AuthorizationRequestRepository;
... ...
... ... @@ -90,7 +90,7 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS
90 90 token.getAuthorizedClientRegistrationId(),
91 91 token.getPrincipal().getName());
92 92 OAuth2ClientMapper mapper = oauth2ClientMapperProvider.getOAuth2ClientMapperByType(registration.getMapperConfig().getType());
93   - SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
  93 + SecurityUser securityUser = mapper.getOrCreateUserByClientPrincipal(request, token, oAuth2AuthorizedClient.getAccessToken().getTokenValue(),
94 94 registration);
95 95
96 96 JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
... ...
... ... @@ -16,5 +16,5 @@
16 16 package org.thingsboard.server.common.data.oauth2;
17 17
18 18 public enum MapperType {
19   - BASIC, CUSTOM, GITHUB;
  19 + BASIC, CUSTOM, GITHUB, APPLE;
20 20 }
... ...
... ... @@ -178,7 +178,7 @@ public class OAuth2RegistrationEntity extends BaseSqlEntity<OAuth2Registration>
178 178 .activateUser(activateUser)
179 179 .type(type)
180 180 .basic(
181   - (type == MapperType.BASIC || type == MapperType.GITHUB) ?
  181 + (type == MapperType.BASIC || type == MapperType.GITHUB || type == MapperType.APPLE) ?
182 182 OAuth2BasicMapperConfig.builder()
183 183 .emailAttributeKey(emailAttributeKey)
184 184 .firstNameAttributeKey(firstNameAttributeKey)
... ...
... ... @@ -377,9 +377,6 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
377 377 if (StringUtils.isEmpty(clientRegistration.getScope())) {
378 378 throw new DataValidationException("Scope should be specified!");
379 379 }
380   - if (StringUtils.isEmpty(clientRegistration.getUserInfoUri())) {
381   - throw new DataValidationException("User info uri should be specified!");
382   - }
383 380 if (StringUtils.isEmpty(clientRegistration.getUserNameAttributeName())) {
384 381 throw new DataValidationException("User name attribute name should be specified!");
385 382 }
... ...
... ... @@ -387,7 +387,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration (
387 387 created_time bigint NOT NULL,
388 388 additional_info varchar,
389 389 client_id varchar(255),
390   - client_secret varchar(255),
  390 + client_secret varchar(2048),
391 391 authorization_uri varchar(255),
392 392 token_uri varchar(255),
393 393 scope varchar(255),
... ...
... ... @@ -424,7 +424,7 @@ CREATE TABLE IF NOT EXISTS oauth2_registration (
424 424 created_time bigint NOT NULL,
425 425 additional_info varchar,
426 426 client_id varchar(255),
427   - client_secret varchar(255),
  427 + client_secret varchar(2048),
428 428 authorization_uri varchar(255),
429 429 token_uri varchar(255),
430 430 scope varchar(255),
... ...
... ... @@ -89,6 +89,13 @@ export class AppComponent implements OnInit {
89 89 )
90 90 );
91 91
  92 + this.matIconRegistry.addSvgIconLiteral(
  93 + 'apple-logo',
  94 + this.domSanitizer.bypassSecurityTrustHtml(
  95 + '<svg viewBox="0 0 256 315"><path d="M213.803394,167.030943 C214.2452,214.609646 255.542482,230.442639 256,230.644727 C255.650812,231.761357 249.401383,253.208293 234.24263,275.361446 C221.138555,294.513969 207.538253,313.596333 186.113759,313.991545 C165.062051,314.379442 158.292752,301.507828 134.22469,301.507828 C110.163898,301.507828 102.642899,313.596301 82.7151126,314.379442 C62.0350407,315.16201 46.2873831,293.668525 33.0744079,274.586162 C6.07529317,235.552544 -14.5576169,164.286328 13.147166,116.18047 C26.9103111,92.2909053 51.5060917,77.1630356 78.2026125,76.7751096 C98.5099145,76.3877456 117.677594,90.4371851 130.091705,90.4371851 C142.497945,90.4371851 165.790755,73.5415029 190.277627,76.0228474 C200.528668,76.4495055 229.303509,80.1636878 247.780625,107.209389 C246.291825,108.132333 213.44635,127.253405 213.803394,167.030988 M174.239142,50.1987033 C185.218331,36.9088319 192.607958,18.4081019 190.591988,0 C174.766312,0.636050225 155.629514,10.5457909 144.278109,23.8283506 C134.10507,35.5906758 125.195775,54.4170275 127.599657,72.4607932 C145.239231,73.8255433 163.259413,63.4970262 174.239142,50.1987249" fill="#000000"></path></svg>'
  96 + )
  97 + );
  98 +
92 99 this.storageService.testLocalStorage();
93 100
94 101 this.setupTranslate();
... ...
... ... @@ -321,16 +321,13 @@
321 321
322 322 <mat-form-field fxFlex class="mat-block">
323 323 <mat-label translate>admin.oauth2.user-info-uri</mat-label>
324   - <input matInput formControlName="userInfoUri" required>
  324 + <input matInput formControlName="userInfoUri">
325 325 <button mat-icon-button matSuffix
326 326 type="button"
327 327 (click)="toggleEditMode(registration, 'userInfoUri')"
328 328 *ngIf="!isCustomProvider(registration)">
329 329 <mat-icon class="material-icons">create</mat-icon>
330 330 </button>
331   - <mat-error *ngIf="registration.get('userInfoUri').hasError('required')">
332   - {{ 'admin.oauth2.user-info-uri-required' | translate }}
333   - </mat-error>
334 331 <mat-error *ngIf="registration.get('userInfoUri').hasError('pattern')">
335 332 {{ 'admin.oauth2.uri-pattern-error' | translate }}
336 333 </mat-error>
... ...
... ... @@ -311,8 +311,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
311 311 scope: this.fb.array(registration?.scope ? registration.scope : [], OAuth2SettingsComponent.validateScope),
312 312 jwkSetUri: [registration?.jwkSetUri ? registration.jwkSetUri : '', Validators.pattern(this.URL_REGEXP)],
313 313 userInfoUri: [registration?.userInfoUri ? registration.userInfoUri : '',
314   - [Validators.required,
315   - Validators.pattern(this.URL_REGEXP)]],
  314 + [Validators.pattern(this.URL_REGEXP)]],
316 315 clientAuthenticationMethod: [
317 316 registration?.clientAuthenticationMethod ? registration.clientAuthenticationMethod : ClientAuthenticationMethod.POST,
318 317 Validators.required],
... ...
... ... @@ -54,7 +54,8 @@ export const domainSchemaTranslations = new Map<DomainSchema, string>(
54 54 export enum MapperConfigType{
55 55 BASIC = 'BASIC',
56 56 CUSTOM = 'CUSTOM',
57   - GITHUB = 'GITHUB'
  57 + GITHUB = 'GITHUB',
  58 + APPLE = 'APPLE'
58 59 }
59 60
60 61 export enum TenantNameStrategy{
... ...