Introduce mobile app oauth2 request authentication using application token signed by application secret.

This commit is contained in:
Igor Kulikov 2021-06-11 13:37:23 +03:00
parent dbc3ef95ce
commit 1ed624d30b
23 changed files with 226 additions and 84 deletions

View File

@ -136,7 +136,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile (
oauth2_params_id uuid NOT NULL,
created_time bigint NOT NULL,
pkg_name varchar(255),
callback_url_scheme varchar(255),
app_secret varchar(2048),
CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE,
CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name)
);

View File

@ -39,6 +39,7 @@ import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames;
import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory;
import org.thingsboard.server.utils.MiscUtils;
import javax.servlet.http.HttpServletRequest;
@ -69,6 +70,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
@Autowired
private OAuth2Service oAuth2Service;
@Autowired
private OAuth2AppTokenFactory oAuth2AppTokenFactory;
@Autowired(required = false)
private OAuth2Configuration oauth2Configuration;
@ -78,7 +82,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
String registrationId = this.resolveRegistrationId(request);
String redirectUriAction = getAction(request, "login");
String appPackage = getAppPackage(request);
return resolve(request, registrationId, redirectUriAction, appPackage);
String appToken = getAppToken(request);
return resolve(request, registrationId, redirectUriAction, appPackage, appToken);
}
@Override
@ -88,7 +93,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
}
String redirectUriAction = getAction(request, "authorize");
String appPackage = getAppPackage(request);
return resolve(request, registrationId, redirectUriAction, appPackage);
String appToken = getAppToken(request);
return resolve(request, registrationId, redirectUriAction, appPackage, appToken);
}
private String getAction(HttpServletRequest request, String defaultAction) {
@ -103,8 +109,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
return request.getParameter("pkg");
}
private String getAppToken(HttpServletRequest request) {
return request.getParameter("appToken");
}
@SuppressWarnings("deprecation")
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage) {
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) {
if (registrationId == null) {
return null;
}
@ -117,10 +127,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza
Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId());
if (!StringUtils.isEmpty(appPackage)) {
String callbackUrlScheme = this.oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), appPackage);
if (StringUtils.isEmpty(callbackUrlScheme)) {
throw new IllegalArgumentException("Invalid package: " + appPackage + ". No package info found for Client Registration.");
if (StringUtils.isEmpty(appToken)) {
throw new IllegalArgumentException("Invalid application token.");
} else {
String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage);
if (StringUtils.isEmpty(appSecret)) {
throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package.");
}
String callbackUrlScheme = this.oAuth2AppTokenFactory.validateTokenAndGetCallbackUrlScheme(appPackage, appToken, appSecret);
attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme);
}
}

View File

