Skip to content
Merged
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
4 changes: 2 additions & 2 deletions dev-tools/kind/config/structures-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ image:
# Repository matches bootBuildImage output
repository: mindsignited/structures-server
# Tag from gradle.properties version (structuresVersion=3.5.3-SNAPSHOT)
tag: 3.5.4
tag: 3.5.5
# Never pull - use images loaded into KinD cluster
pullPolicy: Never
sha: ""
Expand All @@ -46,7 +46,7 @@ migration:
activeDeadlineSeconds: 300
image:
repository: mindsignited/structures-migration
tag: 3.5.4
tag: 3.5.5
# Never pull - use images loaded into KinD cluster
pullPolicy: Never
sha: ""
Expand Down
4 changes: 2 additions & 2 deletions docker-compose/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
structures-migration:
container_name: structures-migration
pull_policy: always
image: mindsignited/structures-migration:${structuresVersion:-3.5.4}
image: mindsignited/structures-migration:${structuresVersion:-3.5.5}
depends_on:
structures-elasticsearch:
condition: service_healthy
Expand All @@ -17,7 +17,7 @@ services:
structures-server:
container_name: structures-server
pull_policy: always
image: mindsignited/structures-server:${structuresVersion:-3.5.4}
image: mindsignited/structures-server:${structuresVersion:-3.5.5}
depends_on:
structures-elasticsearch:
condition: service_healthy
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
structuresVersion=3.5.4
structuresVersion=3.5.5

allureVersion=2.32.0
antlrVersion=4.13.1
Expand Down
4 changes: 2 additions & 2 deletions helm/structures/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ nameOverride: ""
image:
repository: mindsignited/structures-server
pullPolicy: Always
tag: "3.5.4"
tag: "3.5.5"
sha: ""

