|
15 | 15 | import org.keycloak.email.EmailTemplateProvider; |
16 | 16 | import org.keycloak.events.Errors; |
17 | 17 | import org.keycloak.forms.login.LoginFormsProvider; |
| 18 | +import org.keycloak.models.AuthenticationExecutionModel; |
| 19 | +import org.keycloak.models.AuthenticationFlowModel; |
18 | 20 | import org.keycloak.models.AuthenticatorConfigModel; |
19 | 21 | import org.keycloak.models.KeycloakSession; |
20 | 22 | import org.keycloak.models.RealmModel; |
@@ -123,9 +125,15 @@ public void action(AuthenticationFlowContext context) { |
123 | 125 |
|
124 | 126 | @Override |
125 | 127 | public void authenticate(AuthenticationFlowContext context) { |
126 | | - // Check role condition - skip OTP if user doesn't match criteria |
| 128 | + // Check role condition - if user doesn't match criteria, skip this authenticator |
127 | 129 | if (!this.shouldRequireOtp(context)) { |
128 | | - context.success(); |
| 130 | + // In REQUIRED mode: use success() to skip (allows role-based filtering) |
| 131 | + // In ALTERNATIVE mode: use attempted() to prevent 2FA bypass |
| 132 | + if (context.getExecution().isRequired()) { |
| 133 | + context.success(); |
| 134 | + } else { |
| 135 | + context.attempted(); |
| 136 | + } |
129 | 137 | return; |
130 | 138 | } |
131 | 139 |
|
@@ -192,7 +200,78 @@ public boolean configuredFor(AuthenticationFlowContext context, AuthenticatorCon |
192 | 200 |
|
193 | 201 | @Override |
194 | 202 | public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { |
195 | | - return null != user.getEmail() && !user.getEmail().isEmpty(); |
| 203 | + if (user == null || user.getEmail() == null || user.getEmail().isEmpty()) { |
| 204 | + logger.debugf("configuredFor: user is null or has no email"); |
| 205 | + return false; |
| 206 | + } |
| 207 | + |
| 208 | + // Check all authentication flows for email-otp-form executions (including nested subflows) |
| 209 | + // Return true only if user matches at least one execution's role requirement |
| 210 | + for (AuthenticationFlowModel flow : realm.getAuthenticationFlowsStream().toList()) { |
| 211 | + logger.debugf("configuredFor: checking flow %s", flow.getAlias()); |
| 212 | + if (isUserConfiguredForEmailOtpInFlow(realm, user, flow.getId())) { |
| 213 | + logger.debugf("configuredFor: user %s is configured for email-otp in flow %s", user.getUsername(), flow.getAlias()); |
| 214 | + return true; |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + // User doesn't match any email-otp-form execution's requirements |
| 219 | + logger.debugf("configuredFor: user %s is NOT configured for any email-otp execution", user.getUsername()); |
| 220 | + return false; |
| 221 | + } |
| 222 | + |
| 223 | + private boolean isUserConfiguredForEmailOtpInFlow(RealmModel realm, UserModel user, String flowId) { |
| 224 | + for (AuthenticationExecutionModel execution : realm.getAuthenticationExecutionsStream(flowId).toList()) { |
| 225 | + // If this is a subflow, recursively check it |
| 226 | + if (execution.isAuthenticatorFlow()) { |
| 227 | + String subflowId = execution.getFlowId(); |
| 228 | + if (subflowId != null && isUserConfiguredForEmailOtpInFlow(realm, user, subflowId)) { |
| 229 | + return true; |
| 230 | + } |
| 231 | + continue; |
| 232 | + } |
| 233 | + |
| 234 | + // Check if this is an email-otp-form execution |
| 235 | + if (!EmailOTPFormAuthenticatorFactory.PROVIDER_ID.equals(execution.getAuthenticator())) { |
| 236 | + continue; |
| 237 | + } |
| 238 | + |
| 239 | + // Get the config for this execution |
| 240 | + String configId = execution.getAuthenticatorConfig(); |
| 241 | + if (configId == null) { |
| 242 | + // No config means no role restriction - user is eligible |
| 243 | + return true; |
| 244 | + } |
| 245 | + |
| 246 | + AuthenticatorConfigModel config = realm.getAuthenticatorConfigById(configId); |
| 247 | + if (config == null) { |
| 248 | + // Config not found, treat as no restriction |
| 249 | + return true; |
| 250 | + } |
| 251 | + |
| 252 | + // Check role requirement |
| 253 | + String configuredRole = ConfigHelper.getRole(config); |
| 254 | + if (configuredRole == null || configuredRole.isEmpty()) { |
| 255 | + // No role configured - user is eligible |
| 256 | + return true; |
| 257 | + } |
| 258 | + |
| 259 | + RoleModel role = realm.getRole(configuredRole); |
| 260 | + if (role == null) { |
| 261 | + // Role doesn't exist - treat as no restriction |
| 262 | + return true; |
| 263 | + } |
| 264 | + |
| 265 | + // Check if user matches the role requirement (considering negation) |
| 266 | + boolean hasRole = user.hasRole(role); |
| 267 | + boolean negateRole = ConfigHelper.getNegateRole(config); |
| 268 | + if (hasRole != negateRole) { |
| 269 | + // User matches this execution's requirement |
| 270 | + return true; |
| 271 | + } |
| 272 | + } |
| 273 | + |
| 274 | + return false; |
196 | 275 | } |
197 | 276 |
|
198 | 277 | @Override |
|
0 commit comments