Skip to content

Commit a4789c4

Browse files
authored
Merge pull request #62 from devondragon/issue-61-feat-sample-registration-guard
feat: Add sample RegistrationGuard for domain-restricted registration
2 parents 6312089 + 4159460 commit a4789c4

4 files changed

Lines changed: 166 additions & 2 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ repositories {
3939

4040
dependencies {
4141
// DigitalSanctuary Spring User Framework
42-
implementation 'com.digitalsanctuary:ds-spring-user-framework:4.2.1'
42+
implementation 'com.digitalsanctuary:ds-spring-user-framework:4.3.0'
4343

4444
// WebAuthn support (Passkey authentication)
4545
implementation 'org.springframework.security:spring-security-webauthn'
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.digitalsanctuary.spring.demo.registration;
2+
3+
import java.util.Locale;
4+
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Profile;
7+
import org.springframework.stereotype.Component;
8+
9+
import com.digitalsanctuary.spring.user.registration.RegistrationContext;
10+
import com.digitalsanctuary.spring.user.registration.RegistrationDecision;
11+
import com.digitalsanctuary.spring.user.registration.RegistrationGuard;
12+
import com.digitalsanctuary.spring.user.registration.RegistrationSource;
13+
14+
import lombok.extern.slf4j.Slf4j;
15+
16+
/**
17+
* Sample {@link RegistrationGuard} that restricts form and passwordless registration to a
18+
* configurable email domain while allowing all OAuth2/OIDC registrations.
19+
*
20+
* <p>This guard is only active when the {@code registration-guard} Spring profile is enabled.
21+
* To try it out, add {@code registration-guard} to your active profiles:</p>
22+
*
23+
* <pre>
24+
* ./gradlew bootRun --args='--spring.profiles.active=local,registration-guard'
25+
* </pre>
26+
*
27+
* <p>The allowed domain can be configured via the {@code registration.guard.allowed-domain}
28+
* property (defaults to {@code @example.com}).</p>
29+
*
30+
* <p>See the
31+
* <a href="https://github.com/devondragon/SpringUserFramework/blob/main/REGISTRATION-GUARD.md">
32+
* Registration Guard documentation</a> for the full SPI reference.</p>
33+
*
34+
* @see RegistrationGuard
35+
* @see RegistrationContext
36+
* @see RegistrationDecision
37+
*/
38+
@Slf4j
39+
@Component
40+
@Profile("registration-guard")
41+
public class DomainRegistrationGuard implements RegistrationGuard {
42+
43+
private final String allowedDomain;
44+
45+
public DomainRegistrationGuard(
46+
@Value("${registration.guard.allowed-domain:@example.com}") String allowedDomain) {
47+
this.allowedDomain = allowedDomain.toLowerCase(Locale.ROOT);
48+
}
49+
50+
@Override
51+
public RegistrationDecision evaluate(RegistrationContext context) {
52+
log.debug("Evaluating registration for email: {}, source: {}, provider: {}",
53+
context.email(), context.source(), context.providerName());
54+
55+
// Allow all OAuth2/OIDC registrations regardless of email domain
56+
if (context.source() == RegistrationSource.OAUTH2
57+
|| context.source() == RegistrationSource.OIDC) {
58+
log.debug("Allowing {} registration for: {}", context.source(), context.email());
59+
return RegistrationDecision.allow();
60+
}
61+
62+
// For form/passwordless, restrict to the allowed domain
63+
if (context.email() != null && context.email().toLowerCase(Locale.ROOT).endsWith(allowedDomain)) {
64+
log.debug("Allowing registration for approved domain: {}", context.email());
65+
return RegistrationDecision.allow();
66+
}
67+
68+
log.info("Denied registration for: {} (domain not allowed)", context.email());
69+
return RegistrationDecision.deny(
70+
"Registration is restricted to " + allowedDomain + " email addresses.");
71+
}
72+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.digitalsanctuary.spring.demo.registration;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
import com.digitalsanctuary.spring.user.registration.RegistrationContext;
10+
import com.digitalsanctuary.spring.user.registration.RegistrationDecision;
11+
import com.digitalsanctuary.spring.user.registration.RegistrationSource;
12+
13+
class DomainRegistrationGuardTest {
14+
15+
private final DomainRegistrationGuard guard = new DomainRegistrationGuard("@example.com");
16+
17+
@Test
18+
void formRegistrationWithAllowedDomainIsAllowed() {
19+
RegistrationContext context = new RegistrationContext("user@example.com", RegistrationSource.FORM, null);
20+
RegistrationDecision decision = guard.evaluate(context);
21+
assertTrue(decision.allowed());
22+
}
23+
24+
@Test
25+
void formRegistrationWithDisallowedDomainIsDenied() {
26+
RegistrationContext context = new RegistrationContext("user@other.com", RegistrationSource.FORM, null);
27+
RegistrationDecision decision = guard.evaluate(context);
28+
assertFalse(decision.allowed());
29+
assertTrue(decision.reason().contains("@example.com"));
30+
}
31+
32+
@Test
33+
void passwordlessRegistrationWithAllowedDomainIsAllowed() {
34+
RegistrationContext context = new RegistrationContext("user@example.com", RegistrationSource.PASSWORDLESS, null);
35+
RegistrationDecision decision = guard.evaluate(context);
36+
assertTrue(decision.allowed());
37+
}
38+
39+
@Test
40+
void passwordlessRegistrationWithDisallowedDomainIsDenied() {
41+
RegistrationContext context = new RegistrationContext("user@other.com", RegistrationSource.PASSWORDLESS, null);
42+
RegistrationDecision decision = guard.evaluate(context);
43+
assertFalse(decision.allowed());
44+
}
45+
46+
@Test
47+
void oauth2RegistrationIsAlwaysAllowed() {
48+
RegistrationContext context = new RegistrationContext("user@other.com", RegistrationSource.OAUTH2, "google");
49+
RegistrationDecision decision = guard.evaluate(context);
50+
assertTrue(decision.allowed());
51+
}
52+
53+
@Test
54+
void oidcRegistrationIsAlwaysAllowed() {
55+
RegistrationContext context = new RegistrationContext("user@other.com", RegistrationSource.OIDC, "keycloak");
56+
RegistrationDecision decision = guard.evaluate(context);
57+
assertTrue(decision.allowed());
58+
}
59+
60+
@Test
61+
void nullEmailIsDenied() {
62+
RegistrationContext context = new RegistrationContext(null, RegistrationSource.FORM, null);
63+
RegistrationDecision decision = guard.evaluate(context);
64+
assertFalse(decision.allowed());
65+
}
66+
67+
@Test
68+
void domainCheckIsCaseInsensitive() {
69+
RegistrationContext context = new RegistrationContext("user@EXAMPLE.COM", RegistrationSource.FORM, null);
70+
RegistrationDecision decision = guard.evaluate(context);
71+
assertTrue(decision.allowed());
72+
}
73+
74+
@Test
75+
void customDomainIsRespected() {
76+
DomainRegistrationGuard customGuard = new DomainRegistrationGuard("@mycompany.org");
77+
RegistrationContext allowed = new RegistrationContext("user@mycompany.org", RegistrationSource.FORM, null);
78+
RegistrationContext denied = new RegistrationContext("user@example.com", RegistrationSource.FORM, null);
79+
assertTrue(customGuard.evaluate(allowed).allowed());
80+
assertFalse(customGuard.evaluate(denied).allowed());
81+
}
82+
83+
@Test
84+
void configuredDomainIsCaseInsensitive() {
85+
DomainRegistrationGuard upperGuard = new DomainRegistrationGuard("@EXAMPLE.COM");
86+
RegistrationContext context = new RegistrationContext("user@example.com", RegistrationSource.FORM, null);
87+
assertTrue(upperGuard.evaluate(context).allowed());
88+
}
89+
}

src/test/java/com/digitalsanctuary/spring/user/service/UserServiceTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public class UserServiceTest {
5555
@Mock
5656
private PasswordHistoryRepository passwordHistoryRepository;
5757

58+
@Mock
59+
private SessionInvalidationService sessionInvalidationService;
60+
5861
private UserService userService;
5962
private User testUser;
6063
private UserDto testUserDto;
@@ -78,7 +81,7 @@ void setUp() {
7881

7982
userService = new UserService(userRepository, tokenRepository, passwordTokenRepository, passwordEncoder,
8083
roleRepository, sessionRegistry, userEmailService, userVerificationService, authorityService,
81-
dsUserDetailsService, eventPublisher, passwordHistoryRepository);
84+
dsUserDetailsService, eventPublisher, passwordHistoryRepository, sessionInvalidationService);
8285
}
8386

8487
@Test

0 commit comments

Comments
 (0)