# Migration configuration
Expand All @@ -15,7 +15,7 @@ migration:
activeDeadlineSeconds: 300
image:
repository: mindsignited/structures-migration
tag: "3.5.4"
tag: "3.5.5"
pullPolicy: Always
sha: ""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public class OidcProvider {
*/
private List<String> domains;

/**
* If true, this provider matches on issuer alone and skips the email-domain check.
* Required for M2M tokens (no email claim, sub is a client id) and useful when the
* IDP (e.g. Okta) issues tokens for users from arbitrary email domains.
* A provider whose {@link #domains} explicitly matches a token's email domain is
* preferred over an allowAnyDomain provider with the same issuer.
*/
private boolean allowAnyDomain;

/**
* The audience this service will expect for this OIDC provider.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;


@Slf4j
Expand Down Expand Up @@ -90,28 +89,25 @@ private CompletableFuture<Participant> validateTokenWithKey(String token, Jwk<?
.parseSignedClaims(token)
.getPayload();

// Validate issuer - this must be hardcoded as email is a standard claim.
// there are standard OIDC claims for email, preferred_username, and sub. We test for each
// of those in that order. Then try a few Microsoft specific claims.
// Look for an email-formatted value across the standard OIDC claims and a couple of
// Microsoft-specific ones. May legitimately be null for M2M tokens — in that case the
// matched provider must have allowAnyDomain=true.
String[] emailClaims = new String[] { "email", "preferred_username", "sub", "upn", "unique_name" };
String email = null;
for(String emailClaim : emailClaims) {
email = claims.get(emailClaim, String.class);
if(email != null && email.contains("@")) {
String value = claims.get(emailClaim, String.class);
if(value != null && value.contains("@")) {
email = value;
break;
}
}
if(email == null) {
log.trace("Token has no email found in claims");
return CompletableFuture.failedFuture(new RuntimeException("No email found in claims"));
}

String issuer = claims.getIssuer();
log.trace("Token has issuer: {}", issuer);
OidcProvider oidcProvider = isValidIssuer(issuer, email);
OidcProvider oidcProvider = findMatchingProvider(issuer, email);
if (oidcProvider == null) {
log.trace("Token has invalid issuer: {}", issuer);
return CompletableFuture.failedFuture(new RuntimeException("Invalid issuer: " + issuer));
log.trace("No matching OIDC provider for issuer: {} email: {}", issuer, email);
return CompletableFuture.failedFuture(new RuntimeException("No matching OIDC provider for issuer: " + issuer));
}

// Validate audience
Expand Down Expand Up @@ -159,27 +155,56 @@ private CompletableFuture<Participant> validateTokenWithKey(String token, Jwk<?
}
}

private OidcProvider isValidIssuer(String issuer, String email) {
/**
* Finds an enabled OIDC provider that matches the token's issuer.
* Match rules:
* 1. Issuer must equal the provider's authority.
* 2. If the token has an email, a provider whose configured domains contain the email's
* domain wins. Otherwise we fall back to a provider with allowAnyDomain=true.
* 3. If the token has no email, only a provider with allowAnyDomain=true matches.
*/
private OidcProvider findMatchingProvider(String issuer, String email) {
if (issuer == null) {
return null;
}

OidcProvider oidcProvider = properties.getOidcProviders().stream()
.filter(provider -> issuer.equals(provider.getAuthority()) && provider.getDomains().contains(email.split("@")[1]))
.findFirst()
.orElse(null);
List<OidcProvider> candidates = properties.getOidcProviders().stream()
.filter(p -> issuer.equals(p.getAuthority()))
.filter(OidcProvider::isEnabled)
.toList();

if (oidcProvider == null) {
log.warn("No allowed Oidc Providers configured for issuer: {}", issuer);
if (candidates.isEmpty()) {
log.warn("No enabled Oidc Providers configured for issuer: {}", issuer);
return null;
}

if(!oidcProvider.isEnabled()) {
log.warn("Oidc Provider found but is not enabled: {}", issuer);
return null;
String emailDomain = email != null && email.contains("@") ? email.split("@")[1] : null;

if (emailDomain != null) {
OidcProvider domainMatch = candidates.stream()
.filter(p -> p.getDomains() != null && p.getDomains().contains(emailDomain))
.findFirst()
.orElse(null);
if (domainMatch != null) {
return domainMatch;
}
}

return oidcProvider;
OidcProvider anyDomainMatch = candidates.stream()
.filter(OidcProvider::isAllowAnyDomain)
.findFirst()
.orElse(null);
if (anyDomainMatch != null) {
return anyDomainMatch;
}

if (emailDomain != null) {
log.warn("Issuer {} matched but no provider allows domain {} and no allowAnyDomain provider configured",
issuer, emailDomain);
} else {
log.warn("Issuer {} matched but token has no email and no allowAnyDomain provider configured", issuer);
}
return null;
}

private boolean isValidAudience(OidcProvider oidcProvider, Set<String> audiences) {
Expand Down Expand Up @@ -224,15 +249,14 @@ private Participant createParticipantFromClaims(OidcProvider oidcProvider, Claim
// and can add other users as admins/users. If that is how we manage that, how do we handle the enterprise
// cases, automatically configure those users as we validate the tokens?

// Create metadata
HashMap<String, String> metadata = new HashMap<>(Map.of(
ParticipantConstants.PARTICIPANT_TYPE_METADATA_KEY,
ParticipantConstants.PARTICIPANT_TYPE_USER,
"email", email != null ? email : "",
"name", name != null ? name : (preferredUsername != null ? preferredUsername : subject),
"iss", claims.getIssuer(),
"aud", claims.getAudience().stream().collect(Collectors.joining(", "))
));
HashMap<String, String> metadata = new HashMap<>();
metadata.put(ParticipantConstants.PARTICIPANT_TYPE_METADATA_KEY, ParticipantConstants.PARTICIPANT_TYPE_USER);
metadata.put("name", name != null ? name : (preferredUsername != null ? preferredUsername : subject));
metadata.put("iss", claims.getIssuer());
metadata.put("aud", String.join(", ", claims.getAudience()));
if (email != null) {
metadata.put("email", email);
}

if(oidcProvider.getMetadata() != null && !oidcProvider.getMetadata().isEmpty()) {
metadata.putAll(oidcProvider.getMetadata());
Expand Down
Loading