diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/AuthenticationStatement.java b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/AuthenticationStatement.java index 6a7ba939d..b0a49e97e 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/AuthenticationStatement.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/AuthenticationStatement.java @@ -16,5 +16,6 @@ public class AuthenticationStatement { private List authenticatingAuthorities; private String authnContextClassRef; + private String authnInstant; } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SamlAuthenticationDetails.java b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SamlAuthenticationDetails.java index 1db1f6179..dd4205b25 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SamlAuthenticationDetails.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SamlAuthenticationDetails.java @@ -33,6 +33,7 @@ public class SamlAuthenticationDetails { public static final String REMOTE_ENTITY_ID = "remoteEntityId"; public static final String ATTRIBUTES = "attributes"; public static final String AUTHN_STATEMENTS = "authnStatements"; + public static final String AUTHN_INSTANT = "authnInstant"; public static final String AUTHN_CONTEXT_CLASS_REF = "authnContextClassRef"; public static final String AUTHENTICATING_AUTHORITIES = "authenticatingAuthorities"; @@ -73,7 +74,8 @@ public class SamlAuthenticationDetails { { authnContextClassRef = as.getAuthnContext().getAuthnContextClassRef().getAuthnContextClassRef(); } - authenticationStatements.add(new AuthenticationStatement(authenticatingAuthorities, authnContextClassRef)); + String authnInstant = as.getAuthnInstant() != null ? as.getAuthnInstant().toString() : ""; + authenticationStatements.add(new AuthenticationStatement(authenticatingAuthorities, authnContextClassRef, authnInstant)); } } return authenticationStatements; @@ -127,11 +129,10 @@ public class SamlAuthenticationDetails { authorities.add(authority.getAsString()); } String authnContextClassRef = getStringOrNull(obj.get(AUTHN_CONTEXT_CLASS_REF)); - authnStatements.add(new AuthenticationStatement(authorities, authnContextClassRef)); + String authnInstant = getStringOrNull(obj.get(AUTHN_INSTANT)); + authnStatements.add(new AuthenticationStatement(authorities, authnContextClassRef, authnInstant)); } details.setAuthnStatements(authnStatements); - - return details; } @@ -155,9 +156,11 @@ public class SamlAuthenticationDetails { } object.add(ATTRIBUTES, attrs); JsonArray authnStatements = new JsonArray(); + for (AuthenticationStatement as: o.getAuthnStatements()) { JsonObject asJson = new JsonObject(); addStringOrNull(asJson, AUTHN_CONTEXT_CLASS_REF, as.getAuthnContextClassRef()); + addStringOrNull(asJson, AUTHN_INSTANT, as.getAuthnInstant()); JsonArray authorities = new JsonArray(); for (String authAuthority: as.getAuthenticatingAuthorities()) { if (authAuthority == null) { @@ -165,7 +168,8 @@ public class SamlAuthenticationDetails { } authorities.add(authAuthority); } - asJson.add(AUTHENTICATING_AUTHORITIES, asJson); + asJson.add(AUTHENTICATING_AUTHORITIES, authorities); + authnStatements.add(asJson); } object.add(AUTHN_STATEMENTS, authnStatements); return object.toString(); diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SavedUserAuthentication.java b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SavedUserAuthentication.java index 1b0d0ecde..62f9870e7 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SavedUserAuthentication.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oauth2/model/SavedUserAuthentication.java @@ -16,7 +16,6 @@ package cz.muni.ics.oauth2.model; -import cz.muni.ics.oauth2.model.convert.JsonElementStringConverter; import cz.muni.ics.oauth2.model.convert.SamlAuthenticationDetailsStringConverter; import cz.muni.ics.oauth2.model.convert.SimpleGrantedAuthorityStringConverter; import cz.muni.ics.oidc.saml.SamlPrincipal; diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthenticationContextClassRefClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthenticationContextClassRefClaimSource.java new file mode 100644 index 000000000..b066aa8af --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthenticationContextClassRefClaimSource.java @@ -0,0 +1,61 @@ +package cz.muni.ics.oidc.server.claims.sources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import cz.muni.ics.oauth2.model.AuthenticationStatement; +import cz.muni.ics.oauth2.model.SamlAuthenticationDetails; +import cz.muni.ics.oidc.server.claims.ClaimSource; +import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext; +import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * Claim source which extracts the AuthenticationContextClassRef value from a SAML AuthN statement. + * + * @author Dominik Frantisek Bucik + */ +@Slf4j +public class SamlAuthenticationContextClassRefClaimSource extends SamlAuthnStatementExtractorBaseClaimSource { + + public static final String KEY_ACR = "acr"; + + public SamlAuthenticationContextClassRefClaimSource(ClaimSourceInitContext ctx) { + super(ctx); + log.debug("{} - initialized", getClaimName()); + } + + @Override + public Set getAttrIdentifiers() { + return Collections.emptySet(); + } + + @Override + public JsonNode produceValue(ClaimSourceProduceContext pctx) { + JsonNode res = JsonNodeFactory.instance.nullNode(); + + if (!hasAuthnStatements(pctx)) { + return res; + } + + List statements = pctx.getSamlAuthenticationDetails().getAuthnStatements(); + for (AuthenticationStatement s: statements) { + if (!isValidStatement(s)) { + continue; + } + res = JsonNodeFactory.instance.textNode(s.getAuthnContextClassRef()); + break; + } + log.debug("{} - produced value '{}'", getClaimName(), res); + return res; + } + + private boolean isValidStatement(AuthenticationStatement s) { + return s != null && StringUtils.hasText(s.getAuthnContextClassRef()); + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthnInstantClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthnInstantClaimSource.java new file mode 100644 index 000000000..f14ea737d --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthnInstantClaimSource.java @@ -0,0 +1,59 @@ +package cz.muni.ics.oidc.server.claims.sources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import cz.muni.ics.oauth2.model.AuthenticationStatement; +import cz.muni.ics.oauth2.model.SamlAuthenticationDetails; +import cz.muni.ics.oidc.server.claims.ClaimSource; +import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext; +import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * Claim source which extracts the AuthN instant value from a SAML AuthN statement. + * + * @author Dominik Frantisek Bucik + */ +@Slf4j +public class SamlAuthnInstantClaimSource extends SamlAuthnStatementExtractorBaseClaimSource { + + public SamlAuthnInstantClaimSource(ClaimSourceInitContext ctx) { + super(ctx); + log.debug("{} - initialized", getClaimName()); + } + + @Override + public Set getAttrIdentifiers() { + return Collections.emptySet(); + } + + @Override + public JsonNode produceValue(ClaimSourceProduceContext pctx) { + JsonNode res = JsonNodeFactory.instance.nullNode(); + + if (!hasAuthnStatements(pctx)) { + return res; + } + + List statements = pctx.getSamlAuthenticationDetails().getAuthnStatements(); + for (AuthenticationStatement s: statements) { + if (!isValidStatement(s)) { + continue; + } + res = JsonNodeFactory.instance.textNode(s.getAuthnInstant()); + break; + } + log.debug("{} - produced value '{}'", getClaimName(), res); + return res; + } + + private boolean isValidStatement(AuthenticationStatement s) { + return s != null && StringUtils.hasText(s.getAuthnInstant()); + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthnStatementExtractorBaseClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthnStatementExtractorBaseClaimSource.java new file mode 100644 index 000000000..755e35f17 --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/SamlAuthnStatementExtractorBaseClaimSource.java @@ -0,0 +1,37 @@ +package cz.muni.ics.oidc.server.claims.sources; + +import cz.muni.ics.oauth2.model.AuthenticationStatement; +import cz.muni.ics.oauth2.model.SamlAuthenticationDetails; +import cz.muni.ics.oidc.server.claims.ClaimSource; +import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext; +import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext; +import java.util.List; +import lombok.extern.slf4j.Slf4j; + +/** + * Base class for a claim source which extracts value from a SAML AuthN statement. + * + * @author Dominik Frantisek Bucik + */ +@Slf4j +public abstract class SamlAuthnStatementExtractorBaseClaimSource extends ClaimSource { + + public SamlAuthnStatementExtractorBaseClaimSource(ClaimSourceInitContext ctx) { + super(ctx); + } + + protected boolean hasAuthnStatements(ClaimSourceProduceContext pctx) { + SamlAuthenticationDetails details = pctx.getSamlAuthenticationDetails(); + + if (details == null || details.getAttributes() == null || details.getAttributes().isEmpty()) { + return false; + } + + List statements = details.getAuthnStatements(); + if (statements == null || statements.isEmpty()) { + return false; + } + return true; + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java index 91982cec2..25dfdaa5d 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/ControllerUtils.java @@ -8,6 +8,7 @@ import com.google.common.collect.Sets; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import cz.muni.ics.oauth2.model.SystemScope; import cz.muni.ics.oauth2.service.SystemScopeService; import cz.muni.ics.oidc.server.configurations.PerunOidcConfig; @@ -26,6 +27,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; @@ -179,7 +181,14 @@ public class ControllerUtils { JsonArray arr = userJson.getAsJsonArray(claim); List values = new ArrayList<>(); for (int i = 0; i < arr.size(); i++) { - values.add(arr.get(i).getAsString()); + JsonElement el = arr.get(i); + if (el instanceof JsonObject) { + values.add(transformObject((JsonObject) el)); + } else if (el instanceof JsonPrimitive) { + values.add(arr.get(i).getAsString()); + } else if (el instanceof JsonArray) { + values.add(transformArray((JsonArray) el)); + } } claimValues.put(claim, values); } @@ -204,6 +213,23 @@ public class ControllerUtils { model.put(SCOPES, sortedScopes); } + private static String transformObject(JsonObject obj) { + StringJoiner sj = new StringJoiner(", "); + for (String s: obj.keySet()) { + sj.add(s + ": " + obj.get(s).getAsString()); + } + return sj.toString(); + } + + + private static String transformArray(JsonArray arr) { + StringJoiner sj = new StringJoiner(", "); + for (int i = 0; i < arr.size(); i++) { + sj.add(arr.get(i).getAsString()); + } + return sj.toString(); + } + /** * Create URL form base and parameters.