View Javadoc
1   /*
2    * Copyright © 2025 rosestack.github.io
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 io.github.rose.security.rest.mfa;
17  
18  import static org.apache.commons.lang3.StringUtils.repeat;
19  
20  import io.github.rose.security.SecurityProperties;
21  import io.github.rose.security.rest.mfa.config.EmailMfaConfig;
22  import io.github.rose.security.rest.mfa.config.MfaConfig;
23  import io.github.rose.security.rest.mfa.config.SmsMfaConfig;
24  import io.github.rose.security.rest.mfa.provider.MfaProvider;
25  import io.github.rose.security.rest.mfa.provider.MfaProviderConfig;
26  import io.github.rose.security.rest.mfa.provider.MfaProviderType;
27  import io.github.rose.security.support.TokenFactory;
28  import io.github.rose.security.util.SecurityUser;
29  import io.github.rose.security.util.SecurityUtils;
30  import io.github.rose.security.util.TokenPair;
31  import java.util.*;
32  import java.util.stream.Collectors;
33  import org.apache.commons.lang3.StringUtils;
34  import org.springframework.beans.factory.annotation.Autowired;
35  import org.springframework.stereotype.Service;
36  
37  @Service
38  public class DefaultMfaSettingService implements MfaSettingService {
39  
40      private static final RuntimeException PROVIDER_NOT_CONFIGURED_ERROR =
41              new RuntimeException("mfa provider is not configured");
42  
43      private static final RuntimeException PROVIDER_NOT_AVAILABLE_ERROR =
44              new RuntimeException("mfa provider is not available");
45  
46      private final Map<MfaProviderType, MfaProvider<MfaProviderConfig, MfaConfig>> providers =
47              new EnumMap<>(MfaProviderType.class);
48  
49      private final TokenFactory tokenFactory;
50  
51      private final MfaProperties mfaProperties;
52  
53      private final SecurityProperties securityProperties;
54  
55      public DefaultMfaSettingService(
56              TokenFactory tokenFactory, MfaProperties mfaProperties, SecurityProperties securityProperties) {
57          this.tokenFactory = tokenFactory;
58          this.mfaProperties = mfaProperties;
59          this.securityProperties = securityProperties;
60      }
61  
62      private static String obfuscate(
63              String input, int seenMargin, char obfuscationChar, int startIndexInclusive, int endIndexExclusive) {
64          String part = input.substring(startIndexInclusive, endIndexExclusive);
65          String obfuscatedPart;
66          if (part.length() <= seenMargin * 2) {
67              obfuscatedPart = repeat(obfuscationChar, part.length());
68          } else {
69              obfuscatedPart = part.substring(0, seenMargin)
70                      + repeat(obfuscationChar, part.length() - seenMargin * 2)
71                      + part.substring(part.length() - seenMargin);
72          }
73          return input.substring(0, startIndexInclusive) + obfuscatedPart + input.substring(endIndexExclusive);
74      }
75  
76      @Autowired
77      private void setProviders(Collection<MfaProvider> providers) {
78          providers.forEach(provider -> {
79              this.providers.put(provider.getType(), provider);
80          });
81      }
82  
83      @Override
84      public void prepareVerificationCode() {
85          MfaConfig mfaConfig = mfaProperties.getDefaultConfig();
86          MfaProviderConfig providerConfig = mfaProperties
87                  .getProviderConfig(mfaConfig.getProviderType())
88                  .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
89          getTwoFaProvider(mfaConfig.getProviderType())
90                  .prepareVerificationCode(SecurityUtils.getCurrentUser(), providerConfig, mfaConfig);
91      }
92  
93      @Override
94      public TokenPair checkVerificationCode(String verificationCode) {
95          SecurityUser user = SecurityUtils.getCurrentUser();
96          MfaConfig mfaConfig = mfaProperties.getDefaultConfig();
97          MfaProviderConfig providerConfig = mfaProperties
98                  .getProviderConfig(mfaConfig.getProviderType())
99                  .orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
100 
101         boolean verificationSuccess = false;
102         if (StringUtils.isNotBlank(verificationCode)) {
103             if (StringUtils.isNumeric(verificationCode) || mfaConfig.getProviderType() == MfaProviderType.BACKUP_CODE) {
104                 verificationSuccess = getTwoFaProvider(mfaConfig.getProviderType())
105                         .checkVerificationCode(user, verificationCode, providerConfig, mfaConfig);
106             }
107         }
108 
109         if (verificationSuccess) {
110             return tokenFactory.createTokenPair(user);
111         } else {
112             throw new RuntimeException("Verification code is incorrect");
113         }
114     }
115 
116     private MfaProvider<MfaProviderConfig, MfaConfig> getTwoFaProvider(MfaProviderType providerType) {
117         return Optional.ofNullable(providers.get(providerType)).orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR);
118     }
119 
120     @Override
121     public List<MfaAuthController.TwoFaProviderInfo> getAvailableTwoFaProviders() {
122         return mfaProperties.getAllConfigs().stream()
123                 .map(config -> {
124                     String contact = null;
125                     switch (config.getProviderType()) {
126                         case SMS:
127                             String phoneNumber = ((SmsMfaConfig) config).getPhoneNumber();
128                             contact =
129                                     obfuscate(phoneNumber, 2, '*', phoneNumber.indexOf('+') + 1, phoneNumber.length());
130                             break;
131                         case EMAIL:
132                             String email = ((EmailMfaConfig) config).getEmail();
133                             contact = obfuscate(email, 2, '*', 0, email.indexOf('@'));
134                             break;
135                     }
136                     return MfaAuthController.TwoFaProviderInfo.builder()
137                             .type(config.getProviderType())
138                             .useByDefault(config.isUseByDefault())
139                             .contact(contact)
140                             .minVerificationCodeSendPeriod(mfaProperties.getMinVerificationCodeSendPeriod())
141                             .build();
142                 })
143                 .collect(Collectors.toList());
144     }
145 }