mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-10 15:32:05 -03:30
Polishing recovery codes
closes #39213 Signed-off-by: mposolda <mposolda@gmail.com>
This commit is contained in:
parent
f05f602912
commit
237d0553ae
@ -4,6 +4,7 @@ import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.credential.CredentialMetadata;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.models.PasswordPolicy;
|
||||
@ -64,7 +65,7 @@ public class RecoveryAuthnCodesCredentialModel extends CredentialModel {
|
||||
try {
|
||||
List<RecoveryAuthnCodeRepresentation> recoveryCodes = IntStream.range(0, originalGeneratedCodes.size())
|
||||
.mapToObj(i -> new RecoveryAuthnCodeRepresentation(i + 1,
|
||||
RecoveryAuthnCodesUtils.hashRawCode(originalGeneratedCodes.get(i))))
|
||||
Base64.encodeBytes(RecoveryAuthnCodesUtils.hashRawCode(originalGeneratedCodes.get(i)))))
|
||||
.collect(Collectors.toList());
|
||||
secretData = new RecoveryAuthnCodesSecretData(recoveryCodes);
|
||||
credentialData = new RecoveryAuthnCodesCredentialData(RecoveryAuthnCodesUtils.NUM_HASH_ITERATIONS,
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
package org.keycloak.models.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.common.util.SecretGenerator;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
@ -19,6 +23,8 @@ import java.util.stream.Stream;
|
||||
|
||||
public class RecoveryAuthnCodesUtils {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(RecoveryAuthnCodesUtils.class);
|
||||
|
||||
private static final int QUANTITY_OF_CODES_TO_GENERATE = 12;
|
||||
private static final int CODE_LENGTH = 12;
|
||||
public static final char[] UPPERNUM = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789".toCharArray();
|
||||
@ -28,18 +34,22 @@ public class RecoveryAuthnCodesUtils {
|
||||
public static final String RECOVERY_AUTHN_CODES_INPUT_DEFAULT_ERROR_MESSAGE = "recovery-codes-error-invalid";
|
||||
public static final String FIELD_RECOVERY_CODE_IN_BROWSER_FLOW = "recoveryCodeInput";
|
||||
|
||||
public static String hashRawCode(String rawGeneratedCode) {
|
||||
public static byte[] hashRawCode(String rawGeneratedCode) {
|
||||
Objects.requireNonNull(rawGeneratedCode, "rawGeneratedCode cannot be null");
|
||||
|
||||
byte[] rawCodeHashedAsBytes = HashUtils.hash(JavaAlgorithm.getJavaAlgorithmForHash(NOM_ALGORITHM_TO_HASH),
|
||||
return HashUtils.hash(JavaAlgorithm.getJavaAlgorithmForHash(NOM_ALGORITHM_TO_HASH),
|
||||
rawGeneratedCode.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return Base64.encodeBytes(rawCodeHashedAsBytes);
|
||||
}
|
||||
|
||||
public static boolean verifyRecoveryCodeInput(String rawInputRecoveryCode, String hashedSavedRecoveryCode) {
|
||||
String hashedInputBackupCode = hashRawCode(rawInputRecoveryCode);
|
||||
return (hashedInputBackupCode.equals(hashedSavedRecoveryCode));
|
||||
byte[] hashedInputBackupCode = hashRawCode(rawInputRecoveryCode);
|
||||
try {
|
||||
byte[] savedCode = Base64.decode(hashedSavedRecoveryCode);
|
||||
return MessageDigest.isEqual(hashedInputBackupCode, savedCode);
|
||||
} catch (IOException ioe) {
|
||||
logger.warnf("Error when decoding saved recovery code", ioe);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<String> generateRawCodes() {
|
||||
|
||||
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2025 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package org.keycloak.models.credential;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.utils.RecoveryAuthnCodesUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class RecoveryCodesUnitTest {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(RecoveryCodesUnitTest.class);
|
||||
|
||||
@Test
|
||||
public void testBasicVerification() {
|
||||
Assert.assertTrue(RecoveryAuthnCodesUtils.verifyRecoveryCodeInput("L9RRAUWYKARB", "OnOEi8vsNqnI2s6t2IxU2+A+KrzWAVpR9AHExeQgDzTuoiU1qPzFsOpFIy8wb6EtPEGHKj0ehgHyTbuBTyChhg=="));
|
||||
Assert.assertFalse(RecoveryAuthnCodesUtils.verifyRecoveryCodeInput("L9RRAUWYKARC", "OnOEi8vsNqnI2s6t2IxU2+A+KrzWAVpR9AHExeQgDzTuoiU1qPzFsOpFIy8wb6EtPEGHKj0ehgHyTbuBTyChhg=="));
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
public void testPerf() {
|
||||
testPerf("Successful code verifications", () ->
|
||||
Assert.assertTrue(RecoveryAuthnCodesUtils.verifyRecoveryCodeInput("L9RRAUWYKARB", "OnOEi8vsNqnI2s6t2IxU2+A+KrzWAVpR9AHExeQgDzTuoiU1qPzFsOpFIy8wb6EtPEGHKj0ehgHyTbuBTyChhg=="))
|
||||
);
|
||||
testPerf("Failed code verifications 1", () ->
|
||||
Assert.assertFalse(RecoveryAuthnCodesUtils.verifyRecoveryCodeInput("L9RRAUWYKARC", "OnOEi8vsNqnI2s6t2IxU2+A+KrzWAVpR9AHExeQgDzTuoiU1qPzFsOpFIy8wb6EtPEGHKj0ehgHyTbuBTyChhg=="))
|
||||
);
|
||||
testPerf("Failed code verifications 2", () ->
|
||||
Assert.assertFalse(RecoveryAuthnCodesUtils.verifyRecoveryCodeInput("A8CWGYUIUILP", "OnOEi8vsNqnI2s6t2IxU2+A+KrzWAVpR9AHExeQgDzTuoiU1qPzFsOpFIy8wb6EtPEGHKj0ehgHyTbuBTyChhg=="))
|
||||
);
|
||||
}
|
||||
|
||||
private void testPerf(String prefix, Runnable task) {
|
||||
long start = Time.currentTimeMillis();
|
||||
int count = 10000000;
|
||||
for (int i = 0 ; i < count ; i++) {
|
||||
task.run();
|
||||
}
|
||||
logger.infof("Task '%s' took %d ms", prefix, Time.currentTimeMillis() - start);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.authentication.AuthenticationFlowError;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.authenticators.util.AuthenticatorUtils;
|
||||
import org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory;
|
||||
import org.keycloak.common.util.ObjectUtil;
|
||||
import org.keycloak.credential.CredentialModel;
|
||||
import org.keycloak.events.Details;
|
||||
@ -152,7 +153,9 @@ public class RecoveryAuthnCodesFormAuthenticator implements Authenticator {
|
||||
@Override
|
||||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
|
||||
authenticationSession.addRequiredAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
|
||||
if (!authenticationSession.getRequiredActions().contains(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name())) {
|
||||
authenticationSession.addRequiredAction(UserModel.RequiredAction.CONFIGURE_RECOVERY_AUTHN_CODES.name());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user