@ -0,0 +1,69 @@
/**
* 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.model.token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class OAuth2AppTokenFactory {
private static final String CALLBACK_URL_SCHEME = "callbackUrlScheme";
private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5);
public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) {
Jws<Claims> jwsClaims;
try {
jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken);
}
catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) {
throw new IllegalArgumentException("Invalid Application token: ", ex);
} catch (ExpiredJwtException expiredEx) {
throw new IllegalArgumentException("Application token expired", expiredEx);
}
Claims claims = jwsClaims.getBody();
Date expiration = claims.getExpiration();
if (expiration == null) {
throw new IllegalArgumentException("Application token must have expiration date");
}
long timeDiff = expiration.getTime() - System.currentTimeMillis();
if (timeDiff > MAX_EXPIRATION_TIME_DIFF_MS) {
throw new IllegalArgumentException("Application token expiration time can't be longer than 5 minutes");
}
if (!claims.getIssuer().equals(appPackage)) {
throw new IllegalArgumentException("Application token issuer doesn't match application package");
}
String callbackUrlScheme = claims.get(CALLBACK_URL_SCHEME, String.class);
if (StringUtils.isEmpty(callbackUrlScheme)) {
throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme");
}
return callbackUrlScheme;
}
}

View File

@ -42,5 +42,5 @@ public interface OAuth2Service {
List<OAuth2Registration> findAllRegistrations();
String findCallbackUrlScheme(UUID registrationId, String pkgName);
String findAppSecret(UUID registrationId, String pkgName);
}

View File

@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData<OAuth2MobileId> {
private OAuth2ParamsId oauth2ParamsId;
private String pkgName;
private String callbackUrlScheme;
private String appSecret;
public OAuth2Mobile(OAuth2Mobile mobile) {
super(mobile);
this.oauth2ParamsId = mobile.oauth2ParamsId;
this.pkgName = mobile.pkgName;
this.callbackUrlScheme = mobile.callbackUrlScheme;
this.appSecret = mobile.appSecret;
}
}

View File

@ -30,5 +30,5 @@ import lombok.ToString;
@Builder
public class OAuth2MobileInfo {
private String pkgName;
private String callbackUrlScheme;
private String appSecret;
}

View File

@ -418,7 +418,7 @@ public class ModelConstants {
public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile";
public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id";
public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name";
public static final String OAUTH2_CALLBACK_URL_SCHEME_PROPERTY = "callback_url_scheme";
public static final String OAUTH2_APP_SECRET_PROPERTY = "app_secret";
public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info";
public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration";

View File

@ -40,8 +40,8 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
@Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY)
private String pkgName;
@Column(name = ModelConstants.OAUTH2_CALLBACK_URL_SCHEME_PROPERTY)
private String callbackUrlScheme;
@Column(name = ModelConstants.OAUTH2_APP_SECRET_PROPERTY)
private String appSecret;
public OAuth2MobileEntity() {
super();
@ -56,7 +56,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
this.oauth2ParamsId = mobile.getOauth2ParamsId().getId();
}
this.pkgName = mobile.getPkgName();
this.callbackUrlScheme = mobile.getCallbackUrlScheme();
this.appSecret = mobile.getAppSecret();
}
@Override
@ -66,7 +66,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
mobile.setCreatedTime(createdTime);
mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId));
mobile.setPkgName(pkgName);
mobile.setCallbackUrlScheme(callbackUrlScheme);
mobile.setAppSecret(appSecret);
return mobile;
}
}

View File

@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao<OAuth2Registration> {
List<OAuth2Registration> findByOAuth2ParamsId(UUID oauth2ParamsId);
String findCallbackUrlScheme(UUID id, String pkgName);
String findAppSecret(UUID id, String pkgName);
}

View File

@ -21,7 +21,23 @@ import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.*;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Domain;
import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Info;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Mobile;
import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Params;
import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo;
import org.thingsboard.server.common.data.oauth2.PlatformType;
import org.thingsboard.server.common.data.oauth2.SchemeType;
import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType;
import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto;
import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo;
import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo;
@ -36,7 +52,11 @@ import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao;
import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao;
import javax.transaction.Transactional;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -164,11 +184,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
}
@Override
public String findCallbackUrlScheme(UUID id, String pkgName) {
log.trace("Executing findCallbackUrlScheme [{}][{}]", id, pkgName);
public String findAppSecret(UUID id, String pkgName) {
log.trace("Executing findAppSecret [{}][{}]", id, pkgName);
validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id);
validateString(pkgName, "Incorrect package name");
return oauth2RegistrationDao.findCallbackUrlScheme(id, pkgName);
return oauth2RegistrationDao.findAppSecret(id, pkgName);
}
@ -323,8 +343,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se
if (StringUtils.isEmpty(mobileInfo.getPkgName())) {
throw new DataValidationException("Package should be specified!");
}
if (StringUtils.isEmpty(mobileInfo.getCallbackUrlScheme())) {
throw new DataValidationException("Callback URL scheme should be specified!");
if (StringUtils.isEmpty(mobileInfo.getAppSecret())) {
throw new DataValidationException("Application secret should be specified!");
}
if (mobileInfo.getAppSecret().length() < 16) {
throw new DataValidationException("Application secret should be at least 16 characters!");
}
}
oauth2Params.getMobileInfos().stream()

View File

@ -148,7 +148,7 @@ public class OAuth2Utils {
public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) {
return OAuth2MobileInfo.builder()
.pkgName(mobile.getPkgName())
.callbackUrlScheme(mobile.getCallbackUrlScheme())
.appSecret(mobile.getAppSecret())
.build();
}
@ -191,7 +191,7 @@ public class OAuth2Utils {
OAuth2Mobile mobile = new OAuth2Mobile();
mobile.setOauth2ParamsId(oauth2ParamsId);
mobile.setPkgName(mobileInfo.getPkgName());
mobile.setCallbackUrlScheme(mobileInfo.getCallbackUrlScheme());
mobile.setAppSecret(mobileInfo.getAppSecret());
return mobile;
}

View File

@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao<OAuth2RegistrationE
}
@Override
public String findCallbackUrlScheme(UUID id, String pkgName) {
return repository.findCallbackUrlScheme(id, pkgName);
public String findAppSecret(UUID id, String pkgName) {
return repository.findAppSecret(id, pkgName);
}
}

View File

@ -42,12 +42,12 @@ public interface OAuth2RegistrationRepository extends CrudRepository<OAuth2Regis
List<OAuth2RegistrationEntity> findByOauth2ParamsId(UUID oauth2ParamsId);
@Query("SELECT mobile.callbackUrlScheme " +
@Query("SELECT mobile.appSecret " +
"FROM OAuth2MobileEntity mobile " +
"LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " +
"WHERE reg.id = :registrationId " +
"AND mobile.pkgName = :pkgName")
String findCallbackUrlScheme(@Param("registrationId") UUID id,
@Param("pkgName") String pkgName);
String findAppSecret(@Param("registrationId") UUID id,
@Param("pkgName") String pkgName);
}

View File

@ -431,7 +431,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile (
oauth2_params_id uuid NOT NULL,
created_time bigint NOT NULL,
pkg_name varchar(255),
callback_url_scheme varchar(255),
app_secret varchar(2048),
CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE,
CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name)
);

View File

@ -468,7 +468,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile (
oauth2_params_id uuid NOT NULL,
created_time bigint NOT NULL,
pkg_name varchar(255),
callback_url_scheme varchar(255),
app_secret varchar(2048),
CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE,
CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name)
);

View File

@ -15,8 +15,8 @@
*/
package org.thingsboard.server.dao.service;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@ -487,7 +487,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
}
@Test
public void testFindCallbackUrlScheme() {
public void testFindAppSecret() {
OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList(
OAuth2ParamsInfo.builder()
.domainInfos(Lists.newArrayList(
@ -496,8 +496,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.mobileInfos(Lists.newArrayList(
OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(),
OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build()
validMobileInfo("com.test.pkg1", "testPkg1AppSecret"),
validMobileInfo("com.test.pkg2", "testPkg2AppSecret")
))
.clientRegistrations(Lists.newArrayList(
validRegistrationInfo(),
@ -527,14 +527,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) {
String[] segments = clientInfo.getUrl().split("/");
String registrationId = segments[segments.length-1];
String callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg1");
Assert.assertNotNull(callbackUrlScheme);
Assert.assertEquals("testPkg1Callback", callbackUrlScheme);
callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg2");
Assert.assertNotNull(callbackUrlScheme);
Assert.assertEquals("testPkg2Callback", callbackUrlScheme);
callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg3");
Assert.assertNull(callbackUrlScheme);
String appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg1");
Assert.assertNotNull(appSecret);
Assert.assertEquals("testPkg1AppSecret", appSecret);
appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg2");
Assert.assertNotNull(appSecret);
Assert.assertEquals("testPkg2AppSecret", appSecret);
appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg3");
Assert.assertNull(appSecret);
}
}
@ -548,8 +548,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build()
))
.mobileInfos(Lists.newArrayList(
OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(),
OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build()
validMobileInfo("com.test.pkg1", "testPkg1Callback"),
validMobileInfo("com.test.pkg2", "testPkg2Callback")
))
.clientRegistrations(Lists.newArrayList(
validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)),
@ -651,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest {
)
.build();
}
private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) {
return OAuth2MobileInfo.builder().pkgName(pkgName)
.appSecret(appSecret != null ? appSecret : RandomStringUtils.randomAlphanumeric(24))
.build();
}
}

