diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/tags/common/attributesConsent.tag b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/tags/common/attributesConsent.tag index b228f6f1b..77def45f2 100644 --- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/tags/common/attributesConsent.tag +++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/tags/common/attributesConsent.tag @@ -1,5 +1,5 @@ <%@ tag pageEncoding="UTF-8" trimDirectiveWhitespaces="true" - import="cz.muni.ics.oidc.server.elixir.GA4GHClaimSource" %> + import="cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportAndVisaClaimSource" %> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="o" tagdir="/WEB-INF/tags" %> @@ -53,7 +53,7 @@ -
  • <%= GA4GHClaimSource.parseAndVerifyVisa( +
  • <%= Ga4ghPassportAndVisaClaimSource.parseAndVerifyVisa( (String) jspContext.findAttribute("subValue")).getPrettyString() %>
  • diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/themedApproveDevice.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/themedApproveDevice.jsp index 625d08612..bb532ecfa 100644 --- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/themedApproveDevice.jsp +++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/themedApproveDevice.jsp @@ -1,4 +1,4 @@ -<%@ page import="cz.muni.ics.oidc.server.elixir.GA4GHClaimSource" %> +<%@ page import="cz.muni.ics.oidc.server.ga4gh.ElixirGa4ghClaimSource" %> <%@ page import="java.util.ArrayList" %> <%@ page import="java.util.List" %> <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%> diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/AddHeaderInterceptor.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/AddHeaderInterceptor.java similarity index 82% rename from perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/AddHeaderInterceptor.java rename to perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/AddHeaderInterceptor.java index 70576bc7b..e6ce17b1d 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/AddHeaderInterceptor.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/AddHeaderInterceptor.java @@ -1,4 +1,4 @@ -package cz.muni.ics.oidc.server.elixir; +package cz.muni.ics.oidc.server; import java.io.IOException; import org.springframework.http.HttpRequest; @@ -12,12 +12,12 @@ import org.springframework.http.client.ClientHttpResponse; * * @author Martin Kuba */ -class AddHeaderInterceptor implements ClientHttpRequestInterceptor { +public class AddHeaderInterceptor implements ClientHttpRequestInterceptor { private final String header; private final String value; - AddHeaderInterceptor(String header, String value) { + public AddHeaderInterceptor(String header, String value) { this.header = header; this.value = value; } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimSourceInitContext.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimSourceInitContext.java index f3349bb06..210a42082 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimSourceInitContext.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimSourceInitContext.java @@ -41,6 +41,17 @@ public class ClaimSourceInitContext { return properties.getProperty(propertyPrefix + "." + suffix, defaultValue); } + public Long getLongProperty(String suffix, Long defaultValue) { + String propKey = propertyPrefix + '.' + suffix; + String prop = properties.getProperty(propertyPrefix + "." + suffix); + try { + return Long.parseLong(prop); + } catch (NumberFormatException e) { + log.warn("Could not parse value '{}' for property '{}' as Long", prop, propKey); + } + return defaultValue; + } + public JWTSigningAndValidationService getJwtService() { return jwtService; } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/GA4GHClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/GA4GHClaimSource.java deleted file mode 100644 index 088a4ae88..000000000 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/GA4GHClaimSource.java +++ /dev/null @@ -1,591 +0,0 @@ -package cz.muni.ics.oidc.server.elixir; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; -import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.Payload; -import com.nimbusds.jose.crypto.RSASSAVerifier; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKMatcher; -import com.nimbusds.jose.jwk.JWKSelector; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.RemoteJWKSet; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.JWTParser; -import com.nimbusds.jwt.SignedJWT; -import cz.muni.ics.jwt.signer.service.JWTSigningAndValidationService; -import cz.muni.ics.oidc.models.PerunAttribute; -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 cz.muni.ics.oidc.server.connectors.Affiliation; -import cz.muni.ics.openid.connect.web.JWKSetPublishingEndpoint; -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLEncoder; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; -import org.springframework.http.client.InterceptingClientHttpRequestFactory; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestTemplate; - -/** - * Class producing GA4GH Passport claim. The claim is specified in - * https://bit.ly/ga4gh-passport-v1 - * - * Configuration (replace [claimName] with the name of the claim): - *
      - *
    • custom.claim.[claimName].source.config_file - full path to the configuration file for this claim. See - * configuration templates for such a file.
    • - *
    • custom.claim.[claimName].source.bonaFideStatus.attr - mapping for bonaFideStatus Attriute
    • - *
    • custom.claim.[claimName].source.bonaFideStatusREMS.attr - mapping for bonaFideStatus Attriute
    • - *
    • custom.claim.[claimName].source.groupAffiliations.attr - mapping for groupAffiliations Attriute
    • - *
    - * - * @author Martin Kuba - */ -@Slf4j -public class GA4GHClaimSource extends ClaimSource { - - static final String GA4GH_SCOPE = "ga4gh_passport_v1"; - private static final String GA4GH_CLAIM = "ga4gh_passport_v1"; - - private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y"; - private static final String ELIXIR_ORG_URL = "https://elixir-europe.org/"; - private static final String ELIXIR_ID = "elixir_id"; - - private final JWTSigningAndValidationService jwtService; - private final URI jku; - private final String issuer; - private static final List claimRepositories = new ArrayList<>(); - private static final Map> remoteJwkSets = new HashMap<>(); - private static final Map signers = new HashMap<>(); - - private final String bonaFideStatusAttr; - private final String bonaFideStatusREMSAttr; - private final String groupAffiliationsAttr; - - public GA4GHClaimSource(ClaimSourceInitContext ctx) throws URISyntaxException { - super(ctx); - log.debug("initializing"); - //remember context - jwtService = ctx.getJwtService(); - issuer = ctx.getPerunOidcConfig().getConfigBean().getIssuer(); - jku = new URI(issuer + JWKSetPublishingEndpoint.URL); - // load config file - parseConfigFile(ctx.getProperty("config_file", "/etc/mitreid/elixir/ga4gh_config.yml")); - bonaFideStatusAttr = ctx.getProperty("bonaFideStatus.attr", null); - bonaFideStatusREMSAttr = ctx.getProperty("bonaFideStatusREMS.attr", null); - groupAffiliationsAttr = ctx.getProperty("groupAffiliations.attr", null); - } - - static void parseConfigFile(String file) { - YAMLMapper mapper = new YAMLMapper(); - try { - JsonNode root = mapper.readValue(new File(file), JsonNode.class); - // prepare claim repositories - for (JsonNode repo : root.path("repos")) { - String name = repo.path("name").asText(); - String actionURL = repo.path("url").asText(); - JsonNode headers = repo.path("headers"); - Map headersWithValues = new HashMap<>(); - for (JsonNode header: headers) { - headersWithValues.put(header.path("header").asText(), header.path("value").asText()); - } - if (actionURL == null || headersWithValues.isEmpty()) { - log.error("claim repository " + repo + " not defined with url|auth_header|auth_value "); - continue; - } - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setRequestFactory( - new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), - headersWithValues.entrySet() - .stream() - .map(e -> new AddHeaderInterceptor(e.getKey(), e.getValue())) - .collect(Collectors.toList())) - ); - claimRepositories.add(new ClaimRepository(name, restTemplate, actionURL)); - log.info("GA4GH Claims Repository " + name + " configured at " + actionURL); - } - // prepare claim signers - for (JsonNode signer : root.path("signers")) { - String name = signer.path("name").asText(); - String jwks = signer.path("jwks").asText(); - try { - URL jku = new URL(jwks); - remoteJwkSets.put(jku.toURI(), new RemoteJWKSet<>(jku)); - signers.put(jku.toURI(), name); - log.info("JWKS Signer " + name + " added with keys " + jwks); - } catch (MalformedURLException | URISyntaxException e) { - log.error("cannot add to RemoteJWKSet map: " + name + " " + jwks, e); - } - } - } catch (IOException ex) { - log.error("cannot read GA4GH config file", ex); - } - } - - @Override - public Set getAttrIdentifiers() { - Set set = new HashSet<>(); - if (bonaFideStatusAttr != null) { - set.add(bonaFideStatusAttr); - } - if (bonaFideStatusREMSAttr != null) { - set.add(bonaFideStatusREMSAttr); - } - if (groupAffiliationsAttr != null) { - set.add(groupAffiliationsAttr); - } - return set; - } - - @Override - public JsonNode produceValue(ClaimSourceProduceContext pctx) { - if (pctx.getClient() == null) { - log.debug("client is not set"); - return JsonNodeFactory.instance.textNode("Global Alliance For Genomic Health structured claim"); - } - if (!pctx.getClient().getScope().contains(GA4GH_SCOPE)) { - log.debug("Client '{}' does not have scope ga4gh", pctx.getClient().getClientName()); - return null; - } - - List affiliations = pctx.getPerunAdapter() - .getAdapterRpc() - .getUserExtSourcesAffiliations(pctx.getPerunUserId()); - - ArrayNode ga4gh_passport_v1 = JsonNodeFactory.instance.arrayNode(); - long now = Instant.now().getEpochSecond(); - addAffiliationAndRoles(now, pctx, ga4gh_passport_v1, affiliations); - addAcceptedTermsAndPolicies(now, pctx, ga4gh_passport_v1); - addResearcherStatuses(now, pctx, ga4gh_passport_v1, affiliations); - addControlledAccessGrants(now, pctx, ga4gh_passport_v1); - return ga4gh_passport_v1; - } - - - private void addAffiliationAndRoles(long now, ClaimSourceProduceContext pctx, ArrayNode passport, List affiliations) { - //by=system for users with affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") - for (Affiliation affiliation : affiliations) { - //expires 1 year after the last login from the IdP asserting the affiliation - long expires = Instant.ofEpochSecond(affiliation.getAsserted()).atZone(ZoneId.systemDefault()).plusYears(1L).toEpochSecond(); - if (expires < now) continue; - JsonNode visa = createPassportVisa("AffiliationAndRole", pctx, affiliation.getValue(), affiliation.getSource(), "system", affiliation.getAsserted(), expires, null); - if (visa != null) { - passport.add(visa); - } - } - } - - private void addAcceptedTermsAndPolicies(long now, ClaimSourceProduceContext pctx, ArrayNode passport) { - //by=self for members of the group 10432 "Bona Fide Researchers" - boolean userInGroup = pctx.getPerunAdapter().isUserInGroup(pctx.getPerunUserId(), 10432L); - if (userInGroup) { - PerunAttribute bonaFideStatus = pctx.getPerunAdapter() - .getAdapterRpc() - .getUserAttribute(pctx.getPerunUserId(), bonaFideStatusAttr); - String valueCreatedAt = bonaFideStatus.getValueCreatedAt(); - long asserted; - if (valueCreatedAt != null) { - asserted = Timestamp.valueOf(valueCreatedAt).getTime() / 1000L; - } else { - asserted = System.currentTimeMillis() / 1000L; - } - long expires = Instant.ofEpochSecond(asserted).atZone(ZoneId.systemDefault()).plusYears(100L).toEpochSecond(); - if (expires < now) return; - JsonNode visa = createPassportVisa("AcceptedTermsAndPolicies", pctx, BONA_FIDE_URL, ELIXIR_ORG_URL, "self", asserted, expires, null); - if (visa != null) { - passport.add(visa); - } - } - } - - private void addResearcherStatuses(long now, ClaimSourceProduceContext pctx, ArrayNode passport, List affiliations) { - //by=peer for users with attribute elixirBonaFideStatusREMS - PerunAttribute elixirBonaFideStatusREMS = pctx.getPerunAdapter() - .getAdapterRpc() - .getUserAttribute(pctx.getPerunUserId(), bonaFideStatusREMSAttr); - - String valueCreatedAt = null; - if (elixirBonaFideStatusREMS != null) { - valueCreatedAt = elixirBonaFideStatusREMS.getValueCreatedAt(); - } - - if (valueCreatedAt != null) { - long asserted = Timestamp.valueOf(valueCreatedAt).getTime() / 1000L; - long expires = ZonedDateTime.now().plusYears(1L).toEpochSecond(); - if (expires > now) { - JsonNode visa = createPassportVisa("ResearcherStatus", pctx, BONA_FIDE_URL, ELIXIR_ORG_URL, "peer", asserted, expires, null); - if (visa != null) { - passport.add(visa); - } - } - } - //by=system for users with faculty affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") - for (Affiliation affiliation : affiliations) { - if (affiliation.getValue().startsWith("faculty@")) { - long expires = Instant.ofEpochSecond(affiliation.getAsserted()).atZone(ZoneId.systemDefault()).plusYears(1L).toEpochSecond(); - if (expires < now) continue; - JsonNode visa = createPassportVisa("ResearcherStatus", pctx, BONA_FIDE_URL, affiliation.getSource(), "system", affiliation.getAsserted(), expires, null); - if (visa != null) { - passport.add(visa); - } - } - } - //by=so for users with faculty affiliation asserted by membership in a group with groupAffiliations attribute - for (Affiliation affiliation : pctx.getPerunAdapter().getGroupAffiliations(pctx.getPerunUserId(), groupAffiliationsAttr)) { - if (affiliation.getValue().startsWith("faculty@")) { - long expires = ZonedDateTime.now().plusYears(1L).toEpochSecond(); - JsonNode visa = createPassportVisa("ResearcherStatus", pctx, BONA_FIDE_URL, ELIXIR_ORG_URL, "so", affiliation.getAsserted(), expires, null); - if (visa != null) { - passport.add(visa); - } - } - } - } - - private static String isoDate(long linuxTime) { - return DateTimeFormatter.ISO_LOCAL_DATE.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault())); - } - - private static String isoDateTime(long linuxTime) { - return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault())); - } - - private JsonNode createPassportVisa(String type, ClaimSourceProduceContext pctx, String value, String source, String by, long asserted, long expires, JsonNode condition) { - long now = System.currentTimeMillis() / 1000L; - if (asserted > now) { - log.warn("visa asserted in future ! perunUserId {} sub {} type {} value {} source {} by {} asserted {}", pctx.getPerunUserId(), pctx.getSub(), type, value, source, by, Instant.ofEpochSecond(asserted)); - return null; - } - if (expires <= now) { - log.warn("visa already expired ! perunUserId {} sub {} type {} value {} source {} by {} expired {}", pctx.getPerunUserId(), pctx.getSub(), type, value, source, by, Instant.ofEpochSecond(expires)); - return null; - } - - Map passportVisaObject = new HashMap<>(); - passportVisaObject.put("type", type); - passportVisaObject.put("asserted", asserted); - passportVisaObject.put("value", value); - passportVisaObject.put("source", source); - passportVisaObject.put("by", by); - if (condition != null && !condition.isNull() && !condition.isMissingNode()) { - passportVisaObject.put("condition", condition); - } - JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.parse(jwtService.getDefaultSigningAlgorithm().getName())) - .keyID(jwtService.getDefaultSignerKeyId()) - .type(JOSEObjectType.JWT) - .jwkURL(jku) - .build(); - JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() - .issuer(issuer) - .issueTime(new Date()) - .expirationTime(new Date(expires * 1000L)) - .subject(pctx.getSub()) - .jwtID(UUID.randomUUID().toString()) - .claim("ga4gh_visa_v1", passportVisaObject) - .build(); - SignedJWT myToken = new SignedJWT(jwsHeader, jwtClaimsSet); - jwtService.signJwt(myToken); - return JsonNodeFactory.instance.textNode(myToken.serialize()); - } - - private void addControlledAccessGrants(long now, ClaimSourceProduceContext pctx, ArrayNode passport) { - Set linkedIdentities = new HashSet<>(); - //call Resource Entitlement Management System - for (ClaimRepository repo : claimRepositories) { - callPermissionsJwtAPI(repo, Collections.singletonMap(ELIXIR_ID, pctx.getSub()), pctx, passport, linkedIdentities); - } - if (!linkedIdentities.isEmpty()) { - for (String linkedIdentity : linkedIdentities) { - JsonNode visa = createPassportVisa("LinkedIdentities", pctx, linkedIdentity, ELIXIR_ORG_URL, "system", now, now + 3600L * 24 * 365, null); - if (visa != null) { - passport.add(visa); - } - } - } - } - - private void callPermissionsJwtAPI(ClaimRepository repo, Map uriVariables, ClaimSourceProduceContext pctx, ArrayNode passport, Set linkedIdentities) { - JsonNode response = callHttpJsonAPI(repo, uriVariables); - if (response != null) { - JsonNode visas = response.path(GA4GH_CLAIM); - if (visas.isArray()) { - for (JsonNode visaNode : visas) { - if (visaNode.isTextual()) { - PassportVisa visa = parseAndVerifyVisa(visaNode.asText()); - if (visa.isVerified()) { - log.debug("adding a visa to passport: {}", visa); - passport.add(passport.textNode(visa.getJwt())); - linkedIdentities.add(visa.getLinkedIdentity()); - } else { - log.warn("skipping visa: {}", visa); - } - } else { - log.warn("element of ga4gh_passport_v1 is not a String: {}", visaNode); - } - } - } else { - log.warn("ga4gh_passport_v1 is not an array in {}", response); - } - } - } - - - public static PassportVisa parseAndVerifyVisa(String jwtString) { - PassportVisa visa = new PassportVisa(jwtString); - try { - SignedJWT signedJWT = (SignedJWT) JWTParser.parse(jwtString); - URI jku = signedJWT.getHeader().getJWKURL(); - if (jku == null) { - log.error("JKU is missing in JWT header"); - return visa; - } - visa.setSigner(signers.get(jku)); - RemoteJWKSet remoteJWKSet = remoteJwkSets.get(jku); - if (remoteJWKSet == null) { - log.error("JKU {} is not among trusted key sets", jku); - return visa; - } - List keys = remoteJWKSet.get(new JWKSelector(new JWKMatcher.Builder().keyID(signedJWT.getHeader().getKeyID()).build()), null); - RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) keys.get(0)).toRSAPublicKey()); - visa.setVerified(signedJWT.verify(verifier)); - if (visa.isVerified()) { - processPayload(visa, signedJWT.getPayload()); - } - } catch (Exception ex) { - log.error("visa " + jwtString + " cannot be parsed and verified", ex); - } - return visa; - } - - static private final ObjectMapper JSON_MAPPER = new ObjectMapper(); - - static private void processPayload(PassportVisa visa, Payload payload) throws IOException { - JsonNode doc = JSON_MAPPER.readValue(payload.toString(), JsonNode.class); - checkVisaKey(visa, doc, "sub"); - checkVisaKey(visa, doc, "exp"); - checkVisaKey(visa, doc, "iss"); - JsonNode visa_v1 = doc.path("ga4gh_visa_v1"); - checkVisaKey(visa, visa_v1, "type"); - checkVisaKey(visa, visa_v1, "asserted"); - checkVisaKey(visa, visa_v1, "value"); - checkVisaKey(visa, visa_v1, "source"); - checkVisaKey(visa, visa_v1, "by"); - if (!visa.isVerified()) return; - long exp = doc.get("exp").asLong(); - if (exp < Instant.now().getEpochSecond()) { - log.warn("visa expired on " + isoDateTime(exp)); - visa.setVerified(false); - return; - } - visa.setLinkedIdentity(URLEncoder.encode(doc.get("sub").asText(), "utf-8") + "," + URLEncoder.encode(doc.get("iss").asText(), "utf-8")); - visa.setPrettyPayload( - visa_v1.get("type").asText() + ": \"" + visa_v1.get("value").asText() + "\" asserted " + isoDate(visa_v1.get("asserted").asLong()) - ); - } - - static private void checkVisaKey(PassportVisa visa, JsonNode jsonNode, String key) { - if (jsonNode.path(key).isMissingNode()) { - log.warn(key + " is missing"); - visa.setVerified(false); - } else { - switch (key) { - case "sub": - visa.setSub(jsonNode.path(key).asText()); - break; - case "iss": - visa.setIss(jsonNode.path(key).asText()); - break; - case "type": - visa.setType(jsonNode.path(key).asText()); - break; - case "value": - visa.setValue(jsonNode.path(key).asText()); - break; - } - } - } - - @SuppressWarnings("Duplicates") - private static JsonNode callHttpJsonAPI(ClaimRepository repo, Map uriVariables) { - //get permissions data - try { - JsonNode result; - //make the call - try { - if (log.isDebugEnabled()) { - log.debug("calling Permissions API at {}", repo.getRestTemplate().getUriTemplateHandler().expand(repo.getActionURL(), uriVariables)); - } - result = repo.getRestTemplate().getForObject(repo.getActionURL(), JsonNode.class, uriVariables); - } catch (HttpClientErrorException ex) { - MediaType contentType = ex.getResponseHeaders().getContentType(); - String body = ex.getResponseBodyAsString(); - log.error("HTTP ERROR " + ex.getRawStatusCode() + " URL " + repo.getActionURL() + " Content-Type: " + contentType); - if (ex.getRawStatusCode() == 404) { - log.warn("Got status 404 from Permissions endpoint {}, ELIXIR AAI user is not linked to user at Permissions API", repo.getActionURL()); - return null; - } - if ("json".equals(contentType.getSubtype())) { - try { - log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText()); - } catch (IOException e) { - log.error("cannot parse error message from JSON", e); - } - } else { - log.error("cannot make REST call, exception: {} message: {}", ex.getClass().getName(), ex.getMessage()); - } - return null; - } - log.debug("Permissions API response: {}", result); - return result; - } catch (Exception ex) { - log.error("Cannot get dataset permissions", ex); - } - return null; - } - - public static class PassportVisa { - String jwt; - boolean verified = false; - String linkedIdentity; - String signer; - String prettyPayload; - private String sub; - private String iss; - private String type; - private String value; - - PassportVisa(String jwt) { - this.jwt = jwt; - } - - public String getJwt() { - return jwt; - } - - public boolean isVerified() { - return verified; - } - - void setVerified(boolean verified) { - this.verified = verified; - } - - String getLinkedIdentity() { - return linkedIdentity; - } - - void setLinkedIdentity(String linkedIdentity) { - this.linkedIdentity = linkedIdentity; - } - - void setSigner(String signer) { - this.signer = signer; - } - - void setPrettyPayload(String prettyPayload) { - this.prettyPayload = prettyPayload; - } - - public String getPrettyString() { - return prettyPayload + ", signed by " + signer; - } - - @Override - public String toString() { - return "PassportVisa{" + -// "jwt='" + jwt + '\'' + - " type=" + type + - ", sub=" + sub + - ", iss=" + iss + - ", value=" + value + - ", verified=" + verified + - ", linkedIdentity=" + linkedIdentity + - '}'; - } - - public void setSub(String sub) { - this.sub = sub; - } - - public String getSub() { - return sub; - } - - public void setIss(String iss) { - this.iss = iss; - } - - public String getIss() { - return iss; - } - - public void setType(String type) { - this.type = type; - } - - public String getType() { - return type; - } - - public void setValue(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - } - - public static class ClaimRepository { - private final String name; - private final RestTemplate restTemplate; - private final String actionURL; - - public ClaimRepository(String name, RestTemplate restTemplate, String actionURL) { - this.name = name; - this.restTemplate = restTemplate; - this.actionURL = actionURL; - } - - public RestTemplate getRestTemplate() { - return restTemplate; - } - - public String getActionURL() { - return actionURL; - } - - public String getName() { - return name; - } - } - -} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/BbmriGa4ghClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/BbmriGa4ghClaimSource.java new file mode 100644 index 000000000..1af62c866 --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/BbmriGa4ghClaimSource.java @@ -0,0 +1,235 @@ +package cz.muni.ics.oidc.server.ga4gh; + +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_PEER; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_SELF; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_SO; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_SYSTEM; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import cz.muni.ics.oidc.models.PerunAttribute; +import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext; +import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext; +import cz.muni.ics.oidc.server.connectors.Affiliation; +import java.net.URISyntaxException; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * Class producing GA4GH Passport claim. The claim is specified in + * https://bit.ly/ga4gh-passport-v1 + * + * Configuration (replace [claimName] with the name of the claim): + *
      + *
    • custom.claim.[claimName].source.config_file - full path to the configuration file for this claim. See + * configuration templates for such a file.
    • (Passed to Ga4ghPassportAndVisaClaimSource.class) + *
    • custom.claim.[claimName].source.bonaFideStatus.attr - mapping for bonaFideStatus Attribute
    • + *
    • custom.claim.[claimName].source.groupAffiliations.attr - mapping for groupAffiliations Attribute
    • + *
    • custom.claim.[claimName].source.termsAndPoliciesGroupId - ID of group in which the membership represents acceptance of terms and policies
    • + *
    + * + * @author Martin Kuba + */ +@Slf4j +public class BbmriGa4ghClaimSource extends Ga4ghPassportAndVisaClaimSource { + + private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y"; + private static final String BBMRI_ERIC_ORG_URL = "https://www.bbmri-eric.eu/"; + private static final String BBMRI_ID = "bbmri_id"; + private static final String FACULTY_AT = "faculty@"; + + private final String bonaFideStatusAttr; + private final String groupAffiliationsAttr; + private final Long termsAndPoliciesGroupId; + + public BbmriGa4ghClaimSource(ClaimSourceInitContext ctx) throws URISyntaxException { + super(ctx, "BBMRI-ERIC"); + bonaFideStatusAttr = ctx.getProperty("bonaFideStatus.attr", null); + groupAffiliationsAttr = ctx.getProperty("groupAffiliations.attr", null); + //TODO: update group ID + termsAndPoliciesGroupId = ctx.getLongProperty("termsAndPoliciesGroupId", 10432L); + } + + @Override + public Set getAttrIdentifiers() { + Set set = new HashSet<>(); + if (bonaFideStatusAttr != null) { + set.add(bonaFideStatusAttr); + } + if (groupAffiliationsAttr != null) { + set.add(groupAffiliationsAttr); + } + return set; + } + + @Override + protected String getDefaultConfigFilePath() { + return "/etc/mitreid/bbmri/ga4gh_config.yml"; + } + + @Override + protected void addAffiliationAndRoles(long now, + ClaimSourceProduceContext pctx, + ArrayNode passport, + List affiliations) + { + //by=system for users with affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") + if (affiliations == null) { + return; + } + for (Affiliation affiliation: affiliations) { + //expires 1 year after the last login from the IdP asserting the affiliation + long expires = Ga4ghUtils.getOneYearExpires(affiliation.getAsserted()); + if (expires < now) { + continue; + } + JsonNode visa = createPassportVisa(TYPE_AFFILIATION_AND_ROLE, pctx, affiliation.getValue(), + affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + @Override + protected void addAcceptedTermsAndPolicies(long now, ClaimSourceProduceContext pctx, ArrayNode passport) { + //by=self for members of the group 10432 "Bona Fide Researchers" + boolean userInGroup = pctx.getPerunAdapter().isUserInGroup(pctx.getPerunUserId(), termsAndPoliciesGroupId); + if (!userInGroup) { + return; + } + long asserted = now; + if (bonaFideStatusAttr != null) { + PerunAttribute bonaFideStatus = pctx.getPerunAdapter() + .getAdapterRpc() + .getUserAttribute(pctx.getPerunUserId(), bonaFideStatusAttr); + if (bonaFideStatus != null && bonaFideStatus.getValueCreatedAt() != null) { + asserted = Timestamp.valueOf(bonaFideStatus.getValueCreatedAt()).getTime() / 1000L; + } + } + long expires = Ga4ghUtils.getExpires(asserted, 100L); + if (expires < now) { + return; + } + JsonNode visa = createPassportVisa(TYPE_ACCEPTED_TERMS_AND_POLICIES, pctx, BONA_FIDE_URL, + BBMRI_ERIC_ORG_URL, BY_SELF, asserted, expires, null); + if (visa != null) { + passport.add(visa); + } + } + + @Override + protected void addResearcherStatuses(long now, + ClaimSourceProduceContext pctx, ArrayNode passport, + List affiliations) + { + addResearcherStatusFromBonaFideAttribute(pctx, now, passport); + addResearcherStatusFromAffiliation(pctx, affiliations, now, passport); + addResearcherStatusGroupAffiliations(pctx, now, passport); + } + + @Override + protected void addControlledAccessGrants(long now, ClaimSourceProduceContext pctx, ArrayNode passport) { + if (CLAIM_REPOSITORIES.isEmpty()) { + return; + } + Set linkedIdentities = new HashSet<>(); + for (Ga4ghClaimRepository repo: CLAIM_REPOSITORIES) { + callPermissionsJwtAPI(repo, Collections.singletonMap(BBMRI_ID, pctx.getSub()), passport, linkedIdentities); + } + if (linkedIdentities.isEmpty()) { + return; + } + for (String linkedIdentity : linkedIdentities) { + long expires = Ga4ghUtils.getOneYearExpires(now); + JsonNode visa = createPassportVisa(TYPE_LINKED_IDENTITIES, pctx, linkedIdentity, + BBMRI_ERIC_ORG_URL, BY_SYSTEM, now, expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + private void addResearcherStatusFromBonaFideAttribute(ClaimSourceProduceContext pctx, + long now, + ArrayNode passport) + { + //by=peer for users with attribute elixirBonaFideStatusREMS + PerunAttribute bbmriBonaFideStatus = pctx.getPerunAdapter() + .getAdapterRpc() + .getUserAttribute(pctx.getPerunUserId(), bonaFideStatusAttr); + + String valueCreatedAt = null; + if (bbmriBonaFideStatus != null) { + valueCreatedAt = bbmriBonaFideStatus.getValueCreatedAt(); + } + + if (valueCreatedAt == null) { + return; + } + long asserted = Timestamp.valueOf(valueCreatedAt).getTime() / 1000L; + long expires = Ga4ghUtils.getOneYearExpires(asserted); + if (expires > now) { + JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, pctx, BONA_FIDE_URL, + BBMRI_ERIC_ORG_URL, BY_PEER, asserted, expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + private void addResearcherStatusFromAffiliation(ClaimSourceProduceContext pctx, + List affiliations, + long now, + ArrayNode passport) + { + //by=system for users with faculty affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") + if (affiliations == null) { + return; + } + for (Affiliation affiliation: affiliations) { + if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { + continue; + } + long expires = Ga4ghUtils.getOneYearExpires(affiliation.getAsserted()); + if (expires < now) { + continue; + } + JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, pctx, BONA_FIDE_URL, + affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + private void addResearcherStatusGroupAffiliations(ClaimSourceProduceContext pctx, long now, ArrayNode passport) { + //by=so for users with faculty affiliation asserted by membership in a group with groupAffiliations attribute + List groupAffiliations = pctx.getPerunAdapter() + .getGroupAffiliations(pctx.getPerunUserId(), groupAffiliationsAttr); + if (groupAffiliations == null) { + return; + } + for (Affiliation affiliation: groupAffiliations) { + if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { + continue; + } + long expires = Ga4ghUtils.getOneYearExpires(now); + JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, pctx, BONA_FIDE_URL, + BBMRI_ERIC_ORG_URL, BY_SO, affiliation.getAsserted(), expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/ElixirGa4ghClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/ElixirGa4ghClaimSource.java new file mode 100644 index 000000000..e8f6ac4df --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/ElixirGa4ghClaimSource.java @@ -0,0 +1,240 @@ +package cz.muni.ics.oidc.server.ga4gh; + +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_PEER; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_SELF; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_SO; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.BY_SYSTEM; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import cz.muni.ics.oidc.models.PerunAttribute; +import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext; +import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext; +import cz.muni.ics.oidc.server.connectors.Affiliation; +import java.net.URISyntaxException; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * Class producing GA4GH Passport claim. The claim is specified in + * https://bit.ly/ga4gh-passport-v1 + * + * Configuration (replace [claimName] with the name of the claim): + *
      + *
    • custom.claim.[claimName].source.config_file - full path to the configuration file for this claim. See + * configuration templates for such a file.
    • (Passed to Ga4ghPassportAndVisaClaimSource.class) + *
    • custom.claim.[claimName].source.bonaFideStatus.attr - mapping for bonaFideStatus Attribute
    • + *
    • custom.claim.[claimName].source.bonaFideStatusREMS.attr - mapping for bonaFideStatus Attribute
    • + *
    • custom.claim.[claimName].source.groupAffiliations.attr - mapping for groupAffiliations Attribute
    • + *
    • custom.claim.[claimName].source.termsAndPoliciesGroupId - ID of group in which the membership represents acceptance of terms and policies
    • + *
    + * + * @author Martin Kuba + */ +@Slf4j +public class ElixirGa4ghClaimSource extends Ga4ghPassportAndVisaClaimSource { + + private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y"; + private static final String ELIXIR_ORG_URL = "https://elixir-europe.org/"; + private static final String ELIXIR_ID = "elixir_id"; + private static final String FACULTY_AT = "faculty@"; + + private final String bonaFideStatusAttr; + private final String bonaFideStatusREMSAttr; + private final String groupAffiliationsAttr; + private final Long termsAndPoliciesGroupId; + + public ElixirGa4ghClaimSource(ClaimSourceInitContext ctx) throws URISyntaxException { + super(ctx, "ELIXIR"); + bonaFideStatusAttr = ctx.getProperty("bonaFideStatus.attr", null); + bonaFideStatusREMSAttr = ctx.getProperty("bonaFideStatusREMS.attr", null); + groupAffiliationsAttr = ctx.getProperty("groupAffiliations.attr", null); + termsAndPoliciesGroupId = ctx.getLongProperty("termsAndPoliciesGroupId", 10432L); + } + + @Override + public Set getAttrIdentifiers() { + Set set = new HashSet<>(); + if (bonaFideStatusAttr != null) { + set.add(bonaFideStatusAttr); + } + if (bonaFideStatusREMSAttr != null) { + set.add(bonaFideStatusREMSAttr); + } + if (groupAffiliationsAttr != null) { + set.add(groupAffiliationsAttr); + } + return set; + } + + @Override + protected String getDefaultConfigFilePath() { + return "/etc/mitreid/elixir/ga4gh_config.yml"; + } + + @Override + protected void addAffiliationAndRoles(long now, + ClaimSourceProduceContext pctx, + ArrayNode passport, + List affiliations) + { + //by=system for users with affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") + if (affiliations == null) { + return; + } + for (Affiliation affiliation: affiliations) { + //expires 1 year after the last login from the IdP asserting the affiliation + long expires = Ga4ghUtils.getOneYearExpires(affiliation.getAsserted()); + if (expires < now) { + continue; + } + JsonNode visa = createPassportVisa(TYPE_AFFILIATION_AND_ROLE, pctx, affiliation.getValue(), + affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + @Override + protected void addAcceptedTermsAndPolicies(long now, ClaimSourceProduceContext pctx, ArrayNode passport) { + //by=self for members of the group "Bona Fide Researchers" + boolean userInGroup = pctx.getPerunAdapter().isUserInGroup(pctx.getPerunUserId(), termsAndPoliciesGroupId); + if (!userInGroup) { + return; + } + long asserted = now; + if (bonaFideStatusAttr != null) { + PerunAttribute bonaFideStatus = pctx.getPerunAdapter() + .getAdapterRpc() + .getUserAttribute(pctx.getPerunUserId(), bonaFideStatusAttr); + if (bonaFideStatus != null && bonaFideStatus.getValueCreatedAt() != null) { + asserted = Timestamp.valueOf(bonaFideStatus.getValueCreatedAt()).getTime() / 1000L; + } + } + long expires = Ga4ghUtils.getExpires(asserted, 100L); + if (expires < now) { + return; + } + JsonNode visa = createPassportVisa(TYPE_ACCEPTED_TERMS_AND_POLICIES, pctx, + BONA_FIDE_URL, ELIXIR_ORG_URL, BY_SELF, asserted, expires, null); + if (visa != null) { + passport.add(visa); + } + } + + @Override + protected void addResearcherStatuses(long now, + ClaimSourceProduceContext pctx, + ArrayNode passport, + List affiliations) + { + addResearcherStatusFromBonaFideAttribute(pctx, now, passport); + addResearcherStatusFromAffiliation(pctx, affiliations, now, passport); + addResearcherStatusGroupAffiliations(pctx, now, passport); + } + + @Override + protected void addControlledAccessGrants(long now, ClaimSourceProduceContext pctx, ArrayNode passport) { + if (CLAIM_REPOSITORIES.isEmpty()) { + return; + } + Set linkedIdentities = new HashSet<>(); + for (Ga4ghClaimRepository repo: CLAIM_REPOSITORIES) { + callPermissionsJwtAPI(repo, Collections.singletonMap(ELIXIR_ID, pctx.getSub()), passport, linkedIdentities); + } + if (linkedIdentities.isEmpty()) { + return; + } + for (String linkedIdentity : linkedIdentities) { + long expires = Ga4ghUtils.getOneYearExpires(now); + JsonNode visa = createPassportVisa(TYPE_LINKED_IDENTITIES, pctx, linkedIdentity, + ELIXIR_ORG_URL, BY_SYSTEM, now, expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + private void addResearcherStatusFromBonaFideAttribute(ClaimSourceProduceContext pctx, + long now, + ArrayNode passport) + { + //by=peer for users with attribute elixirBonaFideStatusREMS + String valueCreatedAt = null; + PerunAttribute elixirBonaFideStatusREMS = pctx.getPerunAdapter() + .getAdapterRpc() + .getUserAttribute(pctx.getPerunUserId(), bonaFideStatusREMSAttr); + if (elixirBonaFideStatusREMS != null) { + valueCreatedAt = elixirBonaFideStatusREMS.getValueCreatedAt(); + } + if (valueCreatedAt == null) { + return; + } + long asserted = Timestamp.valueOf(valueCreatedAt).getTime() / 1000L; + long expires = Ga4ghUtils.getOneYearExpires(asserted); + if (expires < now) { + return; + } + JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, pctx, BONA_FIDE_URL, + ELIXIR_ORG_URL, BY_PEER, asserted, expires, null); + if (visa != null) { + passport.add(visa); + } + } + + private void addResearcherStatusFromAffiliation(ClaimSourceProduceContext pctx, + List affiliations, + long now, + ArrayNode passport) + { + //by=system for users with faculty affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") + if (affiliations == null) { + return; + } + for (Affiliation affiliation: affiliations) { + if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { + continue; + } + long expires = Ga4ghUtils.getOneYearExpires(affiliation.getAsserted()); + if (expires < now) { + continue; + } + JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, pctx, BONA_FIDE_URL, + affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + + private void addResearcherStatusGroupAffiliations(ClaimSourceProduceContext pctx, long now, ArrayNode passport) { + //by=so for users with faculty affiliation asserted by membership in a group with groupAffiliations attribute + List groupAffiliations = pctx.getPerunAdapter() + .getGroupAffiliations(pctx.getPerunUserId(), groupAffiliationsAttr); + if (groupAffiliations == null) { + return; + } + for (Affiliation affiliation: groupAffiliations) { + if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { + continue; + } + long expires = Ga4ghUtils.getOneYearExpires(now); + JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, pctx, BONA_FIDE_URL, + ELIXIR_ORG_URL, BY_SO, affiliation.getAsserted(), expires, null); + if (visa != null) { + passport.add(visa); + } + } + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/GA4GHTokenParser.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/GA4GHTokenParser.java similarity index 50% rename from perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/GA4GHTokenParser.java rename to perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/GA4GHTokenParser.java index a705506dd..79f118949 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/GA4GHTokenParser.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/GA4GHTokenParser.java @@ -1,20 +1,21 @@ -package cz.muni.ics.oidc.server.elixir; - -import static cz.muni.ics.oidc.server.elixir.GA4GHClaimSource.parseAndVerifyVisa; +package cz.muni.ics.oidc.server.ga4gh; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTParser; import com.nimbusds.jwt.SignedJWT; import java.io.File; import java.io.IOException; +import java.net.URI; import java.text.ParseException; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * This class is a command-line debugging tool. It parses JSON in GA4GH Passport format, @@ -24,41 +25,38 @@ import java.time.format.DateTimeFormatter; */ public class GA4GHTokenParser { - static ObjectMapper jsonMapper = new ObjectMapper(); + private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final List CLAIM_REPOSITORIES = new ArrayList<>(); + private static final Map> REMOTE_JWK_SETS = new HashMap<>(); + private static final Map SIGNERS = new HashMap<>(); public static void main(String[] args) throws IOException, ParseException, JOSEException { - GA4GHClaimSource.parseConfigFile("ga4gh_config.yml"); + Ga4ghUtils.parseConfigFile("ga4gh_config.yml", CLAIM_REPOSITORIES, REMOTE_JWK_SETS, SIGNERS); String userinfo = "/tmp/ga4gh.json"; - JsonNode doc = jsonMapper.readValue(new File(userinfo), JsonNode.class); - JsonNode ga4gh = doc.get("ga4gh_passport_v1"); + JsonNode doc = MAPPER.readValue(new File(userinfo), JsonNode.class); + JsonNode ga4gh = doc.get(Ga4ghPassportAndVisaClaimSource.GA4GH_CLAIM); long startx = System.currentTimeMillis(); System.out.println(); for (JsonNode jwtString : ga4gh) { String s = jwtString.asText(); - GA4GHClaimSource.PassportVisa visa = parseAndVerifyVisa(s); + Ga4ghPassportVisa visa = Ga4ghUtils.parseAndVerifyVisa(s, SIGNERS, REMOTE_JWK_SETS, MAPPER); if(!visa.isVerified()) { - System.out.println("visa not verified: "+s); + System.out.println("visa not verified: " + s); System.out.println("visa = " + visa.getPrettyString()); - -// System.exit(1); } else { - System.out.println("OK: "+visa.getPrettyString()); + System.out.println("OK: " + visa.getPrettyString()); } SignedJWT jwt = (SignedJWT) JWTParser.parse(s); - ObjectWriter prettyPrinter = jsonMapper.writerWithDefaultPrettyPrinter(); + ObjectWriter prettyPrinter = MAPPER.writerWithDefaultPrettyPrinter(); - JsonNode visaHeader = jsonMapper.readValue(jwt.getHeader().toString(), JsonNode.class); + JsonNode visaHeader = MAPPER.readValue(jwt.getHeader().toString(), JsonNode.class); System.out.println(prettyPrinter.writeValueAsString(visaHeader)); - JsonNode visaPayload = jsonMapper.readValue(jwt.getPayload().toString(), JsonNode.class); + JsonNode visaPayload = MAPPER.readValue(jwt.getPayload().toString(), JsonNode.class); System.out.println(prettyPrinter.writeValueAsString(visaPayload)); } long endx = System.currentTimeMillis(); System.out.println("signature verification time: " + (endx - startx)); - } - private static String isoDateTime(long linuxTime) { - return DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault())); - } } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/ElixirAccessTokenModifier.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghAccessTokenModifier.java similarity index 53% rename from perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/ElixirAccessTokenModifier.java rename to perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghAccessTokenModifier.java index 51acdfc5c..6345cdf94 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/elixir/ElixirAccessTokenModifier.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghAccessTokenModifier.java @@ -1,32 +1,36 @@ -package cz.muni.ics.oidc.server.elixir; +package cz.muni.ics.oidc.server.ga4gh; import com.nimbusds.jwt.JWTClaimsSet; import cz.muni.ics.oidc.server.PerunAccessTokenEnhancer; import cz.muni.ics.openid.connect.model.UserInfo; import java.util.Collections; import java.util.Set; +import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; /** - * Implements changes required by GA4GH specification followed by the ELIXIR AAI. + * Implements changes required by GA4GH specification. * * @author Martin Kuba */ @SuppressWarnings("unused") @Slf4j -public class ElixirAccessTokenModifier implements PerunAccessTokenEnhancer.AccessTokenClaimsModifier { - - public ElixirAccessTokenModifier() { - } +@NoArgsConstructor +public class Ga4ghAccessTokenModifier implements PerunAccessTokenEnhancer.AccessTokenClaimsModifier { @Override - public void modifyClaims(String sub, JWTClaimsSet.Builder builder, OAuth2AccessToken accessToken, OAuth2Authentication authentication, UserInfo userInfo) { + public void modifyClaims(String sub, + JWTClaimsSet.Builder builder, + OAuth2AccessToken accessToken, + OAuth2Authentication authentication, + UserInfo userInfo) + { Set scopes = accessToken.getScope(); //GA4GH - if (scopes.contains(GA4GHClaimSource.GA4GH_SCOPE)) { - log.debug("adding claims required by GA4GH to access token"); + if (scopes.contains(ElixirGa4ghClaimSource.GA4GH_SCOPE)) { + log.debug("Adding claims required by GA4GH to access token"); builder.audience(Collections.singletonList(authentication.getOAuth2Request().getClientId())); } } diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghClaimRepository.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghClaimRepository.java new file mode 100644 index 000000000..0ae363d43 --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghClaimRepository.java @@ -0,0 +1,19 @@ +package cz.muni.ics.oidc.server.ga4gh; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.springframework.web.client.RestTemplate; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +public class Ga4ghClaimRepository { + + private final String name; + private final String actionURL; + private final RestTemplate restTemplate; + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghPassportAndVisaClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghPassportAndVisaClaimSource.java new file mode 100644 index 000000000..d3b722553 --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghPassportAndVisaClaimSource.java @@ -0,0 +1,228 @@ +package cz.muni.ics.oidc.server.ga4gh; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import cz.muni.ics.jwt.signer.service.JWTSigningAndValidationService; +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 cz.muni.ics.oidc.server.connectors.Affiliation; +import cz.muni.ics.openid.connect.web.JWKSetPublishingEndpoint; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.client.HttpClientErrorException; + +/** + * Class producing GA4GH Passport claim. The claim is specified in + * https://bit.ly/ga4gh-passport-v1 + * + * Configuration (replace [claimName] with the name of the claim): + *
      + *
    • custom.claim.[claimName].source.config_file - full path to the configuration file for this claim. See + * configuration templates for such a file.
    • + *
    + * + * @author Martin Kuba + */ +@Slf4j +@Getter +public abstract class Ga4ghPassportAndVisaClaimSource extends ClaimSource { + + public static final String GA4GH_SCOPE = "ga4gh_passport_v1"; + public static final String GA4GH_CLAIM = "ga4gh_passport_v1"; + + protected static final List CLAIM_REPOSITORIES = new ArrayList<>(); + protected static final Map> REMOTE_JWK_SETS = new HashMap<>(); + protected static final Map SIGNERS = new HashMap<>(); + protected static final ObjectMapper MAPPER = new ObjectMapper(); + + private final JWTSigningAndValidationService jwtService; + private final URI jku; + private final String issuer; + + public Ga4ghPassportAndVisaClaimSource(ClaimSourceInitContext ctx, String implType) throws URISyntaxException { + super(ctx); + log.debug("Initializing GA4GH Passports and Visa Claim Source - (version by {})", implType); + //remember context + jwtService = ctx.getJwtService(); + issuer = ctx.getPerunOidcConfig().getConfigBean().getIssuer(); + jku = new URI(issuer + JWKSetPublishingEndpoint.URL); + // load config file + String configFile = ctx.getProperty("config_file", getDefaultConfigFilePath()); + Ga4ghUtils.parseConfigFile(configFile, CLAIM_REPOSITORIES, REMOTE_JWK_SETS, SIGNERS); + } + + @Override + public JsonNode produceValue(ClaimSourceProduceContext pctx) { + if (pctx.getClient() == null) { + log.debug("Client is not set"); + return JsonNodeFactory.instance.textNode("Global Alliance For Genomic Health structured claim"); + } + if (!pctx.getClient().getScope().contains(GA4GH_SCOPE)) { + log.debug("Client '{}' does not have scope ga4gh", pctx.getClient().getClientName()); + return null; + } + + List affiliations = pctx.getPerunAdapter() + .getAdapterRpc() + .getUserExtSourcesAffiliations(pctx.getPerunUserId()); + + ArrayNode ga4gh_passport_v1 = JsonNodeFactory.instance.arrayNode(); + long now = Instant.now().getEpochSecond(); + addAffiliationAndRoles(now, pctx, ga4gh_passport_v1, affiliations); + addAcceptedTermsAndPolicies(now, pctx, ga4gh_passport_v1); + addResearcherStatuses(now, pctx, ga4gh_passport_v1, affiliations); + addControlledAccessGrants(now, pctx, ga4gh_passport_v1); + return ga4gh_passport_v1; + } + + public static Ga4ghPassportVisa parseAndVerifyVisa(String subValue) { + return Ga4ghUtils.parseAndVerifyVisa(subValue, SIGNERS, REMOTE_JWK_SETS, MAPPER); + } + + protected abstract String getDefaultConfigFilePath(); + + protected abstract void addAffiliationAndRoles(long now, ClaimSourceProduceContext pctx, + ArrayNode passport, List affiliations); + + protected abstract void addAcceptedTermsAndPolicies(long now, ClaimSourceProduceContext pctx, ArrayNode passport); + + protected abstract void addResearcherStatuses(long now, ClaimSourceProduceContext pctx, + ArrayNode passport, List affiliations); + + protected abstract void addControlledAccessGrants(long now, ClaimSourceProduceContext pctx, ArrayNode passport); + + protected JsonNode createPassportVisa(String type, ClaimSourceProduceContext pctx, String value, String source, + String by, long asserted, long expires, JsonNode condition) + { + long now = System.currentTimeMillis() / 1000L; + if (asserted > now) { + log.warn("Visa asserted in future, it will be ignored!"); + log.debug("Visa information: perunUserId={}, sub={}, type={}, value={}, source={}, by={}, asserted={}", + pctx.getPerunUserId(), pctx.getSub(), type, value, source, by, Instant.ofEpochSecond(asserted)); + return null; + } + if (expires <= now) { + log.warn("Visa is expired, it will be ignored!"); + log.debug("Visa information: perunUserId={}, sub={}, type={}, value={}, source={}, by={}, expired={}", + pctx.getPerunUserId(), pctx.getSub(), type, value, source, by, Instant.ofEpochSecond(expires)); + return null; + } + + Map passportVisaObject = new HashMap<>(); + passportVisaObject.put(Ga4ghPassportVisa.TYPE, type); + passportVisaObject.put(Ga4ghPassportVisa.ASSERTED, asserted); + passportVisaObject.put(Ga4ghPassportVisa.VALUE, value); + passportVisaObject.put(Ga4ghPassportVisa.SOURCE, source); + passportVisaObject.put(Ga4ghPassportVisa.BY, by); + if (condition != null && !condition.isNull() && !condition.isMissingNode()) { + passportVisaObject.put(Ga4ghPassportVisa.CONDITION, condition); + } + JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.parse(jwtService.getDefaultSigningAlgorithm().getName())) + .keyID(jwtService.getDefaultSignerKeyId()) + .type(JOSEObjectType.JWT) + .jwkURL(jku) + .build(); + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .issuer(issuer) + .issueTime(new Date()) + .expirationTime(new Date(expires * 1000L)) + .subject(pctx.getSub()) + .jwtID(UUID.randomUUID().toString()) + .claim(Ga4ghPassportVisa.GA4GH_VISA_V1, passportVisaObject) + .build(); + SignedJWT myToken = new SignedJWT(jwsHeader, jwtClaimsSet); + jwtService.signJwt(myToken); + return JsonNodeFactory.instance.textNode(myToken.serialize()); + } + + protected void callPermissionsJwtAPI(Ga4ghClaimRepository repo, + Map uriVariables, + ArrayNode passport, + Set linkedIdentities) + { + JsonNode response = callHttpJsonAPI(repo, uriVariables); + if (response != null) { + JsonNode visas = response.path(GA4GH_CLAIM); + if (visas.isArray()) { + for (JsonNode visaNode : visas) { + if (visaNode.isTextual()) { + Ga4ghPassportVisa visa = Ga4ghUtils.parseAndVerifyVisa(visaNode.asText(), SIGNERS, REMOTE_JWK_SETS, MAPPER); + if (visa.isVerified()) { + log.debug("Adding a visa to passport: {}", visa); + passport.add(passport.textNode(visa.getJwt())); + linkedIdentities.add(visa.getLinkedIdentity()); + } else { + log.warn("Skipping visa: {}", visa); + } + } else { + log.warn("Element of {} is not a String: {}", GA4GH_CLAIM, visaNode); + } + } + } else { + log.warn("{} is not an array in {}", GA4GH_CLAIM, response); + } + } + } + + @SuppressWarnings("Duplicates") + private static JsonNode callHttpJsonAPI(Ga4ghClaimRepository repo, Map uriVariables) { + //get permissions data + try { + JsonNode result; + try { + if (log.isDebugEnabled()) { + log.debug("Calling Permissions API at {}", repo.getRestTemplate().getUriTemplateHandler().expand(repo.getActionURL(), uriVariables)); + } + result = repo.getRestTemplate().getForObject(repo.getActionURL(), JsonNode.class, uriVariables); + } catch (HttpClientErrorException ex) { + MediaType contentType = ex.getResponseHeaders().getContentType(); + String body = ex.getResponseBodyAsString(); + log.error("HTTP ERROR: {}, URL: {}, Content-Type: {}", ex.getRawStatusCode(), + repo.getActionURL(), contentType); + if (ex.getRawStatusCode() == 404) { + log.warn("Got status 404 from Permissions endpoint {}, ELIXIR AAI user is not linked to user at Permissions API", + repo.getActionURL()); + return null; + } + if ("json".equals(contentType.getSubtype())) { + try { + log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText()); + } catch (IOException e) { + log.error("cannot parse error message from JSON", e); + } + } else { + log.error("cannot make REST call, exception: {} message: {}", ex.getClass().getName(), ex.getMessage()); + } + return null; + } + log.debug("Permissions API response: {}", result); + return result; + } catch (Exception ex) { + log.error("Cannot get dataset permissions", ex); + } + return null; + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghPassportVisa.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghPassportVisa.java new file mode 100644 index 000000000..e39899bf3 --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghPassportVisa.java @@ -0,0 +1,60 @@ +package cz.muni.ics.oidc.server.ga4gh; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode +public class Ga4ghPassportVisa { + + public static final String GA4GH_VISA_V1 = "ga4gh_visa_v1"; + + public static final String TYPE_AFFILIATION_AND_ROLE = "AffiliationAndRole"; + public static final String TYPE_ACCEPTED_TERMS_AND_POLICIES = "AcceptedTermsAndPolicies"; + public static final String TYPE_RESEARCHER_STATUS = "ResearcherStatus"; + public static final String TYPE_LINKED_IDENTITIES = "LinkedIdentities"; + + public static final String BY_SYSTEM = "system"; + public static final String BY_SO = "so"; + public static final String BY_PEER = "peer"; + public static final String BY_SELF = "self"; + + public static final String SUB = "sub"; + public static final String EXP = "exp"; + public static final String ISS = "iss"; + public static final String TYPE = "type"; + public static final String ASSERTED = "asserted"; + public static final String VALUE = "value"; + public static final String SOURCE = "source"; + public static final String BY = "by"; + public static final String CONDITION = "condition"; + + private boolean verified = false; + private String linkedIdentity; + private String sub; + private String iss; + private String type; + private String value; + + @ToString.Exclude + private String signer; + + @ToString.Exclude + private String jwt; + + @ToString.Exclude + private String prettyPayload; + + public Ga4ghPassportVisa(String jwt) { + this.jwt = jwt; + } + + public String getPrettyString() { + return prettyPayload + ", signed by " + signer; + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghUtils.java new file mode 100644 index 000000000..11c0948e6 --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/ga4gh/Ga4ghUtils.java @@ -0,0 +1,222 @@ +package cz.muni.ics.oidc.server.ga4gh; + +import static cz.muni.ics.oidc.server.ga4gh.Ga4ghPassportAndVisaClaimSource.GA4GH_CLAIM; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; +import com.nimbusds.jose.Payload; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import cz.muni.ics.oidc.server.AddHeaderInterceptor; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.InterceptingClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Slf4j +public class Ga4ghUtils { + + public static final String CONF_KEY_REPOS = "repos"; + public static final String CONF_KEY_NAME = "name"; + public static final String CONF_KEY_URL = "url"; + public static final String CONF_KEY_HEADERS = "headers"; + public static final String CONF_KEY_HEADER = "header"; + public static final String CONF_KEY_VALUE = "value"; + public static final String CONF_KEY_SIGNERS = "signers"; + public static final String CONF_KEY_JWKS = "jwks"; + + public static void parseConfigFile(String file, + List claimRepositories, + Map> remoteJwkSets, + Map signers) + { + YAMLMapper mapper = new YAMLMapper(); + try { + JsonNode root = mapper.readValue(new File(file), JsonNode.class); + // prepare claim repositories + for (JsonNode repo : root.path(CONF_KEY_REPOS)) { + initializeRepo(repo, claimRepositories); + } + // prepare claim signers + for (JsonNode signer : root.path(CONF_KEY_SIGNERS)) { + initializeSigner(signer, signers, remoteJwkSets); + } + } catch (IOException ex) { + log.error("cannot read GA4GH config file", ex); + } + } + + public static Ga4ghPassportVisa parseAndVerifyVisa(String jwtString, + Map signers, + Map> remoteJwkSets, + ObjectMapper mapper) + { + Ga4ghPassportVisa visa = new Ga4ghPassportVisa(jwtString); + try { + SignedJWT signedJWT = (SignedJWT) JWTParser.parse(jwtString); + URI jku = signedJWT.getHeader().getJWKURL(); + if (jku == null) { + log.error("JKU is missing in JWT header"); + return visa; + } + visa.setSigner(signers.get(jku)); + RemoteJWKSet remoteJWKSet = remoteJwkSets.get(jku); + if (remoteJWKSet == null) { + log.error("JKU '{}' is not among trusted key sets", jku); + return visa; + } + List keys = remoteJWKSet.get(new JWKSelector( + new JWKMatcher.Builder().keyID(signedJWT.getHeader().getKeyID()).build()), null); + RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) keys.get(0)).toRSAPublicKey()); + visa.setVerified(signedJWT.verify(verifier)); + if (visa.isVerified()) { + Ga4ghUtils.processPayload(mapper, visa, signedJWT.getPayload()); + } + } catch (Exception ex) { + log.error("Visa '{}' cannot be parsed and verified", jwtString, ex); + } + return visa; + } + + public static void processPayload(ObjectMapper mapper, Ga4ghPassportVisa visa, Payload payload) + throws IOException + { + JsonNode doc = mapper.readValue(payload.toString(), JsonNode.class); + checkVisaKey(visa, doc, Ga4ghPassportVisa.SUB); + checkVisaKey(visa, doc, Ga4ghPassportVisa.EXP); + checkVisaKey(visa, doc, Ga4ghPassportVisa.ISS); + JsonNode visa_v1 = doc.path(Ga4ghPassportVisa.GA4GH_VISA_V1); + if (visa_v1.isMissingNode() || visa_v1.isNull() || visa_v1.isEmpty()) { + log.warn("Nothing available in '{}', considering visa as not verified", Ga4ghPassportVisa.GA4GH_VISA_V1); + visa.setVerified(false); + return; + } + checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.TYPE); + checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.ASSERTED); + checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.VALUE); + checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.SOURCE); + checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.BY); + if (!visa.isVerified()) { + return; + } + long exp = doc.get(Ga4ghPassportVisa.EXP).asLong(); + if (exp < Instant.now().getEpochSecond()) { + log.warn("visa expired on {}", isoDateTime(exp)); + visa.setVerified(false); + return; + } + visa.setLinkedIdentity(URLEncoder.encode(doc.get(Ga4ghPassportVisa.SUB).asText(), "utf-8") + + ',' + URLEncoder.encode(doc.get(Ga4ghPassportVisa.ISS).asText(), "utf-8")); + visa.setPrettyPayload( + visa_v1.get(Ga4ghPassportVisa.TYPE).asText() + ": '" + + visa_v1.get(Ga4ghPassportVisa.VALUE).asText() + "' asserted at '" + + isoDate(visa_v1.get(Ga4ghPassportVisa.ASSERTED).asLong()) + '\'' + ); + } + + public static long getOneYearExpires(long asserted) { + return getExpires(asserted, 1L); + } + + public static long getExpires(long asserted, long addYears) { + return Instant.ofEpochSecond(asserted).atZone(ZoneId.systemDefault()).plusYears(addYears).toEpochSecond(); + } + + private static void initializeSigner(JsonNode signer, + Map signers, + Map> remoteJwkSets) + { + String name = signer.path(CONF_KEY_NAME).asText(); + String jwks = signer.path(CONF_KEY_JWKS).asText(); + try { + URL jku = new URL(jwks); + remoteJwkSets.put(jku.toURI(), new RemoteJWKSet<>(jku)); + signers.put(jku.toURI(), name); + log.info("JWKS Signer '{}' added with keys '{}'", name, jwks); + } catch (MalformedURLException | URISyntaxException e) { + log.error("cannot add to RemoteJWKSet map: '{}' -> '{}'", name, jwks, e); + } + } + + private static void initializeRepo(JsonNode repo, List claimRepositories) { + String name = repo.path(CONF_KEY_NAME).asText(); + String actionURL = repo.path(CONF_KEY_URL).asText(); + JsonNode headers = repo.path(CONF_KEY_HEADERS); + Map headersWithValues = new HashMap<>(); + for (JsonNode header: headers) { + headersWithValues.put(header.path(CONF_KEY_HEADER).asText(), header.path(CONF_KEY_VALUE).asText()); + } + if (actionURL == null || headersWithValues.isEmpty()) { + log.error("claim repository '{}' not defined with url|auth_header|auth_value", repo); + return; + } + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setRequestFactory( + new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), + headersWithValues.entrySet() + .stream() + .map(e -> new AddHeaderInterceptor(e.getKey(), e.getValue())) + .collect(Collectors.toList())) + ); + claimRepositories.add(new Ga4ghClaimRepository(name, actionURL, restTemplate)); + log.info("GA4GH Claims Repository '{}' configured at '{}'", name, actionURL); + } + + private static void checkVisaKey(Ga4ghPassportVisa visa, JsonNode jsonNode, String key) { + if (jsonNode.path(key).isMissingNode()) { + log.warn("Key '{}' is missing in the Visa, therefore cannot be verified", key); + visa.setVerified(false); + } else { + switch (key) { + case Ga4ghPassportVisa.SUB: + visa.setSub(jsonNode.path(key).asText()); + break; + case Ga4ghPassportVisa.ISS: + visa.setIss(jsonNode.path(key).asText()); + break; + case Ga4ghPassportVisa.TYPE: + visa.setType(jsonNode.path(key).asText()); + break; + case Ga4ghPassportVisa.VALUE: + visa.setValue(jsonNode.path(key).asText()); + break; + } + } + } + + private static String isoDate(long linuxTime) { + return isoFormat(linuxTime, DateTimeFormatter.ISO_LOCAL_DATE); + } + + private static String isoDateTime(long linuxTime) { + return isoFormat(linuxTime, DateTimeFormatter.ISO_DATE_TIME); + } + + private static String isoFormat(long linuxTime, DateTimeFormatter formatter) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault()); + return formatter.format(zdt); + } + +}