This guide explains how to use the Registration Guard SPI in Spring User Framework to control who can register in your application.
The Registration Guard is a pre-registration hook that gates all four registration paths: form, passwordless, OAuth2, and OIDC. It allows you to accept or reject registration attempts before a user account is created.
The guard requires zero configuration — it activates by bean presence alone. When no custom guard is defined, a built-in permit-all default is used automatically.
Consider implementing a Registration Guard when you need to:
- Restrict registration to specific email domains (e.g., corporate apps)
- Implement invite-only or beta access registration
- Enforce waitlist-based onboarding
- Apply compliance or legal gates before account creation
- Allow social login but restrict form-based registration (or vice versa)
If your application allows open registration with no restrictions, you do not need to implement a guard.
The Registration Guard SPI consists of these types in the com.digitalsanctuary.spring.user.registration package:
-
RegistrationGuard— The interface you implement. Has a single method:evaluate(RegistrationContext)returning aRegistrationDecision. -
RegistrationContext— An immutable record describing the registration attempt:email— the email address of the user attempting to registersource— the registration path (FORM,PASSWORDLESS,OAUTH2, orOIDC)providerName— the OAuth2/OIDC provider registration ID (e.g."google","keycloak"), ornullfor form/passwordless
-
RegistrationDecision— An immutable record with the guard's verdict:allowed— whether the registration is permittedreason— a human-readable denial reason (may benullwhen allowed)allow()— static factory for an allowing decisiondeny(String reason)— static factory for a denying decision
-
RegistrationSource— Enum identifying the registration path:FORM,PASSWORDLESS,OAUTH2,OIDC -
DefaultRegistrationGuard— The built-in permit-all fallback. Automatically registered via@ConditionalOnMissingBeanwhen no custom guard bean exists.
Create a @Component that implements RegistrationGuard. That's it — the default guard is automatically replaced.
@Component
public class MyRegistrationGuard implements RegistrationGuard {
@Override
public RegistrationDecision evaluate(RegistrationContext context) {
// Your logic here
return RegistrationDecision.allow();
}
}No additional configuration, properties, or wiring is needed. The library detects your bean and uses it in place of the default.
Allow only users with a specific email domain:
@Component
public class DomainGuard implements RegistrationGuard {
@Override
public RegistrationDecision evaluate(RegistrationContext context) {
if (context.email().endsWith("@mycompany.com")) {
return RegistrationDecision.allow();
}
return RegistrationDecision.deny("Registration is restricted to @mycompany.com email addresses.");
}
}Require an invite for form/passwordless registration but allow all OAuth2/OIDC users:
@Component
@RequiredArgsConstructor
public class InviteOnlyGuard implements RegistrationGuard {
private final InviteCodeRepository inviteCodeRepository;
@Override
public RegistrationDecision evaluate(RegistrationContext context) {
// Allow all OAuth2/OIDC registrations
if (context.source() == RegistrationSource.OAUTH2
|| context.source() == RegistrationSource.OIDC) {
return RegistrationDecision.allow();
}
// For form/passwordless, check invite list
if (inviteCodeRepository.existsByEmail(context.email())) {
return RegistrationDecision.allow();
}
return RegistrationDecision.deny("Registration is by invitation only.");
}
}Check a beta-users table before allowing registration:
@Component
@RequiredArgsConstructor
public class BetaAccessGuard implements RegistrationGuard {
private final BetaUserRepository betaUserRepository;
@Override
public RegistrationDecision evaluate(RegistrationContext context) {
if (betaUserRepository.existsByEmail(context.email())) {
return RegistrationDecision.allow();
}
return RegistrationDecision.deny("Registration is currently limited to beta users. "
+ "Please join the waitlist.");
}
}When a guard denies a registration, the behavior depends on the registration path:
| Registration Path | Denial Response |
|---|---|
| Form | HTTP 403 Forbidden with JSON: {"success": false, "code": 6, "messages": ["<reason>"]} |
| Passwordless | HTTP 403 Forbidden with JSON: {"success": false, "code": 6, "messages": ["<reason>"]} |
| OAuth2 | OAuth2AuthenticationException with error code "registration_denied" — handled by Spring Security's OAuth2 failure handler |
| OIDC | OAuth2AuthenticationException with error code "registration_denied" — handled by Spring Security's OAuth2 failure handler |
The JSON error code 6 identifies a registration guard denial specifically, distinguishing it from other registration errors (e.g., code 1 for validation failures, code 2 for duplicate accounts). Client-side code can check this code to display targeted messaging.
For OAuth2/OIDC denials, customize the user experience by configuring Spring Security's OAuth2 login failure handler to inspect the error code and display an appropriate message.
All denied registrations are logged at INFO level with the email, source, and denial reason.
- Single-bean SPI — Only one
RegistrationGuardbean may be active at a time. This is not a chain or filter pattern; define exactly one guard. - Thread safety required — The guard may be invoked concurrently from multiple request threads. Ensure your implementation is thread-safe.
- No configuration properties — The guard is activated entirely by bean presence. There are no
user.*properties involved. - Existing users unaffected — The guard only runs for new registrations. Existing users logging in via OAuth2/OIDC are not evaluated.
Guard Not Activating
- Ensure your guard class is annotated with
@Component(or otherwise registered as a Spring bean) - Verify the class is within a package that is component-scanned by your application
- At startup, the library logs
"No custom RegistrationGuard bean found — using DefaultRegistrationGuard (permit-all)"at INFO level. If you see this message, your custom guard bean is not being detected. - You can also check the active guard via
/actuator/beans(if enabled) or your IDE's Spring tooling.
Multiple Guards Defined
- Only one
RegistrationGuardbean is allowed. If multiple beans are defined, Spring will throw aNoUniqueBeanDefinitionExceptionat startup. - If you need to compose multiple rules, implement a single guard that delegates internally.
OAuth2/OIDC Denial UX
- By default, OAuth2/OIDC denials redirect to Spring Security's default failure URL with a generic error.
- To show a custom message, configure an
AuthenticationFailureHandleron your OAuth2 login that checks for the"registration_denied"error code:http.oauth2Login(oauth2 -> oauth2 .failureHandler((request, response, exception) -> { if (exception instanceof OAuth2AuthenticationException oauthEx && "registration_denied".equals(oauthEx.getError().getErrorCode())) { response.sendRedirect("/registration-denied"); } else { response.sendRedirect("/login?error"); } }) );
This SPI provides a clean extension point for controlling registration without modifying framework internals. Implement a single bean, return allow or deny, and the framework handles the rest across all registration paths.
For a complete working example, refer to the Spring User Framework Demo Application.