View File

@ -445,3 +445,14 @@ export function validateEntityId(entityId: EntityId | null): boolean {
export function isMobileApp(): boolean {
return isDefined((window as any).flutter_inappwebview);
}
const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const alphanumericCharactersLength = alphanumericCharacters.length;
export function randomAlphanumeric(length: number): string {
let result = '';
for ( let i = 0; i < length; i++ ) {
result += alphanumericCharacters.charAt(Math.floor(Math.random() * alphanumericCharactersLength));
}
return result;
}

View File

@ -90,23 +90,26 @@
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.redirect-uri-template</mat-label>
<input matInput [value]="redirectURI(domainInfo)" readonly>
<button mat-icon-button color="primary" matSuffix type="button"
ngxClipboard cbContent="{{ redirectURI(domainInfo) }}"
matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
matTooltipPosition="above">
<mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
<tb-copy-button
matSuffix
color="primary"
[copyText]="redirectURI(domainInfo)"
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
tooltipPosition="above"
mdiIcon="mdi:clipboard-arrow-left">
</tb-copy-button>
</mat-form-field>
<mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block">
<mat-label></mat-label>
<input matInput [value]="redirectURIMixed(domainInfo)" readonly>
<button mat-icon-button color="primary" matSuffix type="button"
ngxClipboard cbContent="{{ redirectURIMixed(domainInfo) }}"
matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
matTooltipPosition="above">
<mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon>
</button>
<tb-copy-button
matSuffix
color="primary"
[copyText]="redirectURIMixed(domainInfo)"
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
tooltipPosition="above"
mdiIcon="mdi:clipboard-arrow-left">
</tb-copy-button>
</mat-form-field>
</div>
</div>
@ -144,18 +147,32 @@
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px">
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<div fxFlex fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>admin.oauth2.mobile-package</mat-label>
<input matInput formControlName="pkgName" required>
<input matInput formControlName="pkgName" placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required>
<mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint>
</mat-form-field>
<mat-error *ngIf="mobileInfo.hasError('unique')">
{{ 'admin.oauth2.mobile-package-unique' | translate }}
</mat-error>
</div>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.mobile-callback-url-scheme</mat-label>
<input matInput formControlName="callbackUrlScheme" required>
</mat-form-field>
<div fxFlex fxLayout="row">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.mobile-app-secret</mat-label>
<textarea matInput formControlName="appSecret" rows="1" required></textarea>
<tb-copy-button
matSuffix
color="primary"
[copyText]="mobileInfo.get('appSecret').value"
tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}"
tooltipPosition="above"
mdiIcon="mdi:clipboard-arrow-left">
</tb-copy-button>
<mat-error *ngIf="mobileInfo.get('appSecret').invalid">
{{ 'admin.oauth2.invalid-mobile-app-secret' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<div fxLayout="column" fxLayoutAlign="center start">
<button type="button" mat-icon-button color="primary"

View File

@ -42,7 +42,7 @@ import { WINDOW } from '@core/services/window.service';
import { forkJoin, Subscription } from 'rxjs';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { isDefined, isDefinedAndNotNull } from '@core/utils';
import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils';
import { OAuth2Service } from '@core/http/oauth2.service';
import { ActivatedRoute } from '@angular/router';
@ -275,7 +275,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup {
return this.fb.group({
pkgName: [mobileInfo?.pkgName, [Validators.required]],
callbackUrlScheme: [mobileInfo?.callbackUrlScheme, [Validators.required]],
appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048),
Validators.pattern(/^[A-Za-z0-9]+$/)]],
}, {validators: this.uniquePkgNameValidator});
}
@ -529,7 +530,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
addMobileInfo(control: AbstractControl): void {
this.mobileInfos(control).push(this.buildMobileInfoForm({
pkgName: '',
callbackUrlScheme: ''
appSecret: randomAlphanumeric(24)
}));
}

