Skip to content
Open
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 @@ -65,7 +65,7 @@ private BedrockIdentityVerifier(Builder builder) {
this.now = builder.now;
this.endpointId = optionalNonEmpty(builder.endpointId, "endpointId");
this.endpointName = requireNonEmpty(builder.endpointName, "endpointName");
this.orgId = optionalNonEmpty(builder.orgId, "orgId");
this.orgId = optionalNullOrEmpty(builder.orgId);
this.sessionId = requireNonEmpty(builder.sessionId, "sessionId");
this.protocol = requireNonEmpty(builder.protocol, "protocol");
this.bedrockAuthPolicy = builder.bedrockAuthPolicy;
Expand Down Expand Up @@ -277,8 +277,7 @@ private static void validate(Envelope envelope) throws BedrockIdentityVerificati
}
if (isEmpty(envelope.issuer) || envelope.endpoint == null ||
isEmpty(envelope.endpoint.id) ||
isEmpty(envelope.endpoint.name) ||
isEmpty(envelope.endpoint.org_id)) {
isEmpty(envelope.endpoint.name)) {
throw new BedrockIdentityVerificationException("identity envelope endpoint scope is incomplete");
}
if (envelope.session == null ||
Expand Down Expand Up @@ -402,6 +401,10 @@ private static String optionalNonEmpty(String value, String name) {
return requireNonEmpty(value, name);
}

private static String optionalNullOrEmpty(String value) {
return value == null || value.isEmpty() ? null : value;
}

private static boolean isEmpty(String value) {
return value == null || value.isEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,10 @@ Decision verify(
return Decision.allowed(null);
}

if (hasEnvelope && (!hasScopeValue(endpointId) || !hasScopeValue(endpointOrgId))) {
// The endpoint itself is the identity binding; the org dimension is
// optional (an org-less endpoint is a first-class endpoint-scoped
// scope). Only a missing endpoint id makes the scope incomplete.
if (hasEnvelope && !hasScopeValue(endpointId)) {
return rejectOrWarn(player, mode, "authenticated endpoint scope is incomplete");
}
if (!identityConfiguration.isUsable()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@ void verifiesEndpointScopedBedrockXuidEnvelopeFromGameProfileProperty() throws E
assertEquals("endpoint-id", claims.getEndpointId());
}

@Test
void verifiesEndpointScopedBedrockXuidEnvelopeWithoutOrg() throws Exception {
KeyPair keyPair = ed25519KeyPair();
String envelope = signedEnvelope(keyPair, VALID_NONCE, "session-1", "endpoint-id", "endpoint", "");
GameProfile profile = profileWithEnvelope(envelope);

BedrockIdentityVerifier verifier = BedrockIdentityVerifier.builder()
.publicKey(rawEd25519PublicKey(keyPair))
.now(NOW)
.endpointId("endpoint-id")
.endpointName("endpoint")
.orgId("")
.sessionId("session-1")
.protocol("bedrock")
.replayCache(new BedrockIdentityReplayCache())
.build();

BedrockIdentityClaims claims = verifier.verify(profile);

assertEquals("bedrock_xuid", claims.getPrincipalType());
assertEquals("endpoint-id", claims.getEndpointId());
assertEquals("", claims.getOrgId());
}

@Test
void rejectsSignedEnvelopeWithQuotedVersion() throws Exception {
KeyPair keyPair = ed25519KeyPair();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,27 @@ void requireModeRejectsReplay() throws Exception {
assertTrue(replay.message().contains("Bedrock identity verification failed"));
}

@Test
void requireModeAllowsOrglessEndpointScope() throws Exception {
KeyPair keyPair = ed25519KeyPair();
ConnectConfig config = config(
"require",
base64(keyPair.getPublic().getEncoded()),
"trusted_bedrock_xuid");
String envelope = signedEnvelope(keyPair, VALID_NONCE, "session-1", config.getEndpoint(), "minekube-connect-test", "");
BedrockIdentityEnforcer enforcer = new BedrockIdentityEnforcer(config, mock(ConnectLogger.class), () -> NOW);
ConnectPlayer player = player("session-1", profileWithEnvelope(envelope));

// An org-less endpoint is a first-class endpoint-scoped identity scope:
// the default trusted_bedrock_xuid policy admits the player even under
// strict require-mode enforcement (support case: gurtville).
BedrockIdentityEnforcer.Decision decision = enforcer.verify(player, "endpoint-id", "");

assertTrue(decision.allowed());
assertNotNull(decision.verifiedClaims());
assertEquals("", decision.verifiedClaims().getOrgId());
}

private static ConnectPlayer player(String sessionId, GameProfile profile) {
return new ConnectPlayerImpl(sessionId, profile, new Auth(false), "");
}
Expand Down Expand Up @@ -701,13 +722,34 @@ private static String signedEnvelope(
String endpointName,
String issuer,
Instant issuedAt) throws Exception {
return signedEnvelope(keyPair, nonce, sessionId, endpointName, issuer, issuedAt, "org-id");
}

private static String signedEnvelope(
KeyPair keyPair,
String nonce,
String sessionId,
String endpointName,
String issuer,
String orgId) throws Exception {
return signedEnvelope(keyPair, nonce, sessionId, endpointName, issuer, NOW, orgId);
}

private static String signedEnvelope(
KeyPair keyPair,
String nonce,
String sessionId,
String endpointName,
String issuer,
Instant issuedAt,
String orgId) throws Exception {
Envelope envelope = new Envelope();
envelope.version = 1;
envelope.issuer = issuer;
envelope.endpoint = new Endpoint();
envelope.endpoint.id = "endpoint-id";
envelope.endpoint.name = endpointName;
envelope.endpoint.org_id = "org-id";
envelope.endpoint.org_id = orgId;
envelope.session = new Session();
envelope.session.id = sessionId;
envelope.session.protocol = "bedrock";
Expand Down
Loading