+
Skip to content

Integrate passkeys with separate username and password forms #40371

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ public interface AbstractAuthenticationFlowContext {
*/
void success();

/**
* Mark the current execution as successful and the auth session sets the
* credential type in the authentication session as the last credential used
* to authenticate the user.
*
* @param credentialType The credential used to authenticate the user
*/
void success(String credentialType);

/**
* Aborts the current flow
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class AuthenticationProcessor {
public static final String LAST_PROCESSED_EXECUTION = "last.processed.execution";
public static final String CURRENT_FLOW_PATH = "current.flow.path";
public static final String FORKED_FROM = "forked.from";
public static final String LAST_AUTHN_CREDENTIAL = "last.authn.credential";

public static final String BROKER_SESSION_ID = "broker.session.id";
public static final String BROKER_USER_ID = "broker.user.id";
Expand Down Expand Up @@ -406,6 +407,14 @@ public ClientAuthenticator getClientAuthenticator() {

@Override
public void success() {
success(null);
}

@Override
public void success(String credentialType) {
if (credentialType != null) {
getAuthenticationSession().setAuthNote(LAST_AUTHN_CREDENTIAL, credentialType);
}
this.status = FlowStatus.SUCCESS;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ public void validateOTP(AuthenticationFlowContext context) {
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
return;
}
context.success();
context.success(OTPCredentialModel.TYPE);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,21 @@

public class PasswordForm extends UsernamePasswordForm implements CredentialValidator<PasswordCredentialProvider> {

public PasswordForm(KeycloakSession session) {
super(session);
}

@Override
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
return validatePassword(context, context.getUser(), formData, false);
}

@Override
public void authenticate(AuthenticationFlowContext context) {
if (alreadyAuthenticatedUsingPasswordlessCredential(context)) {
context.success();
return;
}
Response challengeResponse = context.form().createLoginPassword();
context.challenge(challengeResponse);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@
public class PasswordFormFactory implements AuthenticatorFactory {

public static final String PROVIDER_ID = "auth-password-form";
public static final PasswordForm SINGLETON = new PasswordForm();

@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
return new PasswordForm(session);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void action(AuthenticationFlowContext context) {
context.getEvent().detail(Details.CREDENTIAL_TYPE, RecoveryAuthnCodesCredentialModel.TYPE)
.user(context.getUser());
if (isRecoveryAuthnCodeInputValid(context)) {
context.success();
context.success(RecoveryAuthnCodesCredentialModel.TYPE);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void authenticate(AuthenticationFlowContext context) {
context.getAuthenticationSession().setUserSessionNote(entry.getKey(), entry.getValue());
}
}
context.success();
context.success(UserCredentialModel.KERBEROS);
} else if (output.getAuthStatus() == CredentialValidationOutput.Status.CONTINUE) {
String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN);
Response challenge = challengeNegotiation(context, spnegoResponseToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@

public final class UsernameForm extends UsernamePasswordForm {

public UsernameForm() {
super();
}

public UsernameForm(KeycloakSession session) {
super(session);
}

@Override
public void authenticate(AuthenticationFlowContext context) {
if (context.getUser() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@

package org.keycloak.authentication.authenticators.browser;

import java.util.Collections;
import java.util.Set;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.models.credential.WebAuthnCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.List;
Expand All @@ -35,11 +39,10 @@
public class UsernameFormFactory implements AuthenticatorFactory {

public static final String PROVIDER_ID = "auth-username-form";
public static final UsernameForm SINGLETON = new UsernameForm();

@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
return new UsernameForm(session);
}

@Override
Expand Down Expand Up @@ -67,10 +70,18 @@ public String getReferenceCategory() {
return PasswordCredentialModel.TYPE;
}

@Override
public Set<String> getOptionalReferenceCategories() {
return Profile.isFeatureEnabled(Profile.Feature.PASSKEYS)
? Collections.singleton(WebAuthnCredentialModel.TYPE_PASSWORDLESS)
: AuthenticatorFactory.super.getOptionalReferenceCategories();
}

@Override
public boolean isConfigurable() {
return false;
}

public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@

import org.keycloak.WebAuthnConstants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.AuthenticationManager;

Expand All @@ -37,7 +39,7 @@
*/
public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator implements Authenticator {

private final WebAuthnConditionalUIAuthenticator webauthnAuth;
protected final WebAuthnConditionalUIAuthenticator webauthnAuth;

public UsernamePasswordForm() {
webauthnAuth = null;
Expand All @@ -62,13 +64,19 @@ public void action(AuthenticationFlowContext context) {
// normal username and form authenticator
return;
}
context.success();
context.success(PasswordCredentialModel.TYPE);
}

protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
return validateUserAndPassword(context, formData);
}

protected boolean alreadyAuthenticatedUsingPasswordlessCredential(AuthenticationFlowContext context) {
// check if the authentication was already done using passwordless via passkeys
return webauthnAuth != null && webauthnAuth.isPasskeysEnabled() && webauthnAuth.getCredentialType().equals(
context.getAuthenticationSession().getAuthNote(AuthenticationProcessor.LAST_AUTHN_CREDENTIAL));
}

@Override
public void authenticate(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = new MultivaluedHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ public void action(AuthenticationFlowContext context) {
context.getEvent()
.detail(WebAuthnConstants.USER_VERIFICATION_CHECKED, isUVChecked)
.detail(WebAuthnConstants.PUBKEY_CRED_ID_ATTR, encodedCredentialID);
context.success();
context.success(getCredentialType());
} else {
context.getEvent()
.detail(WebAuthnConstants.AUTHENTICATED_USER_ID, userId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.ServicesLogger;
Expand Down Expand Up @@ -179,7 +180,7 @@ public void authenticate(AuthenticationFlowContext context) {
}
else {
// Bypass the confirmation page and log the user in
context.success();
context.success(UserCredentialModel.CLIENT_CERT);
}
}
catch(Exception e) {
Expand Down Expand Up @@ -269,7 +270,7 @@ public void action(AuthenticationFlowContext context) {
}
if (context.getUser() != null) {
recordX509CertificateAuditDataViaContextEvent(context);
context.success();
context.success(UserCredentialModel.CLIENT_CERT);
return;
}
context.attempted();
Expand Down
Loading
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载