From e79fec534f0922efbde827b71390484564a2000e Mon Sep 17 00:00:00 2001 From: mapidentity Date: Sun, 21 Jul 2024 21:28:34 +0000 Subject: [PATCH] Support Keycloak 25 --- .../workflows/compile-and-liveness-check.yml | 2 +- .github/workflows/mavenpublish.yml | 4 +- jboss-cli/module-add.cli | 2 +- keycloak-phone-provider/pom.xml | 21 ++- .../cc/coopersoft/common/OptionalUtils.java | 7 +- .../cc/coopersoft/keycloak/phone/Utils.java | 110 +++++++------ .../browser/PhoneUsernamePasswordForm.java | 88 +++++----- .../browser/SmsOtpMfaAuthenticator.java | 87 +++++----- .../SmsOtpMfaAuthenticatorFactory.java | 19 +-- ...uthenticationCodeAuthenticatorFactory.java | 46 +++--- .../BaseDirectGrantAuthenticator.java | 28 ++-- .../EverybodyPhoneAuthenticator.java | 15 +- .../EverybodyPhoneAuthenticatorFactory.java | 7 +- .../directgrant/PhoneNumberAuthenticator.java | 16 +- .../PhoneNumberAuthenticatorFactory.java | 5 +- .../resetcred/ResetCredentialWithPhone.java | 74 +++++---- .../forms/RegistrationPhoneUserCreation.java | 89 +++++----- .../RegistrationPhoneVerificationCode.java | 28 ++-- .../RegistrationRedirectParametersReader.java | 18 +-- .../ConfigSmsOtpRequiredAction.java | 152 +++++++++--------- .../UpdatePhoneNumberRequiredAction.java | 25 +-- .../credential/PhoneOtpCredentialModel.java | 71 ++++---- .../PhoneOtpCredentialProviderFactory.java | 3 +- .../phone/providers/jpa/TokenCode.java | 85 +++++----- .../phone/providers/rest/SmsResource.java | 2 +- .../rest/SmsResourceProviderFactory.java | 4 - .../providers/rest/TokenCodeResource.java | 20 +-- .../rest/VerificationCodeResource.java | 23 +-- .../phone/providers/spi/PhoneSpi.java | 6 +- .../spi/impl/DefaultPhoneProvider.java | 41 +++-- .../DefaultPhoneVerificationCodeProvider.java | 135 ++++++++-------- keycloak-sms-provider-aws-sns/pom.xml | 2 +- ...nsMessageSenderServiceProviderFactory.java | 2 +- .../sender/AwsSnsSmsSenderService.java | 10 +- .../BulksmsSmsSenderServiceProvider.java | 8 +- .../CloopenSmsSenderServiceProvider.java | 61 +++---- ...myMessageSenderServiceProviderFactory.java | 2 +- .../sender/DummySmsSenderService.java | 5 +- .../TencentSmsSenderServiceProvider.java | 113 +++++++------ ...ceMessageSenderServiceProviderFactory.java | 2 +- .../TotalVoiceSmsSenderServiceProvider.java | 5 +- ...ioMessageSenderServiceProviderFactory.java | 2 +- .../TwilioSmsSenderServiceProvider.java | 5 +- keycloak-sms-provider-twofactorapi/pom.xml | 5 + ...orMessageSenderServiceProviderFactory.java | 2 +- .../TwoFactorSmsSenderServiceProvider.java | 7 +- pom.xml | 17 +- 47 files changed, 759 insertions(+), 722 deletions(-) diff --git a/.github/workflows/compile-and-liveness-check.yml b/.github/workflows/compile-and-liveness-check.yml index 7a79ed99..14eaec0a 100644 --- a/.github/workflows/compile-and-liveness-check.yml +++ b/.github/workflows/compile-and-liveness-check.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '17' + java-version: '21' - shell: bash run: cd examples && ./docker-build.sh test - run: cd examples && docker compose --verbose up --build --wait diff --git a/.github/workflows/mavenpublish.yml b/.github/workflows/mavenpublish.yml index 7d3a9f7e..d4a6c3f8 100644 --- a/.github/workflows/mavenpublish.yml +++ b/.github/workflows/mavenpublish.yml @@ -13,10 +13,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 18 + - name: Set up JDK 21 uses: actions/setup-java@v1 with: - java-version: 18 + java-version: 21 server-id: github # Value of the distributionManagement/repository/id field of the pom.xml settings-path: ${{ github.workspace }} # location for the settings.xml file diff --git a/jboss-cli/module-add.cli b/jboss-cli/module-add.cli index 9141136f..0a42d069 100644 --- a/jboss-cli/module-add.cli +++ b/jboss-cli/module-add.cli @@ -3,7 +3,7 @@ # main provider module add --name=com.googlecode.libphonenumber --resources=libphonenumber-8.13.7.jar -module add --name=keycloak-phone-provider --resources=keycloak-phone-provider.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-common,org.hibernate,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.jboss.logging,javax.api,javax.ws.rs.api,javax.transaction.api,javax.persistence.api,org.jboss.resteasy.resteasy-jaxrs,org.apache.httpcomponents,org.apache.commons.lang,javax.xml.bind.api,com.squareup.okhttp3,com.googlecode.libphonenumber +module add --name=keycloak-phone-provider --resources=keycloak-phone-provider.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-common,org.hibernate,org.keycloak.keycloak-server-spi,org.keycloak.keycloak-server-spi-private,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.jboss.logging,javax.api,jakarta.ws.rs.api,javax.transaction.api,javax.persistence.api,org.jboss.resteasy.resteasy-jaxrs,org.apache.httpcomponents,org.apache.commons.lang,javax.xml.bind.api,com.squareup.okhttp3,com.googlecode.libphonenumber # dummy provider module add --name=keycloak-sms-provider-dummy --resources=keycloak-sms-provider-dummy.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-server-spi,org.jboss.logging,keycloak-phone-provider diff --git a/keycloak-phone-provider/pom.xml b/keycloak-phone-provider/pom.xml index 9cffc6da..2c150073 100644 --- a/keycloak-phone-provider/pom.xml +++ b/keycloak-phone-provider/pom.xml @@ -20,9 +20,24 @@ provided - com.googlecode.libphonenumber - libphonenumber - 8.13.7 + jakarta.validation + jakarta.validation-api + 3.1.0 + + + com.googlecode.libphonenumber + libphonenumber + 8.13.41 + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + org.jboss.resteasy + resteasy-jaxrs + 3.15.6.Final diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/common/OptionalUtils.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/common/OptionalUtils.java index 00ab66ba..fff81e69 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/common/OptionalUtils.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/common/OptionalUtils.java @@ -3,19 +3,18 @@ import org.keycloak.services.validation.Validation; import java.util.Optional; -import java.util.regex.Pattern; public class OptionalUtils { - public static Optional ofEmpty(String str){ + public static Optional ofEmpty(String str) { return Validation.isEmpty(str) ? Optional.empty() : Optional.of(str); } - public static Optional ofBlank(String str){ + public static Optional ofBlank(String str) { return Validation.isBlank(str) ? Optional.empty() : Optional.of(str).map(String::trim); } - public static Optional ofTrue(boolean b){ + public static Optional ofTrue(boolean b) { return b ? Optional.of(true) : Optional.empty(); } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/Utils.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/Utils.java index 017a64b9..5c075a78 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/Utils.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/Utils.java @@ -1,23 +1,17 @@ package cc.coopersoft.keycloak.phone; import cc.coopersoft.common.OptionalUtils; -import cc.coopersoft.keycloak.phone.credential.PhoneOtpCredentialModel; import cc.coopersoft.keycloak.phone.providers.exception.PhoneNumberInvalidException; import cc.coopersoft.keycloak.phone.providers.spi.PhoneProvider; import org.jboss.logging.Logger; -import org.keycloak.credential.CredentialModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import com.google.i18n.phonenumbers.*; import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat; -import org.keycloak.models.credential.dto.OTPSecretData; -import org.keycloak.services.validation.Validation; -import org.keycloak.util.JsonSerialization; -import javax.validation.constraints.NotNull; -import java.io.IOException; +import jakarta.validation.constraints.NotNull; import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -25,119 +19,123 @@ public class Utils { private static final Logger logger = Logger.getLogger(Utils.class); - public static Optional findUserByPhone(KeycloakSession session,RealmModel realm, String phoneNumber){ + public static Optional findUserByPhone(KeycloakSession session, RealmModel realm, String phoneNumber) { var userProvider = session.users(); Set numbers = new HashSet<>(); numbers.add(phoneNumber); - if (session.getProvider(PhoneProvider.class).compatibleMode()){ + if (session.getProvider(PhoneProvider.class).compatibleMode()) { var phoneNumberUtil = PhoneNumberUtil.getInstance(); try { var parsedNumber = phoneNumberUtil.parse(phoneNumber, defaultRegion(session)); - if (parsedNumber.hasNationalNumber()){ - numbers.add(String.valueOf(parsedNumber.getNationalNumber())) ; + if (parsedNumber.hasNationalNumber()) { + numbers.add(String.valueOf(parsedNumber.getNationalNumber())); } for (PhoneNumberFormat format : PhoneNumberFormat.values()) { numbers.add(phoneNumberUtil.format(parsedNumber, format)); } - }catch (NumberParseException e){ - logger.warn(String.format("%s is not a valid phone number!",phoneNumber),e); + } catch (NumberParseException e) { + logger.warn(String.format("%s is not a valid phone number!", phoneNumber), e); } } - return numbers.stream().flatMap(number -> userProvider - .searchForUserByUserAttributeStream(realm,"phoneNumber", number)) - .max((u1, u2) -> { - var result = comparatorAttributesAnyMatch(u1,u2,"phoneNumberVerified","true"::equals); - if (result == 0){ - result = comparatorAttributesAnyMatch(u1,u2,"phoneNumber", number -> number.startsWith("+")); - } - return result; - }); + .searchForUserByUserAttributeStream(realm, "phoneNumber", number)) + .max((u1, u2) -> { + var result = comparatorAttributesAnyMatch(u1, u2, "phoneNumberVerified", "true"::equals); + if (result == 0) { + result = comparatorAttributesAnyMatch(u1, u2, "phoneNumber", number -> number.startsWith("+")); + } + return result; + }); } -// public static Optional findUserByPhone(UserProvider userProvider, RealmModel realm, String phoneNumber, String notIs){ -// return userProvider -// .searchForUserByUserAttributeStream(realm, "phoneNumber", phoneNumber) -// .filter(u -> !u.getId().equals(notIs)) -// .max(comparatorUser()); -// } + // public static Optional findUserByPhone(UserProvider userProvider, + // RealmModel realm, String phoneNumber, String notIs){ + // return userProvider + // .searchForUserByUserAttributeStream(realm, "phoneNumber", phoneNumber) + // .filter(u -> !u.getId().equals(notIs)) + // .max(comparatorUser()); + // } private static int comparatorAttributesAnyMatch(UserModel user1, UserModel user2, - String attribute, Predicate predicate){ + String attribute, Predicate predicate) { return Boolean.compare(user1.getAttributeStream(attribute).anyMatch(predicate), user2.getAttributeStream(attribute).anyMatch(predicate)); } - private static Optional localeToCountry(String locale){ + private static Optional localeToCountry(String locale) { return OptionalUtils.ofBlank(locale).flatMap(l -> { Pattern countryRegx = Pattern.compile("[^a-z]*\\-?([A-Z]{2,3})"); return Optional.of(countryRegx.matcher(l)) - .flatMap(m -> m.find() ? OptionalUtils.ofBlank(m.group(1)) : Optional.empty()); + .flatMap(m -> m.find() ? OptionalUtils.ofBlank(m.group(1)) : Optional.empty()); }); } - - private static String defaultRegion(KeycloakSession session){ + private static String defaultRegion(KeycloakSession session) { var defaultRegion = session.getProvider(PhoneProvider.class).defaultPhoneRegion(); - return defaultRegion.orElseGet(() -> localeToCountry(session.getContext().getRealm().getDefaultLocale()).orElse(null)); + return defaultRegion + .orElseGet(() -> localeToCountry(session.getContext().getRealm().getDefaultLocale()).orElse(null)); } /** - * Parses a phone number with google's libphonenumber and then outputs it's - * international canonical form - * - */ - public static String canonicalizePhoneNumber(KeycloakSession session,@NotNull String phoneNumber) throws PhoneNumberInvalidException { + * Parses a phone number with google's libphonenumber and then outputs it's + * international canonical form + * + */ + public static String canonicalizePhoneNumber(KeycloakSession session, @NotNull String phoneNumber) + throws PhoneNumberInvalidException { var provider = session.getProvider(PhoneProvider.class); - var phoneNumberUtil = PhoneNumberUtil.getInstance(); var resultPhoneNumber = phoneNumber.trim(); var defaultRegion = defaultRegion(session); - logger.info(String.format("default region '%s' will be used",defaultRegion)); + logger.info(String.format("default region '%s' will be used", defaultRegion)); try { var parsedNumber = phoneNumberUtil.parse(resultPhoneNumber, defaultRegion); if (provider.validPhoneNumber() && !phoneNumberUtil.isValidNumber(parsedNumber)) { - logger.info(String.format("Phone number [%s] Valid fail with google's libphonenumber",resultPhoneNumber)); + logger.info( + String.format("Phone number [%s] Valid fail with google's libphonenumber", resultPhoneNumber)); throw new PhoneNumberInvalidException(PhoneNumberInvalidException.ErrorType.VALID_FAIL, - String.format("Phone number [%s] Valid fail with google's libphonenumber",resultPhoneNumber)); + String.format("Phone number [%s] Valid fail with google's libphonenumber", resultPhoneNumber)); } var canonicalizeFormat = provider.canonicalizePhoneNumber(); try { resultPhoneNumber = canonicalizeFormat - .map(PhoneNumberFormat::valueOf) - .map(format -> phoneNumberUtil.format(parsedNumber, format)) - .orElse(resultPhoneNumber); - }catch (RuntimeException e){ - logger.warn(String.format("canonicalize format param error! '%s' is not in supported list: %s, E164 Will be used.", - Arrays.toString(PhoneNumberFormat.values()), - canonicalizeFormat.orElse("")),e); + .map(PhoneNumberFormat::valueOf) + .map(format -> phoneNumberUtil.format(parsedNumber, format)) + .orElse(resultPhoneNumber); + } catch (RuntimeException e) { + logger.warn(String.format( + "canonicalize format param error! '%s' is not in supported list: %s, E164 Will be used.", + Arrays.toString(PhoneNumberFormat.values()), + canonicalizeFormat.orElse("")), e); resultPhoneNumber = phoneNumberUtil.format(parsedNumber, PhoneNumberFormat.E164); } var phoneNumberRegex = provider.phoneNumberRegex(); - if (!phoneNumberRegex.map(resultPhoneNumber::matches).orElse(true)){ - logger.info(String.format("Phone number [%s] not match regex '%s'",resultPhoneNumber, phoneNumberRegex.orElse(""))); + if (!phoneNumberRegex.map(resultPhoneNumber::matches).orElse(true)) { + logger.info(String.format("Phone number [%s] not match regex '%s'", resultPhoneNumber, + phoneNumberRegex.orElse(""))); throw new PhoneNumberInvalidException(PhoneNumberInvalidException.ErrorType.NOT_SUPPORTED, - String.format("Phone number [%s] not match regex '%s'",resultPhoneNumber, phoneNumberRegex.orElse(""))); + String.format("Phone number [%s] not match regex '%s'", resultPhoneNumber, + phoneNumberRegex.orElse(""))); } return resultPhoneNumber; - }catch (NumberParseException e){ + } catch (NumberParseException e) { logger.info(e); throw new PhoneNumberInvalidException(e); } } - public static boolean isDuplicatePhoneAllowed(KeycloakSession session){ + public static boolean isDuplicatePhoneAllowed(KeycloakSession session) { return session.getProvider(PhoneProvider.class).isDuplicatePhoneAllowed(); } - public static int getOtpExpires(KeycloakSession session){ + public static int getOtpExpires(KeycloakSession session) { return session.getProvider(PhoneProvider.class).otpExpires(); } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/PhoneUsernamePasswordForm.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/PhoneUsernamePasswordForm.java index 2f3e2c99..7979d8b7 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/PhoneUsernamePasswordForm.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/PhoneUsernamePasswordForm.java @@ -27,8 +27,8 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; import java.util.List; @@ -37,7 +37,7 @@ import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE; import static org.keycloak.services.validation.Validation.FIELD_USERNAME; -public class PhoneUsernamePasswordForm extends UsernamePasswordForm implements Authenticator, AuthenticatorFactory { +public class PhoneUsernamePasswordForm extends UsernamePasswordForm implements AuthenticatorFactory { private static final Logger logger = Logger.getLogger(PhoneUsernamePasswordForm.class); @@ -51,29 +51,33 @@ public class PhoneUsernamePasswordForm extends UsernamePasswordForm implements A /** * use phone and password login + * * @param context * @return */ - private boolean isLoginWithPhoneNumber(AuthenticationFlowContext context){ + private boolean isLoginWithPhoneNumber(AuthenticationFlowContext context) { return context.getAuthenticatorConfig() == null || - context.getAuthenticatorConfig().getConfig().getOrDefault(CONFIG_IS_LOGIN_WITH_PHONE_NUMBER, "true").equals("true"); + context.getAuthenticatorConfig().getConfig().getOrDefault(CONFIG_IS_LOGIN_WITH_PHONE_NUMBER, "true") + .equals("true"); } /** * use phone and verify code login + * * @param context * @return */ - private boolean isSupportPhone(AuthenticationFlowContext context){ + private boolean isSupportPhone(AuthenticationFlowContext context) { return context.getAuthenticatorConfig() == null || - context.getAuthenticatorConfig().getConfig().getOrDefault(CONFIG_IS_LOGIN_WITH_PHONE_VERIFY, "true").equals("true"); + context.getAuthenticatorConfig().getConfig().getOrDefault(CONFIG_IS_LOGIN_WITH_PHONE_VERIFY, "true") + .equals("true"); } - private LoginFormsProvider assemblyForm(AuthenticationFlowContext context, LoginFormsProvider form){ + private LoginFormsProvider assemblyForm(AuthenticationFlowContext context, LoginFormsProvider form) { if (isSupportPhone(context)) form.setAttribute(ATTRIBUTE_SUPPORT_PHONE, true); - if (isLoginWithPhoneNumber(context)){ - form.setAttribute("loginWithPhoneNumber",true); + if (isLoginWithPhoneNumber(context)) { + form.setAttribute("loginWithPhoneNumber", true); } return form; } @@ -81,12 +85,13 @@ private LoginFormsProvider assemblyForm(AuthenticationFlowContext context, Login @Override protected Response challenge(AuthenticationFlowContext context, MultivaluedMap formData) { LoginFormsProvider forms = context.form(); - if (formData.size() > 0) forms.setFormData(formData); + if (formData.size() > 0) + forms.setFormData(formData); if (Utils.isDuplicatePhoneAllowed(context.getSession())) { forms.setError("duplicatePhoneAllowedCantLogin"); logger.warn("duplicate phone allowed! phone login is disabled!"); } else { - forms = assemblyForm(context,forms); + forms = assemblyForm(context, forms); } return forms.createLoginUsernamePassword(); } @@ -104,11 +109,10 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap } String phoneNumber = inputData.getFirst(FIELD_PHONE_NUMBER); - if (Validation.isBlank(phoneNumber)) { context.getEvent().error(Errors.USERNAME_MISSING); context.form().setAttribute(ATTEMPTED_PHONE_ACTIVATED, true); - assemblyForm(context,context.form()); + assemblyForm(context, context.form()); Response challengeResponse = challenge(context, SupportPhonePages.Errors.MISSING.message(), FIELD_PHONE_NUMBER); context.forceChallenge(challengeResponse); return false; @@ -120,7 +124,6 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap return false; } - return validatePhone(context, phoneNumber, code.trim()); } @@ -129,8 +132,9 @@ private void invalidVerificationCode(AuthenticationFlowContext context, String n context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); context.form().setAttribute(ATTEMPTED_PHONE_ACTIVATED, true) .setAttribute(ATTEMPTED_PHONE_NUMBER, number); - assemblyForm(context,context.form()); - Response challengeResponse = challenge(context, SupportPhonePages.Errors.NOT_MATCH.message(), FIELD_VERIFICATION_CODE); + assemblyForm(context, context.form()); + Response challengeResponse = challenge(context, SupportPhonePages.Errors.NOT_MATCH.message(), + FIELD_VERIFICATION_CODE); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); } @@ -139,16 +143,18 @@ private boolean validatePhone(AuthenticationFlowContext context, String phoneNum context.clearUser(); try { - var validPhoneNumber = Utils.canonicalizePhoneNumber(context.getSession(),phoneNumber); + var validPhoneNumber = Utils.canonicalizePhoneNumber(context.getSession(), phoneNumber); return Utils.findUserByPhone(context.getSession(), context.getRealm(), validPhoneNumber) - .map(user -> validateVerificationCode(context, user, validPhoneNumber, code) && validateUser(context, user, validPhoneNumber)) + .map(user -> validateVerificationCode(context, user, validPhoneNumber, code) + && validateUser(context, user, validPhoneNumber)) .orElseGet(() -> { context.getEvent().error(Errors.USER_NOT_FOUND); context.form().setAttribute(ATTEMPTED_PHONE_ACTIVATED, true) .setAttribute(ATTEMPTED_PHONE_NUMBER, phoneNumber); - assemblyForm(context,context.form()); - Response challengeResponse = challenge(context, SupportPhonePages.Errors.USER_NOT_FOUND.message(), FIELD_PHONE_NUMBER); + assemblyForm(context, context.form()); + Response challengeResponse = challenge(context, SupportPhonePages.Errors.USER_NOT_FOUND.message(), + FIELD_PHONE_NUMBER); context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse); return false; }); @@ -156,14 +162,15 @@ private boolean validatePhone(AuthenticationFlowContext context, String phoneNum context.getEvent().error(Errors.USERNAME_MISSING); context.form().setAttribute(ATTEMPTED_PHONE_ACTIVATED, true) .setAttribute(ATTEMPTED_PHONE_NUMBER, phoneNumber); - assemblyForm(context,context.form()); - Response challengeResponse = challenge(context,e.getErrorType().message(), FIELD_PHONE_NUMBER); + assemblyForm(context, context.form()); + Response challengeResponse = challenge(context, e.getErrorType().message(), FIELD_PHONE_NUMBER); context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse); return false; } } - private boolean validateVerificationCode(AuthenticationFlowContext context, UserModel user, String phoneNumber, String code) { + private boolean validateVerificationCode(AuthenticationFlowContext context, UserModel user, String phoneNumber, + String code) { try { context.getSession().getProvider(PhoneVerificationCodeProvider.class) .validateCode(user, phoneNumber, code, TokenCodeType.AUTH); @@ -184,7 +191,7 @@ private boolean isDisabledByBruteForce(AuthenticationFlowContext context, UserMo context.getEvent().error(bruteForceError); context.form().setAttribute(ATTEMPTED_PHONE_ACTIVATED, true) .setAttribute(ATTEMPTED_PHONE_NUMBER, phoneNumber); - assemblyForm(context,context.form()); + assemblyForm(context, context.form()); Response challengeResponse = challenge(context, disabledByBruteForceError(), disabledByBruteForceFieldError()); context.forceChallenge(challengeResponse); return true; @@ -193,13 +200,14 @@ private boolean isDisabledByBruteForce(AuthenticationFlowContext context, UserMo } private boolean enabledUser(AuthenticationFlowContext context, UserModel user, String phoneNumber) { - if (isDisabledByBruteForce(context, user, phoneNumber)) return false; + if (isDisabledByBruteForce(context, user, phoneNumber)) + return false; if (!user.isEnabled()) { context.getEvent().user(user); context.getEvent().error(Errors.USER_DISABLED); context.form().setAttribute(ATTEMPTED_PHONE_ACTIVATED, true) .setAttribute(ATTEMPTED_PHONE_NUMBER, phoneNumber); - assemblyForm(context,context.form()); + assemblyForm(context, context.form()); Response challengeResponse = challenge(context, Messages.ACCOUNT_DISABLED); context.forceChallenge(challengeResponse); return false; @@ -216,7 +224,8 @@ private boolean validateUser(AuthenticationFlowContext context, UserModel user, return true; } - private boolean validateUser(AuthenticationFlowContext context, UserModel user, MultivaluedMap inputData) { + private boolean validateUser(AuthenticationFlowContext context, UserModel user, + MultivaluedMap inputData) { if (!enabledUser(context, user)) { return false; } @@ -233,20 +242,23 @@ private boolean validateUser(AuthenticationFlowContext context, UserModel user, } @Override - public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap inputData) { + public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap inputData) { UserModel user = getUser(context, inputData); boolean shouldClearUserFromCtxAfterBadPassword = !isUserAlreadySetBeforeUsernamePasswordAuth(context); - return user != null && validatePassword(context, user, inputData, shouldClearUserFromCtxAfterBadPassword) && validateUser(context, user, inputData); + return user != null && validatePassword(context, user, inputData, shouldClearUserFromCtxAfterBadPassword) + && validateUser(context, user, inputData); } private UserModel getUser(AuthenticationFlowContext context, MultivaluedMap inputData) { if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) { - // Get user from the authentication context in case he was already set before this authenticator + // Get user from the authentication context in case he was already set before + // this authenticator UserModel user = context.getUser(); testInvalidUser(context, user); return user; } else { - // Normal login. In this case this authenticator is supposed to establish identity of the user from the provided username + // Normal login. In this case this authenticator is supposed to establish + // identity of the user from the provided username context.clearUser(); return getUserFromForm(context, inputData); } @@ -257,7 +269,7 @@ private UserModel getUserFromForm(AuthenticationFlowContext context, Multivalued if (username == null) { context.getEvent().error(Errors.USER_NOT_FOUND); Response challengeResponse = challenge(context, getDefaultChallengeMessage(context), FIELD_USERNAME); - assemblyForm(context,context.form()); + assemblyForm(context, context.form()); context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse); return null; } @@ -273,7 +285,7 @@ private UserModel getUserFromForm(AuthenticationFlowContext context, Multivalued user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username); if (user == null && isLoginWithPhoneNumber(context) && - !Utils.isDuplicatePhoneAllowed(context.getSession())){ + !Utils.isDuplicatePhoneAllowed(context.getSession())) { user = Utils.findUserByPhone(context.getSession(), context.getRealm(), username).orElse(null); } } catch (ModelDuplicateException mde) { @@ -281,9 +293,11 @@ private UserModel getUserFromForm(AuthenticationFlowContext context, Multivalued // Could happen during federation import if (mde.getDuplicateFieldName() != null && mde.getDuplicateFieldName().equals(UserModel.EMAIL)) { - setDuplicateUserChallenge(context, Errors.EMAIL_IN_USE, Messages.EMAIL_EXISTS, AuthenticationFlowError.INVALID_USER); + setDuplicateUserChallenge(context, Errors.EMAIL_IN_USE, Messages.EMAIL_EXISTS, + AuthenticationFlowError.INVALID_USER); } else { - setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS, AuthenticationFlowError.INVALID_USER); + setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS, + AuthenticationFlowError.INVALID_USER); } return null; } @@ -302,7 +316,6 @@ public String getReferenceCategory() { return PasswordCredentialModel.TYPE; } - public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { AuthenticationExecutionModel.Requirement.REQUIRED }; @@ -340,6 +353,7 @@ public String getHelpText() { .add() .build(); } + @Override public boolean isConfigurable() { return true; diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticator.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticator.java index 9aa5c7d9..8c8fd02b 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticator.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticator.java @@ -10,23 +10,19 @@ import cc.coopersoft.keycloak.phone.providers.constants.TokenCodeType; import cc.coopersoft.keycloak.phone.providers.spi.PhoneProvider; import org.jboss.logging.Logger; -import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.CredentialValidator; -import org.keycloak.common.util.ServerCookie; import org.keycloak.credential.CredentialProvider; +import org.keycloak.http.HttpResponse; import org.keycloak.models.*; -import org.keycloak.models.credential.dto.OTPSecretData; import org.keycloak.services.validation.Validation; -import org.keycloak.util.JsonSerialization; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.io.IOException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; import java.net.URI; import java.util.Optional; @@ -51,25 +47,21 @@ protected boolean validateCookie(AuthenticationFlowContext context) { return false; return Optional.of(context.getHttpRequest().getHttpHeaders().getCookies()) - .flatMap(cookies -> - Optional.ofNullable(cookies.get("SMS_OTP_ANSWERED")) - .flatMap(cookie -> OptionalUtils.ofBlank(cookie.getValue())) - .flatMap(credentialId -> - Optional.ofNullable(cookies.get(credentialId)) - .flatMap(cookie -> OptionalUtils.ofBlank(cookie.getValue())) - .map(secret -> context.getUser() - .credentialManager() - .isValid(new UserCredentialModel(credentialId, getType(context.getSession()), secret))) - ) - ).orElse(false); + .flatMap(cookies -> Optional.ofNullable(cookies.get("SMS_OTP_ANSWERED")) + .flatMap(cookie -> OptionalUtils.ofBlank(cookie.getValue())) + .flatMap(credentialId -> Optional.ofNullable(cookies.get(credentialId)) + .flatMap(cookie -> OptionalUtils.ofBlank(cookie.getValue())) + .map(secret -> context.getUser() + .credentialManager() + .isValid(new UserCredentialModel(credentialId, getType(context.getSession()), secret))))) + .orElse(false); } protected void setCookie(AuthenticationFlowContext context, String credentialId, String secret) { - int maxCookieAge = Utils.getOtpExpires(context.getSession()); - if (maxCookieAge <= 0 ){ + if (maxCookieAge <= 0) { return; } @@ -91,20 +83,28 @@ protected void setCookie(AuthenticationFlowContext context, String credentialId, false, true); } - public void addCookie(AuthenticationFlowContext context, String name, String value, String path, String domain, String comment, int maxAge, boolean secure, boolean httpOnly) { - HttpResponse response = context.getSession().getContext().getContextObject(HttpResponse.class); - StringBuilder cookieBuf = new StringBuilder(); - ServerCookie.appendCookieValue(cookieBuf, 1, name, value, path, domain, comment, maxAge, secure, httpOnly, null); - String cookie = cookieBuf.toString(); - response.getOutputHeaders().add(HttpHeaders.SET_COOKIE, cookie); + public void addCookie(AuthenticationFlowContext context, String name, String value, String path, String domain, + String comment, int maxAge, boolean secure, boolean httpOnly) { + HttpResponse response = context.getSession().getContext().getHttpResponse(); + NewCookie newCookie = new NewCookie.Builder(name) + .value(value) + .path(path) + .domain(domain) + .version(1) + .comment(comment) + .maxAge(maxAge) + .secure(secure) + .build(); + response.setCookieIfAbsent(newCookie); } @Override public PhoneOtpCredentialProvider getCredentialProvider(KeycloakSession session) { - return (PhoneOtpCredentialProvider) session.getProvider(CredentialProvider.class, PhoneOtpCredentialProviderFactory.PROVIDER_ID); + return (PhoneOtpCredentialProvider) session.getProvider(CredentialProvider.class, + PhoneOtpCredentialProviderFactory.PROVIDER_ID); } - private String getCredentialPhoneNumber(UserModel user){ + private String getCredentialPhoneNumber(UserModel user) { return PhoneOtpCredentialModel.getSmsOtpCredentialData(user) .map(PhoneOtpCredentialModel.SmsOtpCredentialData::getPhoneNumber) .orElseThrow(() -> new IllegalStateException("Not have OTP Credential")); @@ -130,12 +130,12 @@ public void authenticate(AuthenticationFlowContext context) { PhoneProvider phoneProvider = context.getSession().getProvider(PhoneProvider.class); try { - int expires = phoneProvider.sendTokenCode(phoneNumber,context.getConnection().getRemoteAddr(), + int expires = phoneProvider.sendTokenCode(phoneNumber, context.getConnection().getRemoteAddr(), TokenCodeType.OTP, null); context.form() .setInfo("codeSent", phoneNumber) .setAttribute("expires", expires) - .setAttribute("initSend",true); + .setAttribute("initSend", true); } catch (ForbiddenException e) { logger.warn("otp send code Forbidden Exception!", e); context.form().setError(SupportPhonePages.Errors.ABUSED.message()); @@ -144,10 +144,10 @@ public void authenticate(AuthenticationFlowContext context) { context.form().setError(SupportPhonePages.Errors.FAIL.message()); } - var credentialData = new PhoneOtpCredentialModel.SmsOtpCredentialData(phoneNumber,0); - PhoneOtpCredentialModel.updateOtpCredential(context.getUser(),credentialData,null); + var credentialData = new PhoneOtpCredentialModel.SmsOtpCredentialData(phoneNumber, 0); + PhoneOtpCredentialModel.updateOtpCredential(context.getUser(), credentialData, null); - Response challenge = challenge(context,phoneNumber); + Response challenge = challenge(context, phoneNumber); context.challenge(challenge); } @@ -162,35 +162,36 @@ public void action(AuthenticationFlowContext context) { if (credentialId == null || credentialId.isEmpty()) { var defaultOtpCredential = getCredentialProvider(context.getSession()) .getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()); - credentialId = defaultOtpCredential==null ? "" : defaultOtpCredential.getId(); + credentialId = defaultOtpCredential == null ? "" : defaultOtpCredential.getId(); } - if (Validation.isBlank(secret)){ + if (Validation.isBlank(secret)) { context.form() .setError(SupportPhonePages.Errors.NOT_MATCH.message()); - Response challenge = challenge(context,phoneNumber); + Response challenge = challenge(context, phoneNumber); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); } UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret); - boolean validated = getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input); + boolean validated = getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), + input); if (!validated) { context.form() .setError(SupportPhonePages.Errors.NOT_MATCH.message()); - Response challenge = challenge(context,phoneNumber); + Response challenge = challenge(context, phoneNumber); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); return; } - setCookie(context,credentialId,secret); + setCookie(context, credentialId, secret); context.success(); } - protected Response challenge(AuthenticationFlowContext context,String phoneNumber) { + protected Response challenge(AuthenticationFlowContext context, String phoneNumber) { return context.form() .setAttribute(ATTRIBUTE_SUPPORT_PHONE, true) - .setAttribute(SupportPhonePages.ATTEMPTED_PHONE_NUMBER,phoneNumber) + .setAttribute(SupportPhonePages.ATTEMPTED_PHONE_NUMBER, phoneNumber) .createForm(PAGE); } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticatorFactory.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticatorFactory.java index a95aa2e6..d939dcd0 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticatorFactory.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/browser/SmsOtpMfaAuthenticatorFactory.java @@ -1,25 +1,20 @@ package cc.coopersoft.keycloak.phone.authentication.authenticators.browser; -import cc.coopersoft.keycloak.phone.authentication.authenticators.resetcred.ResetCredentialWithPhone; import org.keycloak.Config.Scope; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -import java.util.Collections; import java.util.List; -import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE; - -public class SmsOtpMfaAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { +public class SmsOtpMfaAuthenticatorFactory implements AuthenticatorFactory { public final static String PROVIDER_ID = "sms-otp-authenticator"; -// public final static String COOKIE_MAX_AGE= "cookieMaxAge"; + // public final static String COOKIE_MAX_AGE= "cookieMaxAge"; private final static SmsOtpMfaAuthenticator instance = new SmsOtpMfaAuthenticator(); @Override @@ -54,11 +49,11 @@ public String getHelpText() { @Override public List getConfigProperties() { -// ProviderConfigProperty rep = -// new ProviderConfigProperty(COOKIE_MAX_AGE, -// "Cookie Max Age", -// "Max age in seconds of the SMS_OTP_COOKIE. Zero is Don't use Cookie", -// STRING_TYPE, "3600"); + // ProviderConfigProperty rep = + // new ProviderConfigProperty(COOKIE_MAX_AGE, + // "Cookie Max Age", + // "Max age in seconds of the SMS_OTP_COOKIE. Zero is Don't use Cookie", + // STRING_TYPE, "3600"); return null; } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/AuthenticationCodeAuthenticatorFactory.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/AuthenticationCodeAuthenticatorFactory.java index f0ad9bea..4669c6da 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/AuthenticationCodeAuthenticatorFactory.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/AuthenticationCodeAuthenticatorFactory.java @@ -3,16 +3,14 @@ import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -import java.util.ArrayList; import java.util.List; -public class AuthenticationCodeAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { +public class AuthenticationCodeAuthenticatorFactory implements AuthenticatorFactory { public static final String PROVIDER_ID = "verification-code-authenticator"; @@ -43,8 +41,9 @@ public Authenticator create(KeycloakSession session) { private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { AuthenticationExecutionModel.Requirement.REQUIRED, - //AuthenticationExecutionModel.Requirement.DISABLED + // AuthenticationExecutionModel.Requirement.DISABLED }; + @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; @@ -60,24 +59,26 @@ public List getConfigProperties() { return null; } -// private static final List configProperties = new ArrayList<>(); - -// static { -// ProviderConfigProperty maxAge; -// maxAge = new ProviderConfigProperty(); -// maxAge.setName(MAX_AGE); -// maxAge.setLabel("Verification Code Max Age"); -// maxAge.setType(ProviderConfigProperty.STRING_TYPE); -// maxAge.setHelpText("Max age in seconds of the verification codes."); -// configProperties.add(maxAge); -// ProviderConfigProperty kind = new ProviderConfigProperty(); -// kind.setName(KIND); -// kind.setLabel("Verification Code Kind"); -// kind.setType(ProviderConfigProperty.STRING_TYPE); -// kind.setHelpText("a string that identifies what the verification code is used for, if this is set, " + -// "a parameter of 'kind' is required to be equal with set value"); -// configProperties.add(kind); -// } + // private static final List configProperties = new + // ArrayList<>(); + + // static { + // ProviderConfigProperty maxAge; + // maxAge = new ProviderConfigProperty(); + // maxAge.setName(MAX_AGE); + // maxAge.setLabel("Verification Code Max Age"); + // maxAge.setType(ProviderConfigProperty.STRING_TYPE); + // maxAge.setHelpText("Max age in seconds of the verification codes."); + // configProperties.add(maxAge); + // ProviderConfigProperty kind = new ProviderConfigProperty(); + // kind.setName(KIND); + // kind.setLabel("Verification Code Kind"); + // kind.setType(ProviderConfigProperty.STRING_TYPE); + // kind.setHelpText("a string that identifies what the verification code is used + // for, if this is set, " + + // "a parameter of 'kind' is required to be equal with set value"); + // configProperties.add(kind); + // } @Override public String getDisplayType() { @@ -99,4 +100,3 @@ public boolean isConfigurable() { return false; } } - diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/BaseDirectGrantAuthenticator.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/BaseDirectGrantAuthenticator.java index a2886235..01ff0cf7 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/BaseDirectGrantAuthenticator.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/BaseDirectGrantAuthenticator.java @@ -10,8 +10,8 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.Optional; public abstract class BaseDirectGrantAuthenticator implements Authenticator { @@ -21,33 +21,35 @@ public Response errorResponse(int status, String error, String errorDescription) return Response.status(status).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build(); } - protected Optional getPhoneNumber(AuthenticationFlowContext context){ + protected Optional getPhoneNumber(AuthenticationFlowContext context) { return OptionalUtils.ofBlank(OptionalUtils.ofBlank( - context.getHttpRequest().getDecodedFormParameters().getFirst("phone_number")) - .orElse(context.getHttpRequest().getDecodedFormParameters().getFirst("phoneNumber"))); + context.getHttpRequest().getDecodedFormParameters().getFirst("phone_number")) + .orElse(context.getHttpRequest().getDecodedFormParameters().getFirst("phoneNumber"))); } - protected Optional getAuthenticationCode(AuthenticationFlowContext context){ + protected Optional getAuthenticationCode(AuthenticationFlowContext context) { return OptionalUtils.ofBlank(context.getHttpRequest().getDecodedFormParameters().getFirst("code")); } - protected void invalidCredentials(AuthenticationFlowContext context,AuthenticationFlowError error){ + protected void invalidCredentials(AuthenticationFlowContext context, AuthenticationFlowError error) { context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); - Response challenge = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials"); + Response challenge = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", + "Invalid user credentials"); context.failure(error, challenge); } - protected void invalidCredentials(AuthenticationFlowContext context, UserModel user){ + protected void invalidCredentials(AuthenticationFlowContext context, UserModel user) { context.getEvent().user(user); - invalidCredentials(context,AuthenticationFlowError.INVALID_CREDENTIALS); + invalidCredentials(context, AuthenticationFlowError.INVALID_CREDENTIALS); } - protected void invalidCredentials(AuthenticationFlowContext context){ - invalidCredentials(context,AuthenticationFlowError.INVALID_USER); + protected void invalidCredentials(AuthenticationFlowContext context) { + invalidCredentials(context, AuthenticationFlowError.INVALID_USER); } @Override - public void close() {} + public void close() { + } @Override public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticator.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticator.java index 29a0c6f0..f6b2eb56 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticator.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticator.java @@ -4,7 +4,6 @@ import cc.coopersoft.keycloak.phone.providers.representations.TokenCodeRepresentation; import cc.coopersoft.keycloak.phone.providers.spi.PhoneVerificationCodeProvider; import cc.coopersoft.keycloak.phone.Utils; -import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.models.KeycloakSession; @@ -12,11 +11,8 @@ import org.keycloak.models.UserModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; - public class EverybodyPhoneAuthenticator extends BaseDirectGrantAuthenticator { - private static final Logger logger = Logger.getLogger(EverybodyPhoneAuthenticator.class); - public EverybodyPhoneAuthenticator(KeycloakSession session) { if (session.getContext().getRealm() == null) { throw new IllegalStateException("The service cannot accept a session without a realm in its context."); @@ -37,13 +33,14 @@ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserMo public void authenticate(AuthenticationFlowContext context) { getPhoneNumber(context) .ifPresentOrElse(phoneNumber -> getAuthenticationCode(context) - .ifPresentOrElse(code -> authToUser(context, phoneNumber, code), - ()-> invalidCredentials(context)), + .ifPresentOrElse(code -> authToUser(context, phoneNumber, code), + () -> invalidCredentials(context)), () -> invalidCredentials(context)); } private void authToUser(AuthenticationFlowContext context, String phoneNumber, String code) { - PhoneVerificationCodeProvider phoneVerificationCodeProvider = context.getSession().getProvider(PhoneVerificationCodeProvider.class); + PhoneVerificationCodeProvider phoneVerificationCodeProvider = context.getSession() + .getProvider(PhoneVerificationCodeProvider.class); TokenCodeRepresentation tokenCode = phoneVerificationCodeProvider.ongoingProcess(phoneNumber, TokenCodeType.AUTH); if (tokenCode == null || !tokenCode.getCode().equals(code)) { @@ -53,7 +50,7 @@ private void authToUser(AuthenticationFlowContext context, String phoneNumber, S UserModel user = Utils.findUserByPhone(context.getSession(), context.getRealm(), phoneNumber) .orElseGet(() -> { - if (context.getSession().users().getUserByUsername(context.getRealm(),phoneNumber) != null) { + if (context.getSession().users().getUserByUsername(context.getRealm(), phoneNumber) != null) { invalidCredentials(context, AuthenticationFlowError.USER_CONFLICT); return null; } @@ -65,7 +62,7 @@ private void authToUser(AuthenticationFlowContext context, String phoneNumber, S }); if (user != null) { context.setUser(user); - phoneVerificationCodeProvider.tokenValidated(user, phoneNumber, tokenCode.getId(),false); + phoneVerificationCodeProvider.tokenValidated(user, phoneNumber, tokenCode.getId(), false); context.success(); } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticatorFactory.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticatorFactory.java index 953142bf..369357c7 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticatorFactory.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/EverybodyPhoneAuthenticatorFactory.java @@ -3,7 +3,6 @@ import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -11,13 +10,12 @@ import java.util.List; -public class EverybodyPhoneAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { - +public class EverybodyPhoneAuthenticatorFactory implements AuthenticatorFactory { public static final String PROVIDER_ID = "everybody-phone-authenticator"; @Override - public Authenticator create(KeycloakSession session){ + public Authenticator create(KeycloakSession session) { return new EverybodyPhoneAuthenticator(session); } @@ -36,7 +34,6 @@ public void close() { } - @Override public String getDisplayType() { return "Authentication everybody by phone"; diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticator.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticator.java index 8b103453..244cf9a7 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticator.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticator.java @@ -1,7 +1,6 @@ package cc.coopersoft.keycloak.phone.authentication.authenticators.directgrant; import cc.coopersoft.keycloak.phone.Utils; -import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -9,8 +8,6 @@ public class PhoneNumberAuthenticator extends BaseDirectGrantAuthenticator { - private static final Logger logger = Logger.getLogger(PhoneNumberAuthenticator.class); - @Override public boolean requiresUser() { return false; @@ -23,11 +20,12 @@ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserMo @Override public void authenticate(AuthenticationFlowContext context) { context.clearUser(); - getPhoneNumber(context).ifPresentOrElse(phoneNumber -> - Utils.findUserByPhone(context.getSession(),context.getRealm(),phoneNumber) - .ifPresentOrElse(user -> { - context.setUser(user); - context.success(); - },()->invalidCredentials(context)),() -> invalidCredentials(context)); + getPhoneNumber(context).ifPresentOrElse( + phoneNumber -> Utils.findUserByPhone(context.getSession(), context.getRealm(), phoneNumber) + .ifPresentOrElse(user -> { + context.setUser(user); + context.success(); + }, () -> invalidCredentials(context)), + () -> invalidCredentials(context)); } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticatorFactory.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticatorFactory.java index 3e28517d..85303967 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticatorFactory.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/directgrant/PhoneNumberAuthenticatorFactory.java @@ -3,16 +3,14 @@ import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.authentication.ConfigurableAuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -import java.util.ArrayList; import java.util.List; -public class PhoneNumberAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { +public class PhoneNumberAuthenticatorFactory implements AuthenticatorFactory { public static final String PROVIDER_ID = "phone-number-authenticator"; private static final PhoneNumberAuthenticator SINGLETON = new PhoneNumberAuthenticator(); @@ -81,4 +79,3 @@ public boolean isConfigurable() { return false; } } - diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/resetcred/ResetCredentialWithPhone.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/resetcred/ResetCredentialWithPhone.java index a2b5cbb5..8cd2f691 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/resetcred/ResetCredentialWithPhone.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/authenticators/resetcred/ResetCredentialWithPhone.java @@ -23,8 +23,8 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; import java.util.List; @@ -44,22 +44,29 @@ public void authenticate(AuthenticationFlowContext context) { String existingUserId = context.getAuthenticationSession().getAuthNote(AbstractIdpAuthenticator.EXISTING_USER_INFO); if (existingUserId != null) { - UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), context.getAuthenticationSession()); + UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(), context.getRealm(), + context.getAuthenticationSession()); - logger.debugf("Forget-password triggered when reauthenticating user after first broker login. Prefilling reset-credential-choose-user screen with user '%s' ", existingUser.getUsername()); + logger.debugf( + "Forget-password triggered when reauthenticating user after first broker login. Prefilling reset-credential-choose-user screen with user '%s' ", + existingUser.getUsername()); context.setUser(existingUser); Response challenge = context.form().createPasswordReset(); context.challenge(challenge); return; } - String actionTokenUserId = context.getAuthenticationSession().getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID); + String actionTokenUserId = context.getAuthenticationSession() + .getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID); if (actionTokenUserId != null) { UserModel existingUser = context.getSession().users().getUserById(context.getRealm(), actionTokenUserId); - // Action token logics handles checks for user ID validity and user being enabled + // Action token logics handles checks for user ID validity and user being + // enabled - logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping reset-credential-choose-user screen and using user '%s' ", existingUser.getUsername()); + logger.debugf( + "Forget-password triggered when reauthenticating user after authentication via action token. Skipping reset-credential-choose-user screen and using user '%s' ", + existingUser.getUsername()); context.setUser(existingUser); context.success(); return; @@ -83,8 +90,6 @@ public void action(AuthenticationFlowContext context) { context.success(); } - - protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap inputData) { boolean byPhone = OptionalUtils .ofBlank(inputData.getFirst(FIELD_PATH_PHONE_ACTIVATED)) @@ -97,7 +102,7 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME); UserModel user; - if (!byPhone){ + if (!byPhone) { if (Validation.isBlank(username)) { context.getEvent().error(Errors.USERNAME_MISSING); Response challenge = challenge(context, Validation.FIELD_USERNAME, Messages.MISSING_USERNAME); @@ -105,17 +110,18 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap return false; } user = getUserByUsername(context, username.trim()); - }else{ + } else { if (Validation.isBlank(phoneNumber)) { context.getEvent().error(Errors.USERNAME_MISSING); - Response challenge = challenge(context, FIELD_PHONE_NUMBER, SupportPhonePages.Errors.MISSING.message(), phoneNumber); + Response challenge = challenge(context, FIELD_PHONE_NUMBER, SupportPhonePages.Errors.MISSING.message(), + phoneNumber); context.forceChallenge(challenge); return false; } try { - phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(),phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(), phoneNumber); } catch (PhoneNumberInvalidException e) { context.getEvent().error(Errors.USERNAME_MISSING); Response challenge = challenge(context, FIELD_PHONE_NUMBER, @@ -124,7 +130,6 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap return false; } - String verificationCode = inputData.getFirst(FIELD_VERIFICATION_CODE); if (Validation.isBlank(verificationCode)) { invalidVerificationCode(context, phoneNumber); @@ -133,12 +138,12 @@ protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap user = Utils.findUserByPhone(context.getSession(), context.getRealm(), phoneNumber) .orElse(null); - if (user != null && !validateVerificationCode(context, user, phoneNumber, verificationCode.trim())){ + if (user != null && !validateVerificationCode(context, user, phoneNumber, verificationCode.trim())) { return false; } } - return validateUser(context,user,byPhone,byPhone ? phoneNumber : username); + return validateUser(context, user, byPhone, byPhone ? phoneNumber : username); } protected UserModel getUserByUsername(AuthenticationFlowContext context, String username) { @@ -176,31 +181,35 @@ protected boolean isDisabledByBruteForce(AuthenticationFlowContext context, User } protected boolean validateUser(AuthenticationFlowContext context, - UserModel user, boolean byPhone, String attempted) { + UserModel user, boolean byPhone, String attempted) { - if (user == null){ + if (user == null) { context.getEvent().error(Errors.USER_NOT_FOUND); - if(!byPhone){ + if (!byPhone) { context.getEvent().detail(Details.USERNAME, attempted); } - Response challenge = byPhone ? challenge(context, FIELD_PHONE_NUMBER, SupportPhonePages.Errors.USER_NOT_FOUND.message(), attempted) : challenge(context, Validation.FIELD_USERNAME, Messages.INVALID_USERNAME_OR_EMAIL); + Response challenge = byPhone + ? challenge(context, FIELD_PHONE_NUMBER, SupportPhonePages.Errors.USER_NOT_FOUND.message(), attempted) + : challenge(context, Validation.FIELD_USERNAME, Messages.INVALID_USERNAME_OR_EMAIL); context.forceChallenge(challenge); return false; } - if (byPhone ? isDisabledByBruteForce(context, user, attempted) : isDisabledByBruteForce(context, user)) return false; + if (byPhone ? isDisabledByBruteForce(context, user, attempted) : isDisabledByBruteForce(context, user)) + return false; if (!user.isEnabled()) { - if(!byPhone){ + if (!byPhone) { context.getEvent().detail(Details.USERNAME, attempted); } context.getEvent().user(user); context.getEvent().error(Errors.USER_DISABLED); - Response challenge = byPhone ? challenge(context,FIELD_PHONE_NUMBER, Errors.USER_DISABLED, attempted) : challenge(context,Validation.FIELD_USERNAME,Errors.USER_DISABLED); + Response challenge = byPhone ? challenge(context, FIELD_PHONE_NUMBER, Errors.USER_DISABLED, attempted) + : challenge(context, Validation.FIELD_USERNAME, Errors.USER_DISABLED); context.forceChallenge(challenge); return false; } - if (byPhone){ + if (byPhone) { context.getAuthenticationSession().setAuthNote(SHOULD_SEND_EMAIL, "false"); } context.setUser(user); @@ -209,13 +218,14 @@ protected boolean validateUser(AuthenticationFlowContext context, protected void invalidVerificationCode(AuthenticationFlowContext context, String phoneNumber) { context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); - Response challenge = challenge(context, FIELD_VERIFICATION_CODE, SupportPhonePages.Errors.NOT_MATCH.message(), phoneNumber); + Response challenge = challenge(context, FIELD_VERIFICATION_CODE, SupportPhonePages.Errors.NOT_MATCH.message(), + phoneNumber); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); } protected Response challenge(AuthenticationFlowContext context, - String field, String message, - String phoneNumber) { + String field, String message, + String phoneNumber) { return context.form() .addError(new FormMessage(field, message)) .setAttribute(ATTRIBUTE_SUPPORT_PHONE, true) @@ -225,16 +235,18 @@ protected Response challenge(AuthenticationFlowContext context, } protected Response challenge(AuthenticationFlowContext context, - String field, String message) { + String field, String message) { return context.form() .addError(new FormMessage(field, message)) .setAttribute(ATTRIBUTE_SUPPORT_PHONE, true) .createPasswordReset(); } - private boolean validateVerificationCode(AuthenticationFlowContext context, UserModel user, String phoneNumber, String code) { + private boolean validateVerificationCode(AuthenticationFlowContext context, UserModel user, String phoneNumber, + String code) { try { - context.getSession().getProvider(PhoneVerificationCodeProvider.class).validateCode(user, phoneNumber, code, TokenCodeType.RESET); + context.getSession().getProvider(PhoneVerificationCodeProvider.class).validateCode(user, phoneNumber, code, + TokenCodeType.RESET); logger.debug("verification code success!"); return true; } catch (Exception e) { @@ -245,7 +257,6 @@ private boolean validateVerificationCode(AuthenticationFlowContext context, User } } - @Override public boolean requiresUser() { return false; @@ -261,7 +272,6 @@ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserMo } - @Override public String getDisplayType() { return "Reset Credential Choose User with Phone"; diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneUserCreation.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneUserCreation.java index add35422..6fa42539 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneUserCreation.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneUserCreation.java @@ -24,7 +24,7 @@ import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.ValidationException; -import javax.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.MultivaluedMap; import java.util.ArrayList; import java.util.List; @@ -32,7 +32,7 @@ import static org.keycloak.provider.ProviderConfigProperty.BOOLEAN_TYPE; /** - * replace org.keycloak.authentication.forms.RegistrationUserCreation.java + * replace org.keycloak.authentication.forms.RegistrationUserCreation.java */ public class RegistrationPhoneUserCreation implements FormActionFactory, FormAction { @@ -42,12 +42,11 @@ public class RegistrationPhoneUserCreation implements FormActionFactory, FormAct public static final String CONFIG_PHONE_NUMBER_AS_USERNAME = "phoneNumberAsUsername"; - public static final String CONFIG_INPUT_NAME="isInputName"; + public static final String CONFIG_INPUT_NAME = "isInputName"; - public static final String CONFIG_INPUT_EMAIL="isInputEmail"; + public static final String CONFIG_INPUT_EMAIL = "isInputEmail"; private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { - AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED}; - + AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED }; @Override public String getDisplayType() { @@ -76,7 +75,8 @@ public boolean isConfigurable() { .property().name(CONFIG_PHONE_NUMBER_AS_USERNAME) .type(BOOLEAN_TYPE) .label("Phone number as username") - .helpText("Allow users to set phone number as username. If Realm has `email as username` set to true, this is invalid!") + .helpText( + "Allow users to set phone number as username. If Realm has `email as username` set to true, this is invalid!") .defaultValue(true) .add() .property().name(CONFIG_INPUT_NAME) @@ -104,7 +104,6 @@ public boolean isUserSetupAllowed() { return false; } - @Override public List getConfigProperties() { return CONFIG_PROPERTIES; @@ -136,15 +135,15 @@ public String getId() { // FormAction - private boolean isPhoneNumberAsUsername(FormContext context){ + private boolean isPhoneNumberAsUsername(FormContext context) { if (context.getAuthenticatorConfig() == null || "true".equals(context.getAuthenticatorConfig().getConfig() - .getOrDefault(CONFIG_PHONE_NUMBER_AS_USERNAME,"true"))){ + .getOrDefault(CONFIG_PHONE_NUMBER_AS_USERNAME, "true"))) { - if (context.getRealm().isRegistrationEmailAsUsername()){ + if (context.getRealm().isRegistrationEmailAsUsername()) { logger.warn("Realm set email as username, can't use phone number."); return false; } - if (Utils.isDuplicatePhoneAllowed(context.getSession())){ + if (Utils.isDuplicatePhoneAllowed(context.getSession())) { logger.warn("Duplicate phone allowed! phone number can't be used as username."); return false; } @@ -153,19 +152,19 @@ private boolean isPhoneNumberAsUsername(FormContext context){ return false; } - private boolean isHideName(FormContext context){ + private boolean isHideName(FormContext context) { return context.getAuthenticatorConfig() == null || !"true".equalsIgnoreCase(context.getAuthenticatorConfig().getConfig() - .getOrDefault(CONFIG_INPUT_NAME,"true")); + .getOrDefault(CONFIG_INPUT_NAME, "true")); } - private boolean isHideEmail(FormContext context){ + private boolean isHideEmail(FormContext context) { if (context.getAuthenticatorConfig() == null || "true".equalsIgnoreCase(context.getAuthenticatorConfig().getConfig() - .getOrDefault(CONFIG_INPUT_EMAIL,"true"))){ + .getOrDefault(CONFIG_INPUT_EMAIL, "true"))) { return false; } - if (context.getRealm().isRegistrationEmailAsUsername()){ + if (context.getRealm().isRegistrationEmailAsUsername()) { logger.warn("`email as username` is set, so can't hide email input."); return false; } @@ -178,15 +177,15 @@ public void buildPage(FormContext context, LoginFormsProvider form) { form.setAttribute("phoneNumberRequired", true); - if (isPhoneNumberAsUsername(context)){ + if (isPhoneNumberAsUsername(context)) { form.setAttribute("registrationPhoneNumberAsUsername", true); } - if (isHideName(context)){ + if (isHideName(context)) { form.setAttribute("hideName", true); } - if (isHideEmail(context)){ + if (isHideEmail(context)) { form.setAttribute("hideEmail", true); } } @@ -196,7 +195,6 @@ public void validate(ValidationContext context) { KeycloakSession session = context.getSession(); - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); context.getEvent().detail(Details.REGISTER_METHOD, "form"); @@ -209,9 +207,9 @@ public void validate(ValidationContext context) { context.error(Errors.INVALID_REGISTRATION); context.validationError(formData, errors); success = false; - }else { + } else { try { - phoneNumber = Utils.canonicalizePhoneNumber(session,phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(session, phoneNumber); if (!Utils.isDuplicatePhoneAllowed(session) && Utils.findUserByPhone(session, context.getRealm(), phoneNumber).isPresent()) { context.error(Errors.INVALID_REGISTRATION); @@ -229,31 +227,31 @@ public void validate(ValidationContext context) { } context.getEvent().detail(FIELD_PHONE_NUMBER, phoneNumber); - if (isPhoneNumberAsUsername(context)){ + if (isPhoneNumberAsUsername(context)) { context.getEvent().detail(Details.USERNAME, phoneNumber); - formData.putSingle(UserModel.USERNAME,phoneNumber); + formData.putSingle(UserModel.USERNAME, phoneNumber); } UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); - UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION_USER_CREATION, formData); + UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION, formData); - String username = profile.getAttributes().getFirstValue(UserModel.USERNAME); + String username = profile.getAttributes().getFirst(UserModel.USERNAME); context.getEvent().detail(Details.USERNAME, username); boolean hideName = isHideName(context); boolean hideEmail = isHideEmail(context); - if (!hideName){ - String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME); - String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME); + if (!hideName) { + String firstName = profile.getAttributes().getFirst(UserModel.FIRST_NAME); + String lastName = profile.getAttributes().getFirst(UserModel.LAST_NAME); context.getEvent().detail(Details.FIRST_NAME, firstName); context.getEvent().detail(Details.LAST_NAME, lastName); } - if (!hideEmail){ - String email = profile.getAttributes().getFirstValue(UserModel.EMAIL); + if (!hideEmail) { + String email = profile.getAttributes().getFirst(UserModel.EMAIL); context.getEvent().detail(Details.EMAIL, email); - if (context.getRealm().isRegistrationEmailAsUsername()){ + if (context.getRealm().isRegistrationEmailAsUsername()) { context.getEvent().detail(Details.USERNAME, email); } } @@ -275,7 +273,7 @@ public void validate(ValidationContext context) { if (success) { context.success(); } - context.validationError(formData,errors); + context.validationError(formData, errors); } @Override @@ -289,37 +287,38 @@ public void success(FormContext context) { var session = context.getSession(); try { - phoneNumber = Utils.canonicalizePhoneNumber(session,phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(session, phoneNumber); } catch (PhoneNumberInvalidException e) { // verified in validate process throw new IllegalStateException(); } - if (context.getRealm().isRegistrationEmailAsUsername()){ + if (context.getRealm().isRegistrationEmailAsUsername()) { username = email; - } else if (isPhoneNumberAsUsername(context)){ + } else if (isPhoneNumberAsUsername(context)) { username = phoneNumber; - formData.add(UserModel.USERNAME,phoneNumber); + formData.add(UserModel.USERNAME, phoneNumber); } context.getEvent().detail(Details.USERNAME, username) .detail(Details.REGISTER_METHOD, "form") - .detail(FIELD_PHONE_NUMBER,phoneNumber); + .detail(FIELD_PHONE_NUMBER, phoneNumber); - if (!isHideEmail(context)){ - context.getEvent().detail(Details.EMAIL,email); + if (!isHideEmail(context)) { + context.getEvent().detail(Details.EMAIL, email); } UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); - UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION_USER_CREATION, formData); + UserProfile profile = profileProvider.create(UserProfileContext.REGISTRATION, formData); UserModel user = profile.create(); -// UserModel user = context.getSession().users().addUser(context.getRealm(), username); + // UserModel user = context.getSession().users().addUser(context.getRealm(), + // username); user.setEnabled(true); context.setUser(user); context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); - //AttributeFormDataProcessor.process(formData); + // AttributeFormDataProcessor.process(formData); context.getEvent().user(user); context.getEvent().success(); @@ -342,7 +341,7 @@ public boolean requiresUser() { @Override public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { - return true;//!realmModel.isRegistrationEmailAsUsername(); + return true;// !realmModel.isRegistrationEmailAsUsername(); } @Override diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneVerificationCode.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneVerificationCode.java index 22c9c6d3..b818f88f 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneVerificationCode.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationPhoneVerificationCode.java @@ -8,8 +8,6 @@ import cc.coopersoft.keycloak.phone.providers.exception.PhoneNumberInvalidException; import cc.coopersoft.keycloak.phone.providers.representations.TokenCodeRepresentation; import cc.coopersoft.keycloak.phone.providers.spi.PhoneVerificationCodeProvider; -import cc.coopersoft.keycloak.phone.providers.spi.PhoneProvider; -import com.google.i18n.phonenumbers.NumberParseException; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.authentication.FormAction; @@ -27,7 +25,7 @@ import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.services.validation.Validation; -import javax.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.MultivaluedMap; import java.util.ArrayList; import java.util.List; @@ -40,7 +38,7 @@ public class RegistrationPhoneVerificationCode implements FormAction, FormAction public static final String PROVIDER_ID = "registration-phone"; - public static final String CONFIG_OPT_CREDENTIAL="createOPTCredential"; + public static final String CONFIG_OPT_CREDENTIAL = "createOPTCredential"; @Override public String getHelpText() { @@ -91,7 +89,7 @@ public List getConfigProperties() { } private final static Requirement[] REQUIREMENT_CHOICES = { - Requirement.REQUIRED, Requirement.DISABLED}; + Requirement.REQUIRED, Requirement.DISABLED }; @Override public Requirement[] getRequirementChoices() { @@ -118,7 +116,6 @@ public String getId() { return PROVIDER_ID; } - // FormAction private PhoneVerificationCodeProvider getTokenCodeService(KeycloakSession session) { @@ -128,7 +125,6 @@ private PhoneVerificationCodeProvider getTokenCodeService(KeycloakSession sessio @Override public void validate(ValidationContext context) { - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); List errors = new ArrayList<>(); context.getEvent().detail(Details.REGISTER_METHOD, "form"); @@ -137,7 +133,7 @@ public void validate(ValidationContext context) { String phoneNumber = formData.getFirst(FIELD_PHONE_NUMBER); - if (Validation.isBlank(phoneNumber)){ + if (Validation.isBlank(phoneNumber)) { context.error(Errors.INVALID_REGISTRATION); errors.add(new FormMessage(FIELD_PHONE_NUMBER, SupportPhonePages.Errors.MISSING)); context.validationError(formData, errors); @@ -145,7 +141,7 @@ public void validate(ValidationContext context) { } try { - phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(),phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(), phoneNumber); } catch (PhoneNumberInvalidException e) { context.error(Errors.INVALID_REGISTRATION); errors.add(new FormMessage(FIELD_PHONE_NUMBER, e.getErrorType().message())); @@ -156,7 +152,8 @@ public void validate(ValidationContext context) { context.getEvent().detail(FIELD_PHONE_NUMBER, phoneNumber); String verificationCode = formData.getFirst(FIELD_VERIFICATION_CODE); - TokenCodeRepresentation tokenCode = getTokenCodeService(session).ongoingProcess(phoneNumber, TokenCodeType.REGISTRATION); + TokenCodeRepresentation tokenCode = getTokenCodeService(session).ongoingProcess(phoneNumber, + TokenCodeType.REGISTRATION); if (Validation.isBlank(verificationCode) || tokenCode == null || !tokenCode.getCode().equals(verificationCode)) { context.error(Errors.INVALID_REGISTRATION); formData.remove(FIELD_VERIFICATION_CODE); @@ -180,23 +177,24 @@ public void success(FormContext context) { String phoneNumber = formData.getFirst(FIELD_PHONE_NUMBER); try { - phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(),phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(), phoneNumber); } catch (PhoneNumberInvalidException e) { - //verified in validate process + // verified in validate process throw new IllegalStateException(); } String tokenId = session.getAttribute("tokenId", String.class); logger.info(String.format("registration user %s phone success, tokenId is: %s", user.getId(), tokenId)); - getTokenCodeService(context.getSession()).tokenValidated(user, phoneNumber, tokenId,false); + getTokenCodeService(context.getSession()).tokenValidated(user, phoneNumber, tokenId, false); AuthenticatorConfigModel config = context.getAuthenticatorConfig(); if (config != null && - "true".equalsIgnoreCase(config.getConfig().getOrDefault(CONFIG_OPT_CREDENTIAL,"false"))){ + "true".equalsIgnoreCase(config.getConfig().getOrDefault(CONFIG_OPT_CREDENTIAL, "false"))) { PhoneOtpCredentialProvider ocp = (PhoneOtpCredentialProvider) context.getSession() .getProvider(CredentialProvider.class, PhoneOtpCredentialProviderFactory.PROVIDER_ID); - ocp.createCredential(context.getRealm(), context.getUser(), PhoneOtpCredentialModel.create(phoneNumber,tokenId,0)); + ocp.createCredential(context.getRealm(), context.getUser(), + PhoneOtpCredentialModel.create(phoneNumber, tokenId, 0)); } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationRedirectParametersReader.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationRedirectParametersReader.java index 2e534a55..9de2856a 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationRedirectParametersReader.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/forms/RegistrationRedirectParametersReader.java @@ -17,7 +17,6 @@ import java.util.regex.Pattern; import static org.keycloak.provider.ProviderConfigProperty.MULTIVALUED_STRING_TYPE; -import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE; public class RegistrationRedirectParametersReader implements FormActionFactory, FormAction { @@ -26,10 +25,8 @@ public class RegistrationRedirectParametersReader implements FormActionFactory, public static final String PROVIDER_ID = "registration-redirect-parameter"; public static final String PARAM_NAMES = "acceptParameter"; - private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { - AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED}; - + AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED }; @Override public String getDisplayType() { @@ -48,11 +45,10 @@ public boolean isConfigurable() { @Override public List getConfigProperties() { - ProviderConfigProperty rep = - new ProviderConfigProperty(PARAM_NAMES, - "Accept query param", - "Registration query param accept names.", - MULTIVALUED_STRING_TYPE, null); + ProviderConfigProperty rep = new ProviderConfigProperty(PARAM_NAMES, + "Accept query param", + "Registration query param accept names.", + MULTIVALUED_STRING_TYPE, null); return Collections.singletonList(rep); } @@ -110,7 +106,6 @@ public void validate(ValidationContext validationContext) { @Override public void success(FormContext context) { - String redirectUri = context.getAuthenticationSession().getRedirectUri(); logger.info("add user attribute form redirectUri:" + redirectUri); if (Validation.isBlank(redirectUri)) { @@ -123,7 +118,8 @@ public void success(FormContext context) { logger.error("redirectUri is null"); return; } - //url.queryParameterNames().forEach(s -> logger.info("redirect param name ->" + s)); + // url.queryParameterNames().forEach(s -> logger.info("redirect param name ->" + + // s)); UserModel user = context.getUser(); AuthenticatorConfigModel authenticatorConfig = context.getAuthenticatorConfig(); diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/ConfigSmsOtpRequiredAction.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/ConfigSmsOtpRequiredAction.java index c5b5bbca..94200d7f 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/ConfigSmsOtpRequiredAction.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/ConfigSmsOtpRequiredAction.java @@ -8,89 +8,95 @@ import cc.coopersoft.keycloak.phone.providers.constants.TokenCodeType; import cc.coopersoft.keycloak.phone.providers.exception.PhoneNumberInvalidException; import cc.coopersoft.keycloak.phone.providers.spi.PhoneVerificationCodeProvider; -import cc.coopersoft.keycloak.phone.providers.spi.PhoneProvider; -import com.google.i18n.phonenumbers.NumberParseException; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionProvider; -import org.keycloak.credential.CredentialModel; import org.keycloak.credential.CredentialProvider; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.core.Response; import java.util.Optional; import static cc.coopersoft.keycloak.phone.authentication.authenticators.browser.PhoneUsernamePasswordForm.VERIFIED_PHONE_NUMBER; public class ConfigSmsOtpRequiredAction implements RequiredActionProvider { - public static final String PROVIDER_ID = "CONFIGURE_SMS_OTP"; - - @Override - public void evaluateTriggers(RequiredActionContext context) { - } - -// private Optional getOTPCredential(RequiredActionContext context){ -// return Optional.ofNullable(context.getUser()) -// .flatMap(user -> user.credentialManager().getStoredCredentialsByTypeStream(PhoneOtpCredentialModel.TYPE).findFirst()) -// .map(credentialModel -> (PhoneOtpCredentialModel) credentialModel); -// } - - @Override - public void requiredActionChallenge(RequiredActionContext context) { - - var userPhoneNumber = PhoneOtpCredentialModel.getSmsOtpCredentialData(context.getUser()) - .map(PhoneOtpCredentialModel.SmsOtpCredentialData::getPhoneNumber) - .orElseGet(() -> Optional.ofNullable(context.getUser()) - .flatMap(user -> Optional.ofNullable(user.getFirstAttribute(SupportPhonePages.FIELD_PHONE_NUMBER))) - .orElse(null) - ); - - Response challenge = context.form() - .setAttribute(SupportPhonePages.FIELD_PHONE_NUMBER, userPhoneNumber) - .createForm("login-sms-otp-config.ftl"); - context.challenge(challenge); - } - - @Override - public void processAction(RequiredActionContext context) { - var session = context.getSession(); - PhoneVerificationCodeProvider phoneVerificationCodeProvider = session.getProvider(PhoneVerificationCodeProvider.class); - String phoneNumber = context.getHttpRequest().getDecodedFormParameters().getFirst(SupportPhonePages.FIELD_PHONE_NUMBER); - String code = context.getHttpRequest().getDecodedFormParameters().getFirst(SupportPhonePages.FIELD_VERIFICATION_CODE); - try { - phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(),phoneNumber); - phoneVerificationCodeProvider.validateCode(context.getUser(), phoneNumber, code, TokenCodeType.OTP); - - PhoneOtpCredentialProvider ocp = (PhoneOtpCredentialProvider) context.getSession() - .getProvider(CredentialProvider.class, PhoneOtpCredentialProviderFactory.PROVIDER_ID); - ocp.createCredential(context.getRealm(), context.getUser(), - PhoneOtpCredentialModel.create(phoneNumber,code,Utils.getOtpExpires(context.getSession()))); - context.getAuthenticationSession().setAuthNote(VERIFIED_PHONE_NUMBER, phoneNumber); - context.success(); - } catch (BadRequestException e) { - - Response challenge = context.form() - .setError(SupportPhonePages.Errors.NO_PROCESS.message()) - .createForm("login-sms-otp-config.ftl"); - context.challenge(challenge); - - } catch (ForbiddenException e) { - - Response challenge = context.form() - .setAttribute("phoneNumber", phoneNumber) - .setError(SupportPhonePages.Errors.NOT_MATCH.message()) - .createForm("login-sms-otp-config.ftl"); - context.challenge(challenge); - } catch (PhoneNumberInvalidException e) { - Response challenge = context.form() - .setError(e.getErrorType().message()) - .createForm("login-sms-otp-config.ftl"); - context.challenge(challenge); + public static final String PROVIDER_ID = "CONFIGURE_SMS_OTP"; + + @Override + public void evaluateTriggers(RequiredActionContext context) { + } + + // private Optional + // getOTPCredential(RequiredActionContext context){ + // return Optional.ofNullable(context.getUser()) + // .flatMap(user -> + // user.credentialManager().getStoredCredentialsByTypeStream(PhoneOtpCredentialModel.TYPE).findFirst()) + // .map(credentialModel -> (PhoneOtpCredentialModel) credentialModel); + // } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + + var userPhoneNumber = PhoneOtpCredentialModel.getSmsOtpCredentialData(context.getUser()) + .map(PhoneOtpCredentialModel.SmsOtpCredentialData::getPhoneNumber) + .orElseGet(() -> Optional.ofNullable(context.getUser()) + .flatMap(user -> Optional + .ofNullable(user.getFirstAttribute( + SupportPhonePages.FIELD_PHONE_NUMBER))) + .orElse(null)); + + Response challenge = context.form() + .setAttribute(SupportPhonePages.FIELD_PHONE_NUMBER, userPhoneNumber) + .createForm("login-sms-otp-config.ftl"); + context.challenge(challenge); } - } - @Override - public void close() { - } + @Override + public void processAction(RequiredActionContext context) { + var session = context.getSession(); + PhoneVerificationCodeProvider phoneVerificationCodeProvider = session + .getProvider(PhoneVerificationCodeProvider.class); + String phoneNumber = context.getHttpRequest().getDecodedFormParameters() + .getFirst(SupportPhonePages.FIELD_PHONE_NUMBER); + String code = context.getHttpRequest().getDecodedFormParameters() + .getFirst(SupportPhonePages.FIELD_VERIFICATION_CODE); + try { + phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(), phoneNumber); + phoneVerificationCodeProvider.validateCode(context.getUser(), phoneNumber, code, + TokenCodeType.OTP); + + PhoneOtpCredentialProvider ocp = (PhoneOtpCredentialProvider) context.getSession() + .getProvider(CredentialProvider.class, + PhoneOtpCredentialProviderFactory.PROVIDER_ID); + ocp.createCredential(context.getRealm(), context.getUser(), + PhoneOtpCredentialModel.create(phoneNumber, code, + Utils.getOtpExpires(context.getSession()))); + context.getAuthenticationSession().setAuthNote(VERIFIED_PHONE_NUMBER, phoneNumber); + context.success(); + } catch (BadRequestException e) { + + Response challenge = context.form() + .setError(SupportPhonePages.Errors.NO_PROCESS.message()) + .createForm("login-sms-otp-config.ftl"); + context.challenge(challenge); + + } catch (ForbiddenException e) { + + Response challenge = context.form() + .setAttribute("phoneNumber", phoneNumber) + .setError(SupportPhonePages.Errors.NOT_MATCH.message()) + .createForm("login-sms-otp-config.ftl"); + context.challenge(challenge); + } catch (PhoneNumberInvalidException e) { + Response challenge = context.form() + .setError(e.getErrorType().message()) + .createForm("login-sms-otp-config.ftl"); + context.challenge(challenge); + } + } + + @Override + public void close() { + } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/UpdatePhoneNumberRequiredAction.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/UpdatePhoneNumberRequiredAction.java index 31c98fff..4d6225ea 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/UpdatePhoneNumberRequiredAction.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/authentication/requiredactions/UpdatePhoneNumberRequiredAction.java @@ -4,14 +4,12 @@ import cc.coopersoft.keycloak.phone.authentication.forms.SupportPhonePages; import cc.coopersoft.keycloak.phone.providers.exception.PhoneNumberInvalidException; import cc.coopersoft.keycloak.phone.providers.spi.PhoneVerificationCodeProvider; -import cc.coopersoft.keycloak.phone.providers.spi.PhoneProvider; -import com.google.i18n.phonenumbers.NumberParseException; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionProvider; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.core.Response; public class UpdatePhoneNumberRequiredAction implements RequiredActionProvider { @@ -30,11 +28,14 @@ public void requiredActionChallenge(RequiredActionContext context) { @Override public void processAction(RequiredActionContext context) { - PhoneVerificationCodeProvider phoneVerificationCodeProvider = context.getSession().getProvider(PhoneVerificationCodeProvider.class); - String phoneNumber = context.getHttpRequest().getDecodedFormParameters().getFirst(SupportPhonePages.FIELD_PHONE_NUMBER); - String code = context.getHttpRequest().getDecodedFormParameters().getFirst(SupportPhonePages.FIELD_VERIFICATION_CODE); + PhoneVerificationCodeProvider phoneVerificationCodeProvider = context.getSession() + .getProvider(PhoneVerificationCodeProvider.class); + String phoneNumber = context.getHttpRequest().getDecodedFormParameters() + .getFirst(SupportPhonePages.FIELD_PHONE_NUMBER); + String code = context.getHttpRequest().getDecodedFormParameters() + .getFirst(SupportPhonePages.FIELD_VERIFICATION_CODE); try { - phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(),phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(context.getSession(), phoneNumber); phoneVerificationCodeProvider.validateCode(context.getUser(), phoneNumber, code); context.success(); } catch (BadRequestException e) { @@ -53,9 +54,9 @@ public void processAction(RequiredActionContext context) { context.challenge(challenge); } catch (PhoneNumberInvalidException e) { Response challenge = context.form() - .setAttribute("phoneNumber", phoneNumber) - .setError(e.getErrorType().message()) - .createForm("login-update-phone-number.ftl"); + .setAttribute("phoneNumber", phoneNumber) + .setError(e.getErrorType().message()) + .createForm("login-update-phone-number.ftl"); context.challenge(challenge); } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialModel.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialModel.java index d5ba710e..f36a8d26 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialModel.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialModel.java @@ -10,7 +10,7 @@ import org.keycloak.models.credential.dto.OTPSecretData; import org.keycloak.util.JsonSerialization; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import java.io.IOException; import java.util.Date; import java.util.Optional; @@ -26,39 +26,42 @@ public PhoneOtpCredentialModel(SmsOtpCredentialData credentialData, OTPSecretDat this.secretData = secretData; } - private static Optional getOtpCredentialModel(@NotNull UserModel user){ + private static Optional getOtpCredentialModel(@NotNull UserModel user) { return user.credentialManager() - .getStoredCredentialsByTypeStream(PhoneOtpCredentialModel.TYPE).findFirst(); + .getStoredCredentialsByTypeStream(PhoneOtpCredentialModel.TYPE).findFirst(); } - public static Optional getSmsOtpCredentialData(@NotNull UserModel user){ + public static Optional getSmsOtpCredentialData( + @NotNull UserModel user) { return getOtpCredentialModel(user) - .map(credentialModel -> { - try { - return JsonSerialization.readValue(credentialModel.getCredentialData(), PhoneOtpCredentialModel.SmsOtpCredentialData.class); - } catch (IOException e) { - throw new IllegalArgumentException(e); - } - }); + .map(credentialModel -> { + try { + return JsonSerialization.readValue(credentialModel.getCredentialData(), + PhoneOtpCredentialModel.SmsOtpCredentialData.class); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + }); } public static void updateOtpCredential(@NotNull UserModel user, - @NotNull PhoneOtpCredentialModel.SmsOtpCredentialData credentialData, - String secretValue){ + @NotNull PhoneOtpCredentialModel.SmsOtpCredentialData credentialData, + String secretValue) { getOtpCredentialModel(user) - .ifPresent(credential -> { - try { - credential.setCredentialData(JsonSerialization.writeValueAsString(credentialData)); - credential.setSecretData(JsonSerialization.writeValueAsString(new OTPSecretData(secretValue))); - PhoneOtpCredentialModel credentialModel = PhoneOtpCredentialModel.createFromCredentialModel(credential); - user.credentialManager().updateStoredCredential(credentialModel); - }catch (IOException ioe) { - throw new RuntimeException(ioe); - } - }); + .ifPresent(credential -> { + try { + credential.setCredentialData(JsonSerialization.writeValueAsString(credentialData)); + credential.setSecretData(JsonSerialization.writeValueAsString(new OTPSecretData(secretValue))); + PhoneOtpCredentialModel credentialModel = PhoneOtpCredentialModel + .createFromCredentialModel(credential); + user.credentialManager().updateStoredCredential(credentialModel); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + }); } - public static PhoneOtpCredentialModel create(String phoneNumber, String secretValue,int expires) { + public static PhoneOtpCredentialModel create(String phoneNumber, String secretValue, int expires) { SmsOtpCredentialData credentialData = new SmsOtpCredentialData(phoneNumber, expires); OTPSecretData secretData = new OTPSecretData(secretValue); PhoneOtpCredentialModel credentialModel = new PhoneOtpCredentialModel(credentialData, secretData); @@ -69,8 +72,10 @@ public static PhoneOtpCredentialModel create(String phoneNumber, String secretVa public static PhoneOtpCredentialModel createFromCredentialModel(CredentialModel credentialModel) { try { - SmsOtpCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), SmsOtpCredentialData.class); - OTPSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), OTPSecretData.class); + SmsOtpCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), + SmsOtpCredentialData.class); + OTPSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), + OTPSecretData.class); PhoneOtpCredentialModel credential = new PhoneOtpCredentialModel(credentialData, secretData); credential.setUserLabel(credentialModel.getUserLabel()); @@ -105,8 +110,6 @@ public OTPSecretData getOTPSecretData() { return secretData; } - - @Getter public static class SmsOtpCredentialData { private final String phoneNumber; @@ -116,17 +119,17 @@ public static class SmsOtpCredentialData { private final int expires; @JsonIgnore - public boolean isSecretInvalid(){ - if (expires <= 0){ + public boolean isSecretInvalid() { + if (expires <= 0) { return true; } - return new Date().getTime() > expires * 1000L + secretCreate; + return new Date().getTime() > expires * 1000L + secretCreate; } @JsonCreator -// @ConstructorProperties("phoneNumber") - public SmsOtpCredentialData(@JsonProperty("phoneNumber")String phoneNumber, - @JsonProperty("expires") int expires) { + // @ConstructorProperties("phoneNumber") + public SmsOtpCredentialData(@JsonProperty("phoneNumber") String phoneNumber, + @JsonProperty("expires") int expires) { this.phoneNumber = phoneNumber; this.secretCreate = new Date().getTime(); this.expires = expires; diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialProviderFactory.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialProviderFactory.java index 1166952f..bf002771 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialProviderFactory.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/credential/PhoneOtpCredentialProviderFactory.java @@ -1,6 +1,5 @@ package cc.coopersoft.keycloak.phone.credential; -import org.keycloak.credential.CredentialProvider; import org.keycloak.credential.CredentialProviderFactory; import org.keycloak.models.KeycloakSession; @@ -9,7 +8,7 @@ public class PhoneOtpCredentialProviderFactory implements CredentialProviderFact public final static String PROVIDER_ID = "sms-otp"; @Override - public CredentialProvider create(KeycloakSession session) { + public PhoneOtpCredentialProvider create(KeycloakSession session) { return new PhoneOtpCredentialProvider(session); } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/jpa/TokenCode.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/jpa/TokenCode.java index 8a25ab2f..804dc197 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/jpa/TokenCode.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/jpa/TokenCode.java @@ -2,70 +2,63 @@ import lombok.Data; -import javax.persistence.*; +import jakarta.persistence.*; import java.util.Date; @Entity @Data @Table(name = "PHONE_MESSAGE_TOKEN_CODE") @NamedQueries({ - @NamedQuery( - name = "ongoingProcess", - query = "FROM TokenCode t WHERE t.realmId = :realmId " + - "AND t.phoneNumber = :phoneNumber " + - "AND t.expiresAt >= :now AND t.type = :type" - ), - @NamedQuery( - name = "processesSinceTarget", - query = "SELECT COUNT(t) FROM TokenCode t WHERE t.realmId = :realmId " + - "AND t.phoneNumber = :phoneNumber " + - "AND t.createdAt >= :date AND t.type = :type" - ), - @NamedQuery( - name = "processesSinceSource", - query = "SELECT COUNT(t) FROM TokenCode t WHERE t.realmId = :realmId " + - "AND t.ip = :addr " + - "AND t.createdAt >= :date AND t.type = :type" - ) + @NamedQuery(name = "ongoingProcess", query = "FROM TokenCode t WHERE t.realmId = :realmId " + + "AND t.phoneNumber = :phoneNumber " + + "AND t.expiresAt >= :now AND t.type = :type"), + @NamedQuery(name = "processesSinceTarget", query = "SELECT COUNT(t) FROM TokenCode t WHERE t.realmId = :realmId " + + + "AND t.phoneNumber = :phoneNumber " + + "AND t.createdAt >= :date AND t.type = :type"), + @NamedQuery(name = "processesSinceSource", query = "SELECT COUNT(t) FROM TokenCode t WHERE t.realmId = :realmId " + + + "AND t.ip = :addr " + + "AND t.createdAt >= :date AND t.type = :type") }) public class TokenCode { - @Id - @Column(name = "ID") - private String id; + @Id + @Column(name = "ID") + private String id; - @Column(name = "REALM_ID", nullable = false) - private String realmId; + @Column(name = "REALM_ID", nullable = false) + private String realmId; - @Column(name = "PHONE_NUMBER", nullable = false) - private String phoneNumber; + @Column(name = "PHONE_NUMBER", nullable = false) + private String phoneNumber; - @Column(name = "TYPE", nullable = false) - private String type; + @Column(name = "TYPE", nullable = false) + private String type; - @Column(name = "CODE", nullable = false) - private String code; + @Column(name = "CODE", nullable = false) + private String code; - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "CREATED_AT", nullable = false) - private Date createdAt; + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "CREATED_AT", nullable = false) + private Date createdAt; - @Temporal(TemporalType.TIMESTAMP) - @Column(name = "EXPIRES_AT", nullable = false) - private Date expiresAt; + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "EXPIRES_AT", nullable = false) + private Date expiresAt; - @Column(name = "CONFIRMED", nullable = false) - private Boolean confirmed; + @Column(name = "CONFIRMED", nullable = false) + private Boolean confirmed; - @Column(name = "BY_WHOM", nullable = true) - private String byWhom; + @Column(name = "BY_WHOM", nullable = true) + private String byWhom; - @Column(name = "IP") - private String ip; + @Column(name = "IP") + private String ip; - @Column(name = "PORT") - private Integer port; + @Column(name = "PORT") + private Integer port; - @Column(name = "HOST") - private String host; + @Column(name = "HOST") + private String host; } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResource.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResource.java index 90ecfd94..fa3dc6fe 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResource.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResource.java @@ -3,7 +3,7 @@ import cc.coopersoft.keycloak.phone.providers.constants.TokenCodeType; import org.keycloak.models.KeycloakSession; -import javax.ws.rs.Path; +import jakarta.ws.rs.Path; public class SmsResource { diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResourceProviderFactory.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResourceProviderFactory.java index 7e870da6..69c1ec6d 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResourceProviderFactory.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/SmsResourceProviderFactory.java @@ -1,17 +1,13 @@ package cc.coopersoft.keycloak.phone.providers.rest; -import org.jboss.logging.Logger; import org.keycloak.Config.Scope; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.services.resource.RealmResourceProvider; import org.keycloak.services.resource.RealmResourceProviderFactory; - public class SmsResourceProviderFactory implements RealmResourceProviderFactory { - private static final Logger logger = Logger.getLogger(SmsResourceProviderFactory.class); - @Override public String getId() { return "sms"; diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/TokenCodeResource.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/TokenCodeResource.java index 551f79fe..f3c764c3 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/TokenCodeResource.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/TokenCodeResource.java @@ -9,12 +9,12 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.services.validation.Validation; -import javax.validation.constraints.NotBlank; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.validation.constraints.NotBlank; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; public class TokenCodeResource { @@ -27,26 +27,26 @@ public class TokenCodeResource { this.tokenCodeType = tokenCodeType; } - @GET @NoCache @Path("") @Produces(APPLICATION_JSON) public Response getTokenCode(@NotBlank @QueryParam("phoneNumber") String phoneNumber, - @QueryParam("kind") String kind) { + @QueryParam("kind") String kind) { - if (Validation.isBlank(phoneNumber)) throw new BadRequestException("Must supply a phone number"); + if (Validation.isBlank(phoneNumber)) + throw new BadRequestException("Must supply a phone number"); var phoneProvider = session.getProvider(PhoneProvider.class); try { - phoneNumber = Utils.canonicalizePhoneNumber(session,phoneNumber); + phoneNumber = Utils.canonicalizePhoneNumber(session, phoneNumber); } catch (PhoneNumberInvalidException e) { throw new BadRequestException("Phone number is invalid"); } // everybody phones authenticator send AUTH code - if( !TokenCodeType.REGISTRATION.equals(tokenCodeType) && + if (!TokenCodeType.REGISTRATION.equals(tokenCodeType) && !TokenCodeType.AUTH.equals(tokenCodeType) && !TokenCodeType.VERIFY.equals(tokenCodeType) && Utils.findUserByPhone(session, session.getContext().getRealm(), phoneNumber).isEmpty()) { diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/VerificationCodeResource.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/VerificationCodeResource.java index 95aec5fc..3a98d77a 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/VerificationCodeResource.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/rest/VerificationCodeResource.java @@ -2,22 +2,20 @@ import cc.coopersoft.keycloak.phone.providers.constants.TokenCodeType; import cc.coopersoft.keycloak.phone.providers.spi.PhoneVerificationCodeProvider; -import org.jboss.logging.Logger; + import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.models.KeycloakSession; import org.keycloak.models.UserModel; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager.AuthResult; -import javax.ws.rs.*; -import javax.ws.rs.core.Response; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; -import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; public class VerificationCodeResource extends TokenCodeResource { - private static final Logger logger = Logger.getLogger(VerificationCodeResource.class); - private final AuthResult auth; VerificationCodeResource(KeycloakSession session) { @@ -34,11 +32,14 @@ private PhoneVerificationCodeProvider getTokenCodeService() { @Path("") @Produces(APPLICATION_JSON) public Response checkVerificationCode(@QueryParam("phoneNumber") String phoneNumber, - @QueryParam("code") String code) { - - if (auth == null) throw new NotAuthorizedException("Bearer"); - if (phoneNumber == null) throw new BadRequestException("Must inform a phone number"); - if (code == null) throw new BadRequestException("Must inform a token code"); + @QueryParam("code") String code) { + + if (auth == null) + throw new NotAuthorizedException("Bearer"); + if (phoneNumber == null) + throw new BadRequestException("Must inform a phone number"); + if (code == null) + throw new BadRequestException("Must inform a token code"); UserModel user = auth.getUser(); getTokenCodeService().validateCode(user, phoneNumber, code); diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/PhoneSpi.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/PhoneSpi.java index 1b8153f9..7a4343c8 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/PhoneSpi.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/PhoneSpi.java @@ -1,7 +1,5 @@ package cc.coopersoft.keycloak.phone.providers.spi; -import org.keycloak.provider.Provider; -import org.keycloak.provider.ProviderFactory; import org.keycloak.provider.Spi; public class PhoneSpi implements Spi { @@ -17,12 +15,12 @@ public String getName() { } @Override - public Class getProviderClass() { + public Class getProviderClass() { return PhoneProvider.class; } @Override - public Class getProviderFactoryClass() { + public Class getProviderFactoryClass() { return PhoneProviderFactory.class; } } diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneProvider.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneProvider.java index 350fd97f..857caa55 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneProvider.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneProvider.java @@ -12,8 +12,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.services.validation.Validation; -import javax.ws.rs.ForbiddenException; -import javax.ws.rs.ServiceUnavailableException; +import jakarta.ws.rs.ForbiddenException; +import jakarta.ws.rs.ServiceUnavailableException; import java.time.Instant; import java.util.Optional; @@ -32,24 +32,23 @@ public class DefaultPhoneProvider implements PhoneProvider { this.session = session; this.config = config; - this.service = session.listProviderIds(MessageSenderService.class) .stream().filter(s -> s.equals(config.get("service"))) .findFirst().orElse( session.listProviderIds(MessageSenderService.class) - .stream().findFirst().orElse(null) - ); + .stream().findFirst().orElse(null)); - if (Validation.isBlank(this.service)){ + if (Validation.isBlank(this.service)) { logger.error("Message sender service provider not found!"); } if (Validation.isBlank(config.get("service"))) logger.warn("No message sender service provider specified! Default provider'" + - this.service + "' will be used. You can use keycloak start param '--spi-phone-default-service' to specify a different one. "); + this.service + + "' will be used. You can use keycloak start param '--spi-phone-default-service' to specify a different one. "); this.tokenExpiresIn = config.getInt("tokenExpiresIn", 60); - this.targetHourMaximum = config.getInt("targetHourMaximum",3); + this.targetHourMaximum = config.getInt("targetHourMaximum", 3); this.sourceHourMaximum = config.getInt("sourceHourMaximum", 10); } @@ -57,24 +56,23 @@ public class DefaultPhoneProvider implements PhoneProvider { public void close() { } - private PhoneVerificationCodeProvider getTokenCodeService() { return session.getProvider(PhoneVerificationCodeProvider.class); } - private String getRealmName(){ + private String getRealmName() { return session.getContext().getRealm().getName(); } - private Optional getStringConfigValue(String configName){ + private Optional getStringConfigValue(String configName) { return OptionalUtils.ofBlank(OptionalUtils.ofBlank(config.get(getRealmName() + "-" + configName)) - .orElse(config.get(configName))); + .orElse(config.get(configName))); } - private boolean getBooleanConfigValue(String configName, boolean defaultValue){ - Boolean result = config.getBoolean(getRealmName() + "-" + configName,null); + private boolean getBooleanConfigValue(String configName, boolean defaultValue) { + Boolean result = config.getBoolean(getRealmName() + "-" + configName, null); if (result == null) { - result = config.getBoolean(configName,defaultValue); + result = config.getBoolean(configName, defaultValue); } return result; } @@ -115,27 +113,28 @@ public Optional phoneNumberRegex() { } @Override - public int sendTokenCode(String phoneNumber,String sourceAddr,TokenCodeType type, String kind){ + public int sendTokenCode(String phoneNumber, String sourceAddr, TokenCodeType type, String kind) { - logger.info("send code to:" + phoneNumber ); + logger.info("send code to:" + phoneNumber); - if (getTokenCodeService().isAbusing(phoneNumber, type,sourceAddr, sourceHourMaximum, targetHourMaximum)) { + if (getTokenCodeService().isAbusing(phoneNumber, type, sourceAddr, sourceHourMaximum, targetHourMaximum)) { throw new ForbiddenException("You requested the maximum number of messages the last hour"); } TokenCodeRepresentation ongoing = getTokenCodeService().ongoingProcess(phoneNumber, type); if (ongoing != null) { - logger.info(String.format("No need of sending a new %s code for %s",type.label, phoneNumber)); + logger.info(String.format("No need of sending a new %s code for %s", type.label, phoneNumber)); return (int) (ongoing.getExpiresAt().getTime() - Instant.now().toEpochMilli()) / 1000; } TokenCodeRepresentation token = TokenCodeRepresentation.forPhoneNumber(phoneNumber); try { - session.getProvider(MessageSenderService.class, service).sendSmsMessage(type,phoneNumber,token.getCode(),tokenExpiresIn,kind); + session.getProvider(MessageSenderService.class, service).sendSmsMessage(type, phoneNumber, token.getCode(), + tokenExpiresIn, kind); getTokenCodeService().persistCode(token, type, tokenExpiresIn); - logger.info(String.format("Sent %s code to %s over %s",type.label, phoneNumber, service)); + logger.info(String.format("Sent %s code to %s over %s", type.label, phoneNumber, service)); } catch (MessageSendException e) { diff --git a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneVerificationCodeProvider.java b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneVerificationCodeProvider.java index 430f1571..eb747050 100644 --- a/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneVerificationCodeProvider.java +++ b/keycloak-phone-provider/src/main/java/cc/coopersoft/keycloak/phone/providers/spi/impl/DefaultPhoneVerificationCodeProvider.java @@ -21,11 +21,11 @@ import org.keycloak.services.validation.Validation; import org.keycloak.util.JsonSerialization; -import javax.persistence.EntityManager; -import javax.persistence.NoResultException; -import javax.persistence.TemporalType; -import javax.ws.rs.BadRequestException; -import javax.ws.rs.ForbiddenException; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.TemporalType; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.ForbiddenException; import java.io.IOException; import java.time.Instant; import java.util.Date; @@ -79,37 +79,37 @@ public TokenCodeRepresentation ongoingProcess(String phoneNumber, TokenCodeType } catch (NoResultException e) { return null; } catch (PhoneNumberInvalidException e) { - logger.warn("Invalid number: "+phoneNumber); + logger.warn("Invalid number: " + phoneNumber); throw new BadRequestException("Phone number is invalid"); } } @Override public boolean isAbusing(String phoneNumber, TokenCodeType tokenCodeType, - String sourceAddr, int sourceHourMaximum, int targetHourMaximum) { + String sourceAddr, int sourceHourMaximum, int targetHourMaximum) { Date oneHourAgo = new Date(System.currentTimeMillis() - TimeUnit.HOURS.toMillis(1)); - if (targetHourMaximum > 0){ + if (targetHourMaximum > 0) { long targetCount = (getEntityManager() - .createNamedQuery("processesSinceTarget", Long.class) - .setParameter("realmId", getRealm().getId()) - .setParameter("phoneNumber", phoneNumber) - .setParameter("date", oneHourAgo, TemporalType.TIMESTAMP) - .setParameter("type", tokenCodeType.name()) - .getSingleResult()); + .createNamedQuery("processesSinceTarget", Long.class) + .setParameter("realmId", getRealm().getId()) + .setParameter("phoneNumber", phoneNumber) + .setParameter("date", oneHourAgo, TemporalType.TIMESTAMP) + .setParameter("type", tokenCodeType.name()) + .getSingleResult()); if (targetCount > targetHourMaximum) return true; } - if (sourceHourMaximum > 0){ + if (sourceHourMaximum > 0) { long sourceCount = (getEntityManager() - .createNamedQuery("processesSinceSource", Long.class) - .setParameter("realmId", getRealm().getId()) - .setParameter("addr", sourceAddr) - .setParameter("date", oneHourAgo, TemporalType.TIMESTAMP) - .setParameter("type", tokenCodeType.name()) - .getSingleResult()); + .createNamedQuery("processesSinceSource", Long.class) + .setParameter("realmId", getRealm().getId()) + .setParameter("addr", sourceAddr) + .setParameter("date", oneHourAgo, TemporalType.TIMESTAMP) + .setParameter("type", tokenCodeType.name()) + .getSingleResult()); if (sourceCount > sourceHourMaximum) return true; } @@ -154,60 +154,63 @@ public void validateCode(UserModel user, String phoneNumber, String code, TokenC if (tokenCode == null) throw new BadRequestException(String.format("There is no valid ongoing %s process", tokenCodeType.label)); - if (!tokenCode.getCode().equals(code)) throw new ForbiddenException("Code does not match with expected value"); + if (!tokenCode.getCode().equals(code)) + throw new ForbiddenException("Code does not match with expected value"); logger.info(String.format("User %s correctly answered the %s code", user.getId(), tokenCodeType.label)); - tokenValidated(user,phoneNumber,tokenCode.getId(),TokenCodeType.OTP.equals(tokenCodeType)); + tokenValidated(user, phoneNumber, tokenCode.getId(), TokenCodeType.OTP.equals(tokenCodeType)); if (TokenCodeType.OTP.equals(tokenCodeType)) - updateUserOTPCredential(user,phoneNumber,tokenCode.getCode()); + updateUserOTPCredential(user, phoneNumber, tokenCode.getCode()); } @Override public void tokenValidated(UserModel user, String phoneNumber, String tokenCodeId, boolean isOTP) { boolean updateUserPhoneNumber = !isOTP; - if (isOTP){ + if (isOTP) { updateUserPhoneNumber = PhoneOtpCredentialModel.getSmsOtpCredentialData(user) - .map(PhoneOtpCredentialModel.SmsOtpCredentialData::getPhoneNumber) - .map(pn -> pn.equals(phoneNumber)) - .orElse(false); + .map(PhoneOtpCredentialModel.SmsOtpCredentialData::getPhoneNumber) + .map(pn -> pn.equals(phoneNumber)) + .orElse(false); } - - if (updateUserPhoneNumber){ - if (!Utils.isDuplicatePhoneAllowed(session)){ + if (updateUserPhoneNumber) { + if (!Utils.isDuplicatePhoneAllowed(session)) { session.users() - .searchForUserByUserAttributeStream(session.getContext().getRealm(),"phoneNumber", phoneNumber) - .filter(u -> !u.getId().equals(user.getId())) - .forEach(u -> { - logger.info(String.format("User %s also has phone number %s. Un-verifying.", u.getId(), phoneNumber)); - u.setSingleAttribute("phoneNumberVerified", "false"); - - u.addRequiredAction(UpdatePhoneNumberRequiredAction.PROVIDER_ID); - - //remove otp Credentials - u.credentialManager() - .getStoredCredentialsByTypeStream(PhoneOtpCredentialModel.TYPE) - .filter(c -> { - try { - PhoneOtpCredentialModel.SmsOtpCredentialData credentialData = - JsonSerialization.readValue(c.getCredentialData(), PhoneOtpCredentialModel.SmsOtpCredentialData.class); - if (Validation.isBlank(credentialData.getPhoneNumber())){ - return true; - } - return credentialData.getPhoneNumber().equals(user.getFirstAttribute("phoneNumber")); - } catch (IOException e) { - logger.warn("Unknown format Otp Credential", e); - return true; - } - }) - .map(CredentialModel::getId) - .collect(Collectors.toList()) - .forEach(id -> u.credentialManager().removeStoredCredentialById(id)); - }); + .searchForUserByUserAttributeStream(session.getContext().getRealm(), "phoneNumber", phoneNumber) + .filter(u -> !u.getId().equals(user.getId())) + .forEach(u -> { + logger.info(String.format("User %s also has phone number %s. Un-verifying.", u.getId(), + phoneNumber)); + u.setSingleAttribute("phoneNumberVerified", "false"); + + u.addRequiredAction(UpdatePhoneNumberRequiredAction.PROVIDER_ID); + + // remove otp Credentials + u.credentialManager() + .getStoredCredentialsByTypeStream(PhoneOtpCredentialModel.TYPE) + .filter(c -> { + try { + PhoneOtpCredentialModel.SmsOtpCredentialData credentialData = JsonSerialization + .readValue(c.getCredentialData(), + PhoneOtpCredentialModel.SmsOtpCredentialData.class); + if (Validation.isBlank(credentialData.getPhoneNumber())) { + return true; + } + return credentialData.getPhoneNumber() + .equals(user.getFirstAttribute("phoneNumber")); + } catch (IOException e) { + logger.warn("Unknown format Otp Credential", e); + return true; + } + }) + .map(CredentialModel::getId) + .collect(Collectors.toList()) + .forEach(id -> u.credentialManager().removeStoredCredentialById(id)); + }); } user.setSingleAttribute("phoneNumberVerified", "true"); user.setSingleAttribute("phoneNumber", phoneNumber); @@ -217,7 +220,6 @@ public void tokenValidated(UserModel user, String phoneNumber, String tokenCodeI validateProcess(tokenCodeId, user); - } @Override @@ -228,18 +230,17 @@ public void validateProcess(String tokenCodeId, UserModel user) { getEntityManager().persist(entity); } - - private void updateUserOTPCredential(UserModel user, String phoneNumber, String code) { + private void updateUserOTPCredential(UserModel user, String phoneNumber, String code) { user.removeRequiredAction(ConfigSmsOtpRequiredAction.PROVIDER_ID); - PhoneOtpCredentialProvider ocp = (PhoneOtpCredentialProvider) - session.getProvider(CredentialProvider.class, PhoneOtpCredentialProviderFactory.PROVIDER_ID); + PhoneOtpCredentialProvider ocp = (PhoneOtpCredentialProvider) session.getProvider(CredentialProvider.class, + PhoneOtpCredentialProviderFactory.PROVIDER_ID); if (ocp.isConfiguredFor(getRealm(), user, PhoneOtpCredentialModel.TYPE)) { - var credentialData = new PhoneOtpCredentialModel.SmsOtpCredentialData(phoneNumber,Utils.getOtpExpires(session)); - PhoneOtpCredentialModel.updateOtpCredential(user,credentialData,code); + var credentialData = new PhoneOtpCredentialModel.SmsOtpCredentialData(phoneNumber, + Utils.getOtpExpires(session)); + PhoneOtpCredentialModel.updateOtpCredential(user, credentialData, code); } } - @Override public void close() { } diff --git a/keycloak-sms-provider-aws-sns/pom.xml b/keycloak-sms-provider-aws-sns/pom.xml index e56dd8f7..e165230e 100755 --- a/keycloak-sms-provider-aws-sns/pom.xml +++ b/keycloak-sms-provider-aws-sns/pom.xml @@ -30,7 +30,7 @@ com.amazonaws aws-java-sdk-bom - 1.12.419 + 1.12.762 pom import diff --git a/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsMessageSenderServiceProviderFactory.java b/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsMessageSenderServiceProviderFactory.java index 0ac57f98..a9e88fee 100755 --- a/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsMessageSenderServiceProviderFactory.java +++ b/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsMessageSenderServiceProviderFactory.java @@ -12,7 +12,7 @@ public class AwsSnsMessageSenderServiceProviderFactory implements MessageSenderS @Override public MessageSenderService create(KeycloakSession keycloakSession) { - return new AwsSnsSmsSenderService(keycloakSession.getContext().getRealm().getDisplayName(), config); + return new AwsSnsSmsSenderService(keycloakSession, config); } @Override diff --git a/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsSmsSenderService.java b/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsSmsSenderService.java index 0ccbe155..4a317fb3 100755 --- a/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsSmsSenderService.java +++ b/keycloak-sms-provider-aws-sns/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/AwsSnsSmsSenderService.java @@ -9,6 +9,7 @@ import com.amazonaws.services.sns.model.PublishResult; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; import java.util.HashMap; import java.util.Map; @@ -18,8 +19,8 @@ public class AwsSnsSmsSenderService extends FullSmsSenderAbstractService { private static final Logger logger = Logger.getLogger(AwsSnsSmsSenderService.class); private final Config.Scope config; - public AwsSnsSmsSenderService(String realmDisplay, Config.Scope config) { - super(realmDisplay); + public AwsSnsSmsSenderService(KeycloakSession session, Config.Scope config) { + super(session); this.config = config; } @@ -49,13 +50,14 @@ public void sendMessage(String phoneNumber, String message) throws MessageSendEx } private static void sendSMSMessage(AmazonSNS snsClient, String message, - String phoneNumber, Map smsAttributes) { + String phoneNumber, Map smsAttributes) { PublishResult result = snsClient.publish(new PublishRequest() .withMessage(message) .withPhoneNumber(phoneNumber) .withMessageAttributes(smsAttributes)); - logger.debug(String.format("Sent phone verification code via aws sns with message id %s", result.getMessageId())); + logger.debug( + String.format("Sent phone verification code via aws sns with message id %s", result.getMessageId())); } @Override diff --git a/keycloak-sms-provider-bulksms/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/BulksmsSmsSenderServiceProvider.java b/keycloak-sms-provider-bulksms/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/BulksmsSmsSenderServiceProvider.java index 11f9f073..de1af807 100644 --- a/keycloak-sms-provider-bulksms/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/BulksmsSmsSenderServiceProvider.java +++ b/keycloak-sms-provider-bulksms/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/BulksmsSmsSenderServiceProvider.java @@ -2,8 +2,6 @@ import cc.coopersoft.keycloak.phone.providers.exception.MessageSendException; import cc.coopersoft.keycloak.phone.providers.spi.FullSmsSenderAbstractService; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClients; import org.jboss.logging.Logger; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.models.KeycloakSession; @@ -28,7 +26,9 @@ public class BulksmsSmsSenderServiceProvider extends FullSmsSenderAbstractServic private final String from; private final String encoding; private final String routingGroup; + private final KeycloakSession session; + @SuppressWarnings("unused") // fields used in json serialization private static class BulksmsMessage { public String from; public String to; @@ -55,12 +55,12 @@ public BulksmsMessage(String from, String to, String body, String encoding, Stri this.from = config.get(CONFIG_FROM); this.encoding = config.get(CONFIG_ENCODING); this.routingGroup = config.get(CONFIG_ROUTING_GROUP); + this.session = session; } @Override public void sendMessage(String phoneNumber, String message) throws MessageSendException { - HttpClient httpclient = HttpClients.createDefault(); - SimpleHttp req = SimpleHttp.doPost(url, httpclient); + SimpleHttp req = SimpleHttp.doPost(url, session); req.json(new BulksmsMessage[] { new BulksmsMessage(this.from, phoneNumber, message, this.encoding, this.routingGroup) }); req.authBasic(this.username, this.password); diff --git a/keycloak-sms-provider-cloopen/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/CloopenSmsSenderServiceProvider.java b/keycloak-sms-provider-cloopen/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/CloopenSmsSenderServiceProvider.java index 2ea687d3..02c9ec50 100644 --- a/keycloak-sms-provider-cloopen/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/CloopenSmsSenderServiceProvider.java +++ b/keycloak-sms-provider-cloopen/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/CloopenSmsSenderServiceProvider.java @@ -15,10 +15,8 @@ public class CloopenSmsSenderServiceProvider implements MessageSenderService { - private static final String APP_ID_PARAM_NAME = "app"; - private static final String TEMPLATE_PARAM_NAME = "-template"; private static final Logger logger = Logger.getLogger(CloopenSmsSenderServiceProvider.class); private final CCPRestSmsSDK client; @@ -30,15 +28,14 @@ public CloopenSmsSenderServiceProvider(Config.Scope config, RealmModel realm) { this.config = config; this.realm = realm; - //生产环境请求地址:app.cloopen.com + // 生产环境请求地址:app.cloopen.com String serverIp = "app.cloopen.com"; - //请求端口 + // 请求端口 String serverPort = "8883"; - //主账号,登陆云通讯网站后,可在控制台首页看到开发者主账号ACCOUNT SID和主账号令牌AUTH TOKEN + // 主账号,登陆云通讯网站后,可在控制台首页看到开发者主账号ACCOUNT SID和主账号令牌AUTH TOKEN String accountSId = config.get("account"); String accountToken = config.get("token"); - logger.info(String.format("cloopen account: %s ; accountToken: %s", accountSId, accountToken)); client = new CCPRestSmsSDK(); @@ -55,38 +52,44 @@ public void close() { } @Override - public void sendSmsMessage(TokenCodeType type, String phoneNumber, String code, int expires, String kind) throws MessageSendException { - //请使用管理控制台中已创建应用的APPID + public void sendSmsMessage(TokenCodeType type, String phoneNumber, String code, int expires, String kind) + throws MessageSendException { + // 请使用管理控制台中已创建应用的APPID String appId = Optional.ofNullable(config.get(realm.getName().toLowerCase() + "-" + APP_ID_PARAM_NAME)) .orElse(config.get(APP_ID_PARAM_NAME)); client.setAppId(appId); String kindName = OptionalUtils.ofBlank(kind).orElse(type.name().toLowerCase()); - String templateId = Optional.ofNullable(config.get(realm.getName().toLowerCase() + "-" + kindName + "-template")) - .orElse(config.get(kindName + "-template")); + String templateId = Optional + .ofNullable(config.get(realm.getName().toLowerCase() + "-" + kindName + "-template")) + .orElse(config.get(kindName + "-template")); logger.info(String.format("cloopen appId: %s ; templateId: %s", appId, templateId)); - String[] datas = {code, String.valueOf(expires / 60) }; - -// String subAppend="1234"; //可选 扩展码,四位数字 0~9999 -// String reqId="fadfafas"; //可选 第三方自定义消息id,最大支持32位英文数字,同账号下同一自然天内不允许重复 - Map result = client.sendTemplateSMS(phoneNumber,templateId,datas); - //Map result = client.sendTemplateSMS(phoneNumber,templateId,datas,subAppend,reqId); - if("000000".equals(result.get("statusCode"))){ - //正常返回输出data包体信息(map) - Map data = (Map) result.get("data"); + String[] datas = { code, String.valueOf(expires / 60) }; + + // String subAppend="1234"; //可选 扩展码,四位数字 0~9999 + // String reqId="fadfafas"; //可选 第三方自定义消息id,最大支持32位英文数字,同账号下同一自然天内不允许重复 + Map result = client.sendTemplateSMS(phoneNumber, templateId, datas); + // Map result = + // client.sendTemplateSMS(phoneNumber,templateId,datas,subAppend,reqId); + if ("000000".equals(result.get("statusCode"))) { + // 正常返回输出data包体信息(map) + @SuppressWarnings("unchecked") + Map data = (Map) result.get("data"); logger.info("cloopen send message result: " + data.toString()); -// Set keySet = data.keySet(); -// for(String key:keySet){ -// Object object = data.get(key); -// System.out.println(key +" = "+object); -// -// } - }else{ - //异常返回输出错误码和错误信息 - throw new MessageSendException(500, result.get("statusCode").toString(), result.get("statusMsg").toString()); - //System.out.println("错误码=" + result.get("statusCode") +" 错误信息= "+result.get("statusMsg")); + // Set keySet = data.keySet(); + // for(String key:keySet){ + // Object object = data.get(key); + // System.out.println(key +" = "+object); + // + // } + } else { + // 异常返回输出错误码和错误信息 + throw new MessageSendException(500, result.get("statusCode").toString(), + result.get("statusMsg").toString()); + // System.out.println("错误码=" + result.get("statusCode") +" 错误信息= + // "+result.get("statusMsg")); } } } diff --git a/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummyMessageSenderServiceProviderFactory.java b/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummyMessageSenderServiceProviderFactory.java index c4a8f2a3..3bc06e58 100644 --- a/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummyMessageSenderServiceProviderFactory.java +++ b/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummyMessageSenderServiceProviderFactory.java @@ -10,7 +10,7 @@ public class DummyMessageSenderServiceProviderFactory implements MessageSenderSe @Override public MessageSenderService create(KeycloakSession keycloakSession) { - return new DummySmsSenderService(keycloakSession.getContext().getRealm().getDisplayName()); + return new DummySmsSenderService(keycloakSession); } @Override diff --git a/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummySmsSenderService.java b/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummySmsSenderService.java index f40b0ebf..ba40cb32 100644 --- a/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummySmsSenderService.java +++ b/keycloak-sms-provider-dummy/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/DummySmsSenderService.java @@ -3,6 +3,7 @@ import cc.coopersoft.keycloak.phone.providers.exception.MessageSendException; import cc.coopersoft.keycloak.phone.providers.spi.FullSmsSenderAbstractService; import org.jboss.logging.Logger; +import org.keycloak.models.KeycloakSession; import java.util.Random; @@ -10,8 +11,8 @@ public class DummySmsSenderService extends FullSmsSenderAbstractService { private static final Logger logger = Logger.getLogger(DummySmsSenderService.class); - public DummySmsSenderService(String realmDisplay) { - super(realmDisplay); + public DummySmsSenderService(KeycloakSession session) { + super(session); } @Override diff --git a/keycloak-sms-provider-tencent/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TencentSmsSenderServiceProvider.java b/keycloak-sms-provider-tencent/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TencentSmsSenderServiceProvider.java index 51f4107f..44f944ee 100644 --- a/keycloak-sms-provider-tencent/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TencentSmsSenderServiceProvider.java +++ b/keycloak-sms-provider-tencent/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TencentSmsSenderServiceProvider.java @@ -17,91 +17,98 @@ public class TencentSmsSenderServiceProvider implements MessageSenderService { private static final String APP_ID_PARAM_NAME = "app"; - private static final String TEMPLATE_PARAM_NAME = "template"; private final Config.Scope config; private final RealmModel realm; private final SmsClient client; - public TencentSmsSenderServiceProvider(Config.Scope config,RealmModel realm) { + public TencentSmsSenderServiceProvider(Config.Scope config, RealmModel realm) { this.config = config; this.realm = realm; - /* 必要步骤: + /* + * 必要步骤: * 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId 和 secretKey * 本示例采用从环境变量读取的方式,需要预先在环境变量中设置这两个值 * 您也可以直接在代码中写入密钥对,但需谨防泄露,不要将代码复制、上传或者分享给他人 - * CAM 密钥查询:https://console.cloud.tencent.com/cam/capi*/ + * CAM 密钥查询:https://console.cloud.tencent.com/cam/capi + */ Credential cred = new Credential(config.get("secret"), config.get("key")); -// // 实例化一个 http 选项,可选,无特殊需求时可以跳过 -// HttpProfile httpProfile = new HttpProfile(); -// // 设置代理 -// httpProfile.setProxyHost("host"); -// httpProfile.setProxyPort(port); -// /* SDK 默认使用 POST 方法。 -// * 如需使用 GET 方法,可以在此处设置,但 GET 方法无法处理较大的请求 */ -// httpProfile.setReqMethod("POST"); -// /* SDK 有默认的超时时间,非必要请不要进行调整 -// * 如有需要请在代码中查阅以获取最新的默认值 */ -// httpProfile.setConnTimeout(60); -// /* SDK 会自动指定域名,通常无需指定域名,但访问金融区的服务时必须手动指定域名 -// * 例如 SMS 的上海金融区域名为 sms.ap-shanghai-fsi.tencentcloudapi.com */ -// httpProfile.setEndpoint("sms.tencentcloudapi.com"); - -// /* 非必要步骤: -// * 实例化一个客户端配置对象,可以指定超时时间等配置 */ -// ClientProfile clientProfile = new ClientProfile(); -// /* SDK 默认用 TC3-HMAC-SHA256 进行签名 -// * 非必要请不要修改该字段 */ -// clientProfile.setSignMethod("HmacSHA256"); -// clientProfile.setHttpProfile(httpProfile); - /* 实例化 SMS 的 client 对象 - * 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 */ -// SmsClient client = new SmsClient(cred, "",clientProfile); - - client = new SmsClient(cred, config.get("region","ap-guangzhou")); + // // 实例化一个 http 选项,可选,无特殊需求时可以跳过 + // HttpProfile httpProfile = new HttpProfile(); + // // 设置代理 + // httpProfile.setProxyHost("host"); + // httpProfile.setProxyPort(port); + // /* SDK 默认使用 POST 方法。 + // * 如需使用 GET 方法,可以在此处设置,但 GET 方法无法处理较大的请求 */ + // httpProfile.setReqMethod("POST"); + // /* SDK 有默认的超时时间,非必要请不要进行调整 + // * 如有需要请在代码中查阅以获取最新的默认值 */ + // httpProfile.setConnTimeout(60); + // /* SDK 会自动指定域名,通常无需指定域名,但访问金融区的服务时必须手动指定域名 + // * 例如 SMS 的上海金融区域名为 sms.ap-shanghai-fsi.tencentcloudapi.com */ + // httpProfile.setEndpoint("sms.tencentcloudapi.com"); + + // /* 非必要步骤: + // * 实例化一个客户端配置对象,可以指定超时时间等配置 */ + // ClientProfile clientProfile = new ClientProfile(); + // /* SDK 默认用 TC3-HMAC-SHA256 进行签名 + // * 非必要请不要修改该字段 */ + // clientProfile.setSignMethod("HmacSHA256"); + // clientProfile.setHttpProfile(httpProfile); + /* + * 实例化 SMS 的 client 对象 + * 第二个参数是地域信息,可以直接填写字符串 ap-guangzhou,或者引用预设的常量 + */ + // SmsClient client = new SmsClient(cred, "",clientProfile); + + client = new SmsClient(cred, config.get("region", "ap-guangzhou")); } - @Override - public void sendSmsMessage(TokenCodeType type, String phoneNumber, String code, int expires,String kind) throws MessageSendException { + public void sendSmsMessage(TokenCodeType type, String phoneNumber, String code, int expires, String kind) + throws MessageSendException { try { - /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 + /* + * 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 * 您可以直接查询 SDK 源码确定接口有哪些属性可以设置 * 属性可能是基本类型,也可能引用了另一个数据结构 - * 推荐使用 IDE 进行开发,可以方便地跳转查阅各个接口和数据结构的文档说明 */ + * 推荐使用 IDE 进行开发,可以方便地跳转查阅各个接口和数据结构的文档说明 + */ SendSmsRequest req = new SendSmsRequest(); - /* 填充请求参数,这里 request 对象的成员变量即对应接口的入参 + /* + * 填充请求参数,这里 request 对象的成员变量即对应接口的入参 * 您可以通过官网接口文档或跳转到 request 对象的定义处查看请求参数的定义 * 基本类型的设置: * 帮助链接: * 短信控制台:https://console.cloud.tencent.com/smsv2 - * sms helper:https://cloud.tencent.com/document/product/382/3773 */ + * sms helper:https://cloud.tencent.com/document/product/382/3773 + */ /* 短信应用 ID: 在 [短信控制台] 添加应用后生成的实际 SDKAppID,例如1400006666 */ String appId = Optional.ofNullable(config.get(realm.getName().toLowerCase() + "-" + APP_ID_PARAM_NAME)) - .orElse(config.get(APP_ID_PARAM_NAME)); + .orElse(config.get(APP_ID_PARAM_NAME)); req.setSmsSdkAppid(appId); /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,可登录 [短信控制台] 查看签名信息 */ String sign = realm.getDisplayName(); req.setSign(sign); /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */ -// String senderid = "xxx"; -// req.setSenderId(senderid); + // String senderid = "xxx"; + // req.setSenderId(senderid); /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ -// String session = "xxx"; -// req.setSessionContext(session); + // String session = "xxx"; + // req.setSessionContext(session); /* 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] */ -// String extendcode = "xxx"; -// req.setExtendCode(extendcode); + // String extendcode = "xxx"; + // req.setExtendCode(extendcode); /* 模板 ID: 必须填写已审核通过的模板 ID,可登录 [短信控制台] 查看模板 ID */ String kindName = OptionalUtils.ofBlank(kind).orElse(type.name().toLowerCase()); @@ -109,17 +116,21 @@ public void sendSmsMessage(TokenCodeType type, String phoneNumber, String code, .orElse(config.get(kindName + "-template")); req.setTemplateID(templateId); - /* 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号] - * 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号*/ - String[] phoneNumbers = {phoneNumber}; + /* + * 下发手机号码,采用 e.164 标准,+[国家或地区码][手机号] + * 例如+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 + */ + String[] phoneNumbers = { phoneNumber }; req.setPhoneNumberSet(phoneNumbers); - /* 模板参数: 若无模板参数,则设置为空*/ - String[] templateParams = {code, String.valueOf(expires / 60) }; + /* 模板参数: 若无模板参数,则设置为空 */ + String[] templateParams = { code, String.valueOf(expires / 60) }; req.setTemplateParamSet(templateParams); - /* 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 - * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 */ + /* + * 通过 client 对象调用 SendSms 方法发起请求。注意请求方法名与请求对象是对应的 + * 返回的 res 是一个 SendSmsResponse 类的实例,与请求对象对应 + */ SendSmsResponse res = client.SendSms(req); // 输出 JSON 格式的字符串回包 diff --git a/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceMessageSenderServiceProviderFactory.java b/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceMessageSenderServiceProviderFactory.java index d5a6dc85..f7f8b97e 100644 --- a/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceMessageSenderServiceProviderFactory.java +++ b/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceMessageSenderServiceProviderFactory.java @@ -12,7 +12,7 @@ public class TotalVoiceMessageSenderServiceProviderFactory implements MessageSen @Override public MessageSenderService create(KeycloakSession session) { - return new TotalVoiceSmsSenderServiceProvider(config, session.getContext().getRealm().getDisplayName()); + return new TotalVoiceSmsSenderServiceProvider(session, config); } @Override diff --git a/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceSmsSenderServiceProvider.java b/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceSmsSenderServiceProvider.java index 3319fce9..6d98487e 100644 --- a/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceSmsSenderServiceProvider.java +++ b/keycloak-sms-provider-totalvoice/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TotalVoiceSmsSenderServiceProvider.java @@ -6,14 +6,15 @@ import cc.coopersoft.keycloak.phone.providers.exception.MessageSendException; import org.json.JSONObject; import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; public class TotalVoiceSmsSenderServiceProvider extends FullSmsSenderAbstractService { private final TotalVoiceClient client; private final Sms smsClient; - TotalVoiceSmsSenderServiceProvider(Scope config, String realmDisplay) { - super(realmDisplay); + TotalVoiceSmsSenderServiceProvider(KeycloakSession session, Scope config) { + super(session); this.client = new TotalVoiceClient(config.get("token")); this.smsClient = new Sms(client); } diff --git a/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioMessageSenderServiceProviderFactory.java b/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioMessageSenderServiceProviderFactory.java index 2db25788..50a61104 100644 --- a/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioMessageSenderServiceProviderFactory.java +++ b/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioMessageSenderServiceProviderFactory.java @@ -12,7 +12,7 @@ public class TwilioMessageSenderServiceProviderFactory implements MessageSenderS @Override public MessageSenderService create(KeycloakSession session) { - return new TwilioSmsSenderServiceProvider(config,session.getContext().getRealm().getDisplayName()); + return new TwilioSmsSenderServiceProvider(session, config); } @Override diff --git a/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioSmsSenderServiceProvider.java b/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioSmsSenderServiceProvider.java index 42af6c29..7ac5de36 100644 --- a/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioSmsSenderServiceProvider.java +++ b/keycloak-sms-provider-twilio/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwilioSmsSenderServiceProvider.java @@ -7,14 +7,15 @@ import cc.coopersoft.keycloak.phone.providers.spi.FullSmsSenderAbstractService; import org.jboss.logging.Logger; import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; public class TwilioSmsSenderServiceProvider extends FullSmsSenderAbstractService { private static final Logger logger = Logger.getLogger(TwilioSmsSenderServiceProvider.class); private final String twilioPhoneNumber; - TwilioSmsSenderServiceProvider(Scope config, String realmDisplay) { - super(realmDisplay); + TwilioSmsSenderServiceProvider(KeycloakSession session, Scope config) { + super(session); Twilio.init(config.get("account"), config.get("token")); this.twilioPhoneNumber = config.get("number"); diff --git a/keycloak-sms-provider-twofactorapi/pom.xml b/keycloak-sms-provider-twofactorapi/pom.xml index d9892278..52e3d292 100644 --- a/keycloak-sms-provider-twofactorapi/pom.xml +++ b/keycloak-sms-provider-twofactorapi/pom.xml @@ -30,6 +30,11 @@ okhttp 4.10.0 + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + diff --git a/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorMessageSenderServiceProviderFactory.java b/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorMessageSenderServiceProviderFactory.java index 52b085dd..6dc13142 100644 --- a/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorMessageSenderServiceProviderFactory.java +++ b/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorMessageSenderServiceProviderFactory.java @@ -12,7 +12,7 @@ public class TwoFactorMessageSenderServiceProviderFactory implements MessageSend @Override public MessageSenderService create(KeycloakSession session) { - return new TwoFactorSmsSenderServiceProvider(config,session.getContext().getRealm().getDisplayName()); + return new TwoFactorSmsSenderServiceProvider(session, config); } @Override diff --git a/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorSmsSenderServiceProvider.java b/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorSmsSenderServiceProvider.java index 713686cc..c04b171a 100644 --- a/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorSmsSenderServiceProvider.java +++ b/keycloak-sms-provider-twofactorapi/src/main/java/cc/coopersoft/keycloak/phone/providers/sender/TwoFactorSmsSenderServiceProvider.java @@ -7,8 +7,9 @@ import okhttp3.Response; import org.jboss.logging.Logger; import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; -import javax.annotation.PostConstruct; +import jakarta.annotation.PostConstruct; public class TwoFactorSmsSenderServiceProvider extends FullSmsSenderAbstractService { @@ -23,8 +24,8 @@ public void doSetUp() { .build(); } - TwoFactorSmsSenderServiceProvider(Scope config, String realmDisplay) { - super(realmDisplay); + TwoFactorSmsSenderServiceProvider(KeycloakSession session, Scope config) { + super(session); this.twoFactorApiKey = config.get("twoFactorApiKey"); } diff --git a/pom.xml b/pom.xml index 64be29f9..7bca9934 100644 --- a/pom.xml +++ b/pom.xml @@ -43,13 +43,13 @@ UTF-8 - 17 - 17 - 17 - 21.0.2 + 21 + 21 + 21 + 25.0.2 + 1.18.34 - github @@ -77,7 +77,7 @@ org.projectlombok lombok - 1.18.26 + ${version.lombok} provided @@ -104,16 +104,14 @@ ${version.keycloak} provided - - org.codehaus.mojo versions-maven-plugin - 2.10.0 + 2.17.1 false @@ -121,6 +119,7 @@ false maven-resources-plugin + 3.3.1 copy-resources