View File

@ -16,11 +16,16 @@
-->
<button mat-icon-button
type="button"
[color]="color"
[disabled]="disabled"
[matTooltip]="matTooltipText"
[matTooltipPosition]="matTooltipPosition"
(click)="copy($event)">
<mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}">
{{ iconSymbol }}
<mat-icon [svgIcon]="mdiIcon" [ngStyle]="style" *ngIf="!copied; else copiedTemplate">
{{ icon }}
</mat-icon>
<ng-template #copiedTemplate>
<mat-icon [ngStyle]="style" class="copied">done</mat-icon>
</ng-template>
</button>

View File

@ -26,7 +26,6 @@ import { TranslateService } from '@ngx-translate/core';
})
export class CopyButtonComponent {
private copedIcon = '';
private timer;
copied = false;
@ -52,6 +51,9 @@ export class CopyButtonComponent {
@Input()
style: {[key: string]: any} = {};
@Input()
color: string;
@Output()
successCopied = new EventEmitter<string>();
@ -67,23 +69,13 @@ export class CopyButtonComponent {
}
this.clipboardService.copy(this.copyText);
this.successCopied.emit(this.copyText);
this.copedIcon = 'done';
this.copied = true;
this.timer = setTimeout(() => {
this.copedIcon = null;
this.copied = false;
this.cd.detectChanges();
}, 1500);
}
get iconSymbol(): string {
return this.copedIcon || this.icon;
}
get mdiIconSymbol(): string {
return this.copedIcon ? '' : this.mdiIcon;
}
get matTooltipText(): string {
return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText;
}

View File

@ -34,7 +34,7 @@ export interface OAuth2DomainInfo {
export interface OAuth2MobileInfo {
pkgName: string;
callbackUrlScheme: string;
appSecret: string;
}
export enum DomainSchema{

View File

@ -224,8 +224,12 @@
"mobile-apps": "Mobile applications",
"no-mobile-apps": "No applications configured",
"mobile-package": "Application package",
"mobile-package-placeholder": "Ex.: my.example.app",
"mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.",
"mobile-package-unique": "Application package must be unique.",
"mobile-callback-url-scheme": "Callback URL scheme",
"mobile-app-secret": "Application secret",
"invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.",
"copy-mobile-app-secret": "Copy application secret",
"add-mobile-app": "Add application",
"delete-mobile-app": "Delete application info",
"providers": "Providers",