-
Notifications
You must be signed in to change notification settings - Fork 43
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Summary
Add a RegistrationGuard Service Provider Interface (SPI) that allows consuming applications to control who is allowed to register. The guard is called before a new user account is created at all registration paths (form, passwordless, OAuth2, OIDC).
Motivation
Consumer applications need the ability to restrict registration (invite-only mode, beta access, domain restrictions, etc.). Currently, the library has no pre-registration hook:
- Form registration (
UserAPI.registerUserAccount()) callsUserService.registerNewUserAccount()directly - OAuth2/OIDC auto-registration (
DSOAuth2UserService/DSOidcUserService) callregisterNewOAuthUser()/registerNewOidcUser()insideloadUser()— consumer apps cannot intercept this path without fragile bean overrides
A library-level SPI is the only clean way to gate registration across all paths.
Design
New Types (package: com.digitalsanctuary.spring.user.registration)
RegistrationSource enum
public enum RegistrationSource {
FORM, PASSWORDLESS, OAUTH2, OIDC
}RegistrationContext record
public record RegistrationContext(
String email,
RegistrationSource source,
String providerName // null for FORM/PASSWORDLESS, "google"/"facebook" for OAuth
) {}RegistrationDecision record
public record RegistrationDecision(boolean allowed, String reason) {
public static RegistrationDecision allow() { ... }
public static RegistrationDecision deny(String reason) { ... }
}RegistrationGuard interface
public interface RegistrationGuard {
RegistrationDecision evaluate(RegistrationContext context);
}DefaultRegistrationGuard (backward-compatible default)
@Component
@ConditionalOnMissingBean(RegistrationGuard.class)
public class DefaultRegistrationGuard implements RegistrationGuard {
@Override
public RegistrationDecision evaluate(RegistrationContext context) {
return RegistrationDecision.allow();
}
}Integration Points (4 locations)
| Path | File | Where | On Denial |
|---|---|---|---|
| Form | UserAPI.java ~L107 |
After password validation, before registerNewUserAccount() |
Return 403 JSONResponse |
| Passwordless | UserAPI.java ~L398 |
After WebAuthn check, before registerPasswordlessAccount() |
Return 403 JSONResponse |
| OAuth2 | DSOAuth2UserService.java ~L103 |
In handleOAuthLoginSuccess() else branch, before registerNewOAuthUser() |
Throw OAuth2AuthenticationException |
| OIDC | DSOidcUserService.java ~L91 |
In handleOidcLoginSuccess() else branch, before registerNewOidcUser() |
Throw OAuth2AuthenticationException |
Key Details
- Guard is only called for new user registration, NOT existing user logins (the OAuth/OIDC services already branch on
existingUser != null) - OAuth error handling works automatically: existing
WebSecurityConfigfailure handler catchesOAuth2AuthenticationException, stores message in session, redirects to login page — no security config changes needed - Backward compatible:
@ConditionalOnMissingBeanonDefaultRegistrationGuardmeans existing apps see zero behavioral change - Thread-safe: Guard implementations must be thread-safe (document in Javadoc)
- No configuration properties needed: feature activates purely by bean presence
Files to Modify
UserAPI.java— addRegistrationGuardconstructor param, add guard checksDSOAuth2UserService.java— addRegistrationGuardconstructor param, add guard checkDSOidcUserService.java— addRegistrationGuardconstructor param, add guard check
Files to Create
src/main/java/.../registration/RegistrationSource.javasrc/main/java/.../registration/RegistrationContext.javasrc/main/java/.../registration/RegistrationDecision.javasrc/main/java/.../registration/RegistrationGuard.javasrc/main/java/.../registration/DefaultRegistrationGuard.java
Testing
- Unit:
RegistrationDecisionfactory methods,DefaultRegistrationGuardalways allows - Integration: Mock guard → deny → verify 403 for form paths,
OAuth2AuthenticationExceptionfor OAuth/OIDC paths - Integration: Verify existing users can still log in via OAuth when guard denies new registrations
- Backward compat: No custom guard bean → all registrations succeed as before
Consumer App Example
@Component
public class InviteOnlyGuard implements RegistrationGuard {
private final InviteService inviteService;
private final WhitelistService whitelistService;
@Override
public RegistrationDecision evaluate(RegistrationContext context) {
if (whitelistService.isWhitelisted(context.email())) {
return RegistrationDecision.allow();
}
// Read invite code from HTTP session (stored when user visited registration page)
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String code = (String) request.getSession().getAttribute("inviteCode");
if (inviteService.isValidCode(code, context.email())) {
return RegistrationDecision.allow();
}
return RegistrationDecision.deny("Registration is invite-only.");
}
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request