From 7e1c218cb1740bd8c36f08c901e640bbeb1ed5df Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Wed, 14 Jan 2026 12:12:29 +0100 Subject: [PATCH 1/5] trying auto logout from unilogin --- .../fafnir/sso/provider/UniLoginProvider.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java index 132952e..eb24fdf 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java @@ -144,6 +144,12 @@ public AuthenticationResult callback(UniloginTokenCredentials data, HttpSession accessToken = getAccessToken(accessCode, UL_CLIENT_ID, UL_SECRET, UL_REDIRECT_URL, CODE_VERIFIER, OID_BASE_URL); + // Store ID token in session for use in logout (id_token_hint can help skip confirmation page) + if (accessToken.getId_token() != null && session != null) { + session.setAttribute("id_token", accessToken.getId_token()); + log.debug("Stored ID token in session for logout"); + } + IntrospectionToken intro; intro = getIntrospectToken(accessToken.getAccess_token(), UL_CLIENT_ID, UL_SECRET, OID_BASE_URL); @@ -399,10 +405,19 @@ public AuthenticationResult callbackWithInstitution(String userId, String instit log.debug("Stored one-time token in session for post-logout retrieval"); } + // Retrieve ID token from session to use as id_token_hint (may help skip confirmation page) + String idTokenHint = null; + if (session != null) { + idTokenHint = (String) session.getAttribute("id_token"); + if (idTokenHint != null) { + log.debug("Using ID token from session as id_token_hint for logout"); + } + } + // Use base URL only (no token in URL) to avoid UniLogin redirect URI validation issues String logoutUrl = getLogoutUrl( fafnirConf.getUrl() + "/unilogin/logout-complete", - null // We don't have id_token_hint here, but it's optional + idTokenHint // Pass ID token hint to potentially skip confirmation page ); log.info("Authentication successful, redirecting to UniLogin logout to end UniLogin session"); From 900d080812a7d2aed4951d1456a1bd8281f71838 Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Mon, 6 Jul 2026 12:54:34 +0200 Subject: [PATCH 2/5] Improve UniLogin flow diagnostics and resilience Introduce detailed logging for the UniLogin authentication process, providing better visibility into the flow and failure points. Implement institution deduplication and a fallback mechanism for employee roles, ensuring users without explicit institution affiliations can still authenticate successfully. This addresses inconsistencies in UniLogin's UserInfo and SOAP responses. ``` --- .../fafnir/sso/provider/UniLoginProvider.java | 264 ++++++++++++++++-- .../unilogin/InstitutionTilknytning.java | 3 + .../provider/unilogin/UniLoginFlowTrace.java | 114 ++++++++ .../provider/unilogin/UserInfoResponse.java | 2 + .../controller/UniLoginController.java | 53 +++- .../sso/provider/UniLoginRoleMappingTest.java | 115 ++++++++ .../unilogin/UniLoginFlowTraceTest.java | 44 +++ 7 files changed, 554 insertions(+), 41 deletions(-) create mode 100644 sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTrace.java create mode 100644 sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java create mode 100644 sso/src/test/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTraceTest.java diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java index eb24fdf..413610c 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java @@ -155,10 +155,12 @@ public AuthenticationResult callback(UniloginTokenCredentials data, HttpSession intro = getIntrospectToken(accessToken.getAccess_token(), UL_CLIENT_ID, UL_SECRET, OID_BASE_URL); if (intro == null) { + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.intro_failed, "-", session, null); return AuthenticationResult.failure(FailureReason.AUTHENTICATION_FAILED); } var userId = intro.getUniid(); + UniLoginFlowTrace.rememberUniid(session, userId); // Store aktørgruppe from introspection token for role extraction if (intro.getAktoer_gruppe() != null) { @@ -181,21 +183,49 @@ public AuthenticationResult callback(UniloginTokenCredentials data, HttpSession } catch (Exception e) { log.warn("Failed to retrieve extended user information from UserInfo endpoint, falling back to web services: {}", e.getMessage()); } - + + boolean userInfoHadInstitutions = userInfo != null + && userInfo.getInstBrugere() != null + && !userInfo.getInstBrugere().isEmpty(); + int userInfoInstCount = userInfoHadInstitutions ? userInfo.getInstBrugere().size() : 0; + UniLoginFlowTrace.log("callback", + userInfoHadInstitutions ? UniLoginFlowTrace.Branch.userinfo_ok : UniLoginFlowTrace.Branch.userinfo_empty, + userId, session, Map.of("userinfo_inst_count", userInfoInstCount)); + // Fallback to web services if UserInfo failed or returned no data if (institutions == null || institutions.isEmpty()) { log.debug("Using deprecated web service call to get institution list for user: {}", userId); institutions = getInstitutionList(userId); + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.soap_fallback, userId, session, + Map.of("soap_inst_count", institutions.size())); } + int instCountBeforeDedup = institutions.size(); + institutions = deduplicateInstitutions(institutions); + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.dedup_applied, userId, session, Map.of( + "inst_count_before", instCountBeforeDedup, + "inst_count_after", institutions.size(), + "instnr_list", UniLoginFlowTrace.instnrList(institutions))); + if (institutions.isEmpty()) { + if (canIssueJwtWithoutInstitution(session)) { + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.direct_jwt_no_org, userId, session, + Map.of("inst_count", 0)); + return callbackWithoutInstitution(userId, session); + } + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.error_no_institutions, userId, session, + Map.of("inst_count", 0)); return AuthenticationResult.failure(FailureReason.CONNECTION_FAILED); } else if (institutions.size() == 1) { Institution inst = institutions.get(0); log.debug("Single institution found - id: {}, name: {}", inst.id, inst.name); + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.direct_jwt, userId, session, + Map.of("institution_id", inst.id, "inst_count", 1)); return callbackWithInstitution(userId, inst.id, inst.name, session); } else { String chooseInstitutionUrl = uniloginHelper.getChooseInstitutionUrl(userId); + UniLoginFlowTrace.log("callback", UniLoginFlowTrace.Branch.org_picker, userId, session, + Map.of("inst_count", institutions.size(), "instnr_list", UniLoginFlowTrace.instnrList(institutions))); return AuthenticationResult.redirect(chooseInstitutionUrl); } } @@ -222,12 +252,17 @@ public List getInstitutionListFromSession(String userId, HttpSessio userId, userInfo.getInstBrugere().size()); List institutions = convertUserInfoToInstitutions(userInfo, session); log.info("Converted {} institution(s) from UserInfo", institutions.size()); + UniLoginFlowTrace.log("getOrg_data", UniLoginFlowTrace.Branch.userinfo_ok, userId, session, + Map.of("inst_count", institutions.size(), "instnr_list", UniLoginFlowTrace.instnrList(institutions))); return institutions; } } // Fallback to deprecated web service log.warn("UserInfo not available in session, falling back to deprecated web service for user: {}", userId); - return getInstitutionList(userId); + List soapInstitutions = deduplicateInstitutions(getInstitutionList(userId)); + UniLoginFlowTrace.log("getOrg_data", UniLoginFlowTrace.Branch.soap_fallback, userId, session, + Map.of("soap_inst_count", soapInstitutions.size(), "instnr_list", UniLoginFlowTrace.instnrList(soapInstitutions))); + return soapInstitutions; } /** @@ -305,6 +340,36 @@ public String getFailureUrl(FailureReason reason) { } + public boolean canIssueJwtWithoutInstitution(HttpSession session) { + if (session == null) { + return false; + } + return isEmployeeAktorgruppe((String) session.getAttribute("aktoer_gruppe")); + } + + /** + * Issues a JWT without org_id/org_name when the user is an employee but has no institution affiliations. + * Find2Learn can prompt for institution via /license. + */ + public AuthenticationResult callbackWithoutInstitution(String userId, HttpSession session) { + Set roles = getUserRolesFromAktorgruppe(session); + String roleSource = roles.isEmpty() ? "none" : "aktoer_gruppe"; + + String aktoerGruppe = session != null ? (String) session.getAttribute("aktoer_gruppe") : null; + boolean hadCompatibleRole = hasFind2LearnCompatibleEmployeeRole(roles); + roles = ensureEmployeeLoginRole(roles, aktoerGruppe); + if (!hadCompatibleRole && hasFind2LearnCompatibleEmployeeRole(roles)) { + roleSource = "fallback_employee"; + } + + if (roles.isEmpty()) { + log.warn("Cannot issue JWT without institution for user {}: no roles available", userId); + return AuthenticationResult.failure(FailureReason.CONNECTION_FAILED); + } + + return issueJwtAndRedirectToLogout(userId, session, null, null, roles, roleSource); + } + public AuthenticationResult callbackWithInstitution(String userId, String institutionId) { return callbackWithInstitution(userId, institutionId, null, null); } @@ -314,8 +379,6 @@ public AuthenticationResult callbackWithInstitution(String userId, String instit } public AuthenticationResult callbackWithInstitution(String userId, String institutionId, String institutionName, HttpSession session) { - var name = getUserFullNameFromId(userId); - // Try to get institution name from parameter, then from UserInfo in session, otherwise fallback to web service String orgName = (institutionName != null && !institutionName.isEmpty()) ? institutionName : null; @@ -357,17 +420,53 @@ public AuthenticationResult callbackWithInstitution(String userId, String instit // Try to get roles from UserInfo, fallback to aktørgruppe, then web service Set roles = getUserRolesFromUserInfo(userId, institutionId, session); - if (roles.isEmpty()) { - // Try to get role from aktørgruppe (actor group) from introspection token + String roleSource = "none"; + if (!roles.isEmpty()) { + roleSource = "userinfo"; + } else { roles = getUserRolesFromAktorgruppe(session); if (!roles.isEmpty()) { + roleSource = "aktoer_gruppe"; log.debug("Using aktørgruppe from introspection token for user: {} at institution: {}", userId, institutionId); + } else { + log.debug("Using deprecated web service call to get user roles for user: {} at institution: {}", userId, institutionId); + roles = this.getUserRoles(institutionId, userId); + if (!roles.isEmpty()) { + roleSource = "soap"; + } } } - if (roles.isEmpty()) { - log.debug("Using deprecated web service call to get user roles for user: {} at institution: {}", userId, institutionId); - roles = this.getUserRoles(institutionId, userId); + + String aktoerGruppe = session != null ? (String) session.getAttribute("aktoer_gruppe") : null; + boolean hadCompatibleRole = hasFind2LearnCompatibleEmployeeRole(roles); + roles = ensureEmployeeLoginRole(roles, aktoerGruppe); + if (!hadCompatibleRole && hasFind2LearnCompatibleEmployeeRole(roles)) { + roleSource = "none".equals(roleSource) ? "fallback_employee" : roleSource + "+fallback_employee"; + } + + return issueJwtAndRedirectToLogout(userId, session, institutionId, finalOrgName, roles, roleSource); + } + + private AuthenticationResult issueJwtAndRedirectToLogout( + String userId, + HttpSession session, + String institutionId, + String institutionName, + Set roles, + String roleSource) { + var name = getUserFullNameFromId(userId); + List roleStrings = roles.stream().map(UserRole::toString).collect(Collectors.toList()); + log.info("JWT roles for user {} at institution {}: {}", userId, institutionId != null ? institutionId : "-", roleStrings); + + Map jwtTraceFields = new LinkedHashMap<>(); + jwtTraceFields.put("roles", roleStrings); + jwtTraceFields.put("role_source", roleSource); + if (institutionId != null) { + jwtTraceFields.put("institution_id", institutionId); + } else { + jwtTraceFields.put("org_omitted", true); } + UniLoginFlowTrace.log("jwt", UniLoginFlowTrace.Branch.jwt_built, userId, session, jwtTraceFields); var subjectActual = UserData.builder() .subject(userId) @@ -375,7 +474,7 @@ public AuthenticationResult callbackWithInstitution(String userId, String instit .build(); var orgActual = OrganisationData.builder() .organisationId(institutionId) - .organisationName(finalOrgName) + .organisationName(institutionName) .build(); var claimsActual = ClaimData.builder() .claims(roles.stream() @@ -389,23 +488,18 @@ public AuthenticationResult callbackWithInstitution(String userId, String instit .organisationSupport(OrganisationSupport.NATIVE) .inputs(List.of()) .build()); - - // Generate a one-time token for secure JWT retrieval after logout + String oneTimeToken = generateOneTimeToken(); - - // Store JWT in cache with short TTL (5 minutes) jwtCache.put(oneTimeToken, new CacheEntry(jwt, CACHE_TTL_SECONDS)); log.debug("Stored JWT in cache with one-time token (expires in {} seconds)", CACHE_TTL_SECONDS); - - // Instead of returning success directly, redirect to logout first - // This ensures the user is logged out from UniLogin after we get the data - // Store token in session to retrieve after logout (cookie approach would require HttpServletResponse) + UniLoginFlowTrace.log("jwt", UniLoginFlowTrace.Branch.jwt_cached, userId, session, + Map.of("cache_token_prefix", UniLoginFlowTrace.tokenPrefix(oneTimeToken))); + if (session != null) { session.setAttribute("logout_token", oneTimeToken); log.debug("Stored one-time token in session for post-logout retrieval"); } - - // Retrieve ID token from session to use as id_token_hint (may help skip confirmation page) + String idTokenHint = null; if (session != null) { idTokenHint = (String) session.getAttribute("id_token"); @@ -413,14 +507,14 @@ public AuthenticationResult callbackWithInstitution(String userId, String instit log.debug("Using ID token from session as id_token_hint for logout"); } } - - // Use base URL only (no token in URL) to avoid UniLogin redirect URI validation issues + String logoutUrl = getLogoutUrl( fafnirConf.getUrl() + "/unilogin/logout-complete", - idTokenHint // Pass ID token hint to potentially skip confirmation page + idTokenHint ); - + log.info("Authentication successful, redirecting to UniLogin logout to end UniLogin session"); + UniLoginFlowTrace.log("jwt", UniLoginFlowTrace.Branch.logout_redirect, userId, session, Map.of()); return AuthenticationResult.redirect(logoutUrl); } @@ -444,21 +538,32 @@ private String generateOneTimeToken() { * @return The JWT if found and not expired, null otherwise */ public String retrieveJwtFromCache(String token) { + return retrieveJwtFromCache(token, null, null); + } + + public String retrieveJwtFromCache(String token, String uniid, HttpSession session) { if (token == null || token.isEmpty()) { return null; } - + + String tokenPrefix = UniLoginFlowTrace.tokenPrefix(token); CacheEntry entry = jwtCache.remove(token); if (entry == null) { - log.warn("One-time token not found or already used: {}", token.substring(0, Math.min(8, token.length())) + "..."); + log.warn("One-time token not found or already used: {}", tokenPrefix + "..."); + UniLoginFlowTrace.log("cache", UniLoginFlowTrace.Branch.logout_complete_cache_miss, + uniid != null ? uniid : UniLoginFlowTrace.uniidFromSession(session), session, + Map.of("cache_token_prefix", tokenPrefix, "reason", "not_found_or_used")); return null; } - + if (entry.isExpired(System.currentTimeMillis())) { - log.warn("One-time token expired: {}", token.substring(0, Math.min(8, token.length())) + "..."); + log.warn("One-time token expired: {}", tokenPrefix + "..."); + UniLoginFlowTrace.log("cache", UniLoginFlowTrace.Branch.logout_complete_cache_miss, + uniid != null ? uniid : UniLoginFlowTrace.uniidFromSession(session), session, + Map.of("cache_token_prefix", tokenPrefix, "reason", "expired")); return null; } - + log.debug("Successfully retrieved JWT from cache using one-time token"); return entry.getJwt(); } @@ -680,7 +785,8 @@ private List convertUserInfoToInstitutions(UserInfoResponse userInf }) .collect(Collectors.toList()); - log.info("Successfully converted {} Institution object(s)", institutions.size()); + institutions = deduplicateInstitutions(institutions); + log.info("Successfully converted {} Institution object(s) after deduplication", institutions.size()); return institutions; } @@ -736,10 +842,13 @@ private Set getUserRolesFromUserInfo(String userId, String institution // Format 1: Direct "roller" array with strings like "PÆDAGOG@EMPLOYEE" if (affiliation.getRoller() != null && !affiliation.getRoller().isEmpty()) { + String aktoerGruppe = (String) session.getAttribute("aktoer_gruppe"); for (String roleString : affiliation.getRoller()) { UserRole role = parseRoleString(roleString); if (role != null) { roles.add(role); + } else if (isEmployeeAktorgruppe(aktoerGruppe)) { + roles.add(UserRole.builder().name(roleString).type("EMPLOYEE").build()); } } } @@ -799,7 +908,11 @@ private UserRole parseRoleString(String roleString) { String name = roleString.substring(0, atIndex); String type = roleString.substring(atIndex + 1); - + + if ("ANSAT".equalsIgnoreCase(type)) { + type = "EMPLOYEE"; + } + return UserRole.builder() .name(name) .type(type) @@ -847,6 +960,13 @@ private Set getUserRolesFromAktorgruppe(HttpSession session) { .build()); log.debug("Extracted role from aktørgruppe: {} -> EMPLOYEE", aktoerGruppe); break; + case "MEDARBEJDER": + roles.add(UserRole.builder() + .name("Medarbejder") + .type("EMPLOYEE") + .build()); + log.debug("Extracted role from aktørgruppe: {} -> Medarbejder@EMPLOYEE", aktoerGruppe); + break; case "EMP_EXTERNAL": case "EKSTERN": roles.add(UserRole.builder() @@ -868,6 +988,88 @@ private Set getUserRolesFromAktorgruppe(HttpSession session) { return roles; } + /** + * Ensures the role set contains at least one employee role that Find2Learn accepts for login. + * Adds Medarbejder@EMPLOYEE when aktoer_gruppe indicates employee but only unusable roles were parsed. + */ + static Set ensureEmployeeLoginRole(Set roles, String aktoerGruppe) { + Set result = roles != null ? new HashSet<>(roles) : new HashSet<>(); + if (hasFind2LearnCompatibleEmployeeRole(result)) { + return result; + } + if (isEmployeeAktorgruppe(aktoerGruppe)) { + result.add(UserRole.builder().name("Medarbejder").type("EMPLOYEE").build()); + log.info("Added Medarbejder@EMPLOYEE fallback role based on aktoer_gruppe: {}", aktoerGruppe); + } + return result; + } + + static boolean hasFind2LearnCompatibleEmployeeRole(Set roles) { + if (roles == null || roles.isEmpty()) { + return false; + } + return roles.stream().anyMatch(UniLoginProvider::isFind2LearnCompatibleEmployeeRole); + } + + static boolean isFind2LearnCompatibleEmployeeRole(UserRole role) { + if (role == null || role.getType() == null) { + return false; + } + String type = role.getType(); + String name = role.getName(); + if ("EMPLOYEE".equalsIgnoreCase(type) || "Medarbejder".equalsIgnoreCase(type)) { + return true; + } + return name != null && "Medarbejder".equalsIgnoreCase(name) && "UNKNOWN".equalsIgnoreCase(type); + } + + static boolean isEmployeeAktorgruppe(String aktoerGruppe) { + if (aktoerGruppe == null || aktoerGruppe.isEmpty()) { + return false; + } + return switch (aktoerGruppe.toUpperCase()) { + case "MEDARBEJDER", "EMPLOYEE", "ANSAT" -> true; + default -> false; + }; + } + + static List deduplicateInstitutions(List institutions) { + if (institutions == null || institutions.isEmpty()) { + return Collections.emptyList(); + } + if (institutions.size() == 1) { + return institutions; + } + + Map byId = new LinkedHashMap<>(); + for (Institution inst : institutions) { + if (inst.id == null || inst.id.isBlank()) { + continue; + } + byId.merge(inst.id, inst, (existing, incoming) -> { + List mergedRoles = new ArrayList<>( + existing.roles != null ? existing.roles : Collections.emptyList()); + if (incoming.roles != null) { + for (String role : incoming.roles) { + if (!mergedRoles.contains(role)) { + mergedRoles.add(role); + } + } + } + String name = existing.name != null && !existing.name.isEmpty() + ? existing.name + : incoming.name; + return new Institution(existing.id, name, mergedRoles); + }); + } + + if (byId.size() < institutions.size()) { + log.info("Deduplicated {} institution affiliation(s) to {} unique instnr(s)", + institutions.size(), byId.size()); + } + return new ArrayList<>(byId.values()); + } + ObjectMapper getObjectMapper() { return new ObjectMapper(); } diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/InstitutionTilknytning.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/InstitutionTilknytning.java index b6caf87..b6b85d1 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/InstitutionTilknytning.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/InstitutionTilknytning.java @@ -1,5 +1,6 @@ package dk.acto.fafnir.sso.provider.unilogin; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -14,9 +15,11 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class InstitutionTilknytning { @JsonProperty("instnr") + @JsonAlias({"inTnr"}) public String inTnr; // Institution number @JsonProperty("instnavn") + @JsonAlias({"inTnavn"}) public String inTnavn; // Institution name // Fields for roles, as they might appear in UserInfo response diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTrace.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTrace.java new file mode 100644 index 0000000..e863721 --- /dev/null +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTrace.java @@ -0,0 +1,114 @@ +package dk.acto.fafnir.sso.provider.unilogin; + +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +public final class UniLoginFlowTrace { + + public static final String TRACE_UNIID_SESSION_KEY = "trace_uniid"; + + public enum Branch { + intro_failed, + userinfo_ok, + userinfo_empty, + soap_fallback, + dedup_applied, + error_no_institutions, + direct_jwt, + direct_jwt_no_org, + org_picker, + org_empty_list, + org_render, + org_post, + jwt_built, + jwt_cached, + logout_redirect, + logout_complete_no_token, + logout_complete_cache_miss, + logout_complete_success + } + + private UniLoginFlowTrace() { + } + + public static void log(String step, Branch branch, String uniid, HttpSession session, Map extra) { + StringBuilder message = new StringBuilder("UniLogin trace: step=") + .append(step) + .append(" branch=") + .append(branch) + .append(" uniid=") + .append(uniid != null ? uniid : "-") + .append(" session=") + .append(sessionId(session)); + + if (session != null) { + Object aktoerGruppe = session.getAttribute("aktoer_gruppe"); + if (aktoerGruppe != null) { + message.append(" aktoer_gruppe=").append(aktoerGruppe); + } + } + + if (extra != null) { + for (Map.Entry entry : extra.entrySet()) { + message.append(' ').append(entry.getKey()).append('=').append(entry.getValue()); + } + } + + log.info(message.toString()); + } + + public static String sessionId(HttpSession session) { + if (session == null || session.getId() == null) { + return "-"; + } + String id = session.getId(); + return id.length() <= 8 ? id : id.substring(0, 8); + } + + public static boolean hasUserInfoInSession(HttpSession session) { + if (session == null) { + return false; + } + UserInfoResponse userInfo = (UserInfoResponse) session.getAttribute("userInfo"); + return userInfo != null + && userInfo.getInstBrugere() != null + && !userInfo.getInstBrugere().isEmpty(); + } + + public static List instnrList(List institutions) { + if (institutions == null || institutions.isEmpty()) { + return Collections.emptyList(); + } + return institutions.stream() + .map(inst -> inst.id) + .filter(id -> id != null && !id.isBlank()) + .collect(Collectors.toList()); + } + + public static String tokenPrefix(String token) { + if (token == null || token.isEmpty()) { + return "-"; + } + return token.substring(0, Math.min(8, token.length())); + } + + public static void rememberUniid(HttpSession session, String uniid) { + if (session != null && uniid != null) { + session.setAttribute(TRACE_UNIID_SESSION_KEY, uniid); + } + } + + public static String uniidFromSession(HttpSession session) { + if (session == null) { + return null; + } + Object value = session.getAttribute(TRACE_UNIID_SESSION_KEY); + return value != null ? value.toString() : null; + } +} diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UserInfoResponse.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UserInfoResponse.java index ca415a4..2bd7f59 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UserInfoResponse.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/unilogin/UserInfoResponse.java @@ -1,5 +1,6 @@ package dk.acto.fafnir.sso.provider.unilogin; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; @@ -14,5 +15,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class UserInfoResponse { @JsonProperty("inst_brugere") + @JsonAlias({"instbrugere", "dk:unilogin:instbrugere"}) public List instBrugere; } diff --git a/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginController.java b/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginController.java index 37485e4..25bc862 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginController.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/service/controller/UniLoginController.java @@ -4,6 +4,7 @@ import dk.acto.fafnir.api.model.FailureReason; import dk.acto.fafnir.api.model.conf.FafnirConf; import dk.acto.fafnir.sso.provider.UniLoginProvider; +import dk.acto.fafnir.sso.provider.unilogin.UniLoginFlowTrace; import dk.acto.fafnir.sso.provider.unilogin.UniloginTokenCredentials; import io.vavr.control.Try; import jakarta.annotation.PostConstruct; @@ -20,6 +21,7 @@ import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.Collections; +import java.util.Map; @Controller @Slf4j @@ -69,11 +71,27 @@ public String getOrg(@RequestParam String user, Model model, HttpServletResponse } if (institutionList.isEmpty()) { + if (provider.canIssueJwtWithoutInstitution(session)) { + log.info("No institutions for employee user {}, issuing JWT without org", user); + UniLoginFlowTrace.log("getOrg", UniLoginFlowTrace.Branch.direct_jwt_no_org, user, session, Map.of( + "inst_count", 0, + "session_has_userInfo", UniLoginFlowTrace.hasUserInfoInSession(session))); + AuthenticationResult result = provider.callbackWithoutInstitution(user, session); + response.sendRedirect(result.getUrl(uniloginConf)); + return null; + } log.warn("No institutions found for user: {}, redirecting to failure URL", user); + UniLoginFlowTrace.log("getOrg", UniLoginFlowTrace.Branch.org_empty_list, user, session, Map.of( + "inst_count", 0, + "session_has_userInfo", UniLoginFlowTrace.hasUserInfoInSession(session))); response.sendRedirect(provider.getFailureUrl(FailureReason.CONNECTION_FAILED)); return null; } else { log.info("Adding {} institution(s) to model for template rendering", institutionList.size()); + UniLoginFlowTrace.log("getOrg", UniLoginFlowTrace.Branch.org_render, user, session, Map.of( + "inst_count", institutionList.size(), + "session_has_userInfo", UniLoginFlowTrace.hasUserInfoInSession(session), + "instnr_list", UniLoginFlowTrace.instnrList(institutionList))); model.addAttribute("user", user); model.addAttribute("institutionList", institutionList); log.info("Model attributes set - user: {}, institutionList size: {}", user, institutionList.size()); @@ -86,6 +104,8 @@ public String getOrg(@RequestParam String user, Model model, HttpServletResponse @ResponseBody public RedirectView postOrg(@RequestParam String user, @RequestParam String institution, HttpSession session, HttpServletResponse response) { + UniLoginFlowTrace.log("org_post", UniLoginFlowTrace.Branch.org_post, user, session, + Map.of("institution", institution)); AuthenticationResult result = provider.callbackWithInstitution(user, institution, null, session); String redirectUrl = result.getUrl(uniloginConf); @@ -97,7 +117,8 @@ public RedirectView postOrg(@RequestParam String user, @RequestParam String inst cookie.setPath("/"); cookie.setMaxAge(300); // 5 minutes cookie.setHttpOnly(true); - cookie.setSecure(true); // Use secure cookies in production + String fafnirUrl = System.getenv("FAFNIR_URL"); + cookie.setSecure(fafnirUrl != null && fafnirUrl.startsWith("https://")); response.addCookie(cookie); log.debug("Set logout token cookie as backup"); } @@ -117,40 +138,52 @@ public RedirectView postOrg(@RequestParam String user, @RequestParam String inst @GetMapping("logout-complete") public RedirectView logoutComplete(HttpSession session, jakarta.servlet.http.HttpServletRequest request) { log.info("Logout complete callback - retrieving JWT from cache"); - + String uniid = UniLoginFlowTrace.uniidFromSession(session); + // Try to get token from session first String token = session != null ? (String) session.getAttribute("logout_token") : null; - + String tokenSource = token != null && !token.isEmpty() ? "session" : null; + // Fallback to cookie if session doesn't have it if ((token == null || token.isEmpty()) && request.getCookies() != null) { for (jakarta.servlet.http.Cookie cookie : request.getCookies()) { if ("fafnir_logout_token".equals(cookie.getName())) { token = cookie.getValue(); + tokenSource = "cookie"; log.debug("Retrieved logout token from cookie"); break; } } } - + if (token == null || token.isEmpty()) { log.warn("No logout token found in session or cookie after logout, redirecting to failure"); + UniLoginFlowTrace.log("logout_complete", UniLoginFlowTrace.Branch.logout_complete_no_token, + uniid, session, Map.of("token_source", "none")); return new RedirectView(uniloginConf.getFailureRedirect() + "#" + dk.acto.fafnir.api.model.FailureReason.AUTHENTICATION_FAILED.getErrorCode()); } - + // Clear the token from session and cookie if (session != null) { session.removeAttribute("logout_token"); } - + // Retrieve the JWT from cache using one-time token - String jwt = provider.retrieveJwtFromCache(token); - + String jwt = provider.retrieveJwtFromCache(token, uniid, session); + if (jwt != null && !jwt.isEmpty()) { - log.debug("JWT retrieved from cache, redirecting to success page"); - // Redirect to success page with JWT in hash + log.info("JWT retrieved from cache, redirecting to success page"); + UniLoginFlowTrace.log("logout_complete", UniLoginFlowTrace.Branch.logout_complete_success, + uniid, session, Map.of( + "token_source", tokenSource != null ? tokenSource : "unknown", + "cache_token_prefix", UniLoginFlowTrace.tokenPrefix(token))); return new RedirectView(uniloginConf.getSuccessRedirect() + "#" + jwt); } else { log.warn("No valid JWT found in cache after logout (token may be expired or invalid), redirecting to failure"); + UniLoginFlowTrace.log("logout_complete", UniLoginFlowTrace.Branch.logout_complete_cache_miss, + uniid, session, Map.of( + "token_source", tokenSource != null ? tokenSource : "unknown", + "cache_token_prefix", UniLoginFlowTrace.tokenPrefix(token))); return new RedirectView(uniloginConf.getFailureRedirect() + "#" + dk.acto.fafnir.api.model.FailureReason.AUTHENTICATION_FAILED.getErrorCode()); } } diff --git a/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java b/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java new file mode 100644 index 0000000..9d4cb32 --- /dev/null +++ b/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java @@ -0,0 +1,115 @@ +package dk.acto.fafnir.sso.provider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dk.acto.fafnir.sso.provider.unilogin.Institution; +import dk.acto.fafnir.sso.provider.unilogin.UserInfoResponse; +import dk.acto.fafnir.sso.provider.unilogin.UserRole; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +class UniLoginRoleMappingTest { + + @Test + void ensureEmployeeLoginRole_addsFallbackWhenOnlyLaererUnknown() { + Set roles = Set.of( + UserRole.builder().name("LÆRER").type("UNKNOWN").build() + ); + + Set result = UniLoginProvider.ensureEmployeeLoginRole(roles, "Medarbejder"); + + assertThat(roleStrings(result)).contains("Medarbejder@EMPLOYEE"); + } + + @Test + void ensureEmployeeLoginRole_addsFallbackWhenRolesEmptyAndAktorgruppeMedarbejder() { + Set result = UniLoginProvider.ensureEmployeeLoginRole(Set.of(), "Medarbejder"); + + assertThat(roleStrings(result)).containsExactly("Medarbejder@EMPLOYEE"); + } + + @Test + void ensureEmployeeLoginRole_keepsValidEmployeeRoleWithoutFallback() { + Set roles = Set.of( + UserRole.builder().name("Lærer").type("EMPLOYEE").build() + ); + + Set result = UniLoginProvider.ensureEmployeeLoginRole(roles, "Medarbejder"); + + assertThat(roleStrings(result)).containsExactly("Lærer@EMPLOYEE"); + } + + @Test + void ensureEmployeeLoginRole_acceptsMedarbejderUnknownAsCompatible() { + Set roles = Set.of( + UserRole.builder().name("Medarbejder").type("UNKNOWN").build() + ); + + Set result = UniLoginProvider.ensureEmployeeLoginRole(roles, "Medarbejder"); + + assertThat(roleStrings(result)).containsExactly("Medarbejder@UNKNOWN"); + } + + @Test + void deduplicateInstitutions_mergesSameInstnr() { + List institutions = List.of( + new Institution("12345", "Gladsaxe Skole", List.of("LÆRER@UNKNOWN")), + new Institution("12345", "Gladsaxe Skole", List.of("Medarbejder")) + ); + + List result = UniLoginProvider.deduplicateInstitutions(institutions); + + assertThat(result).hasSize(1); + assertThat(result.get(0).id).isEqualTo("12345"); + assertThat(result.get(0).roles).containsExactlyInAnyOrder("LÆRER@UNKNOWN", "Medarbejder"); + } + + @Test + void deduplicateInstitutions_keepsDistinctInstnr() { + List institutions = List.of( + new Institution("11111", "Skole A", List.of()), + new Institution("22222", "Skole B", List.of()) + ); + + List result = UniLoginProvider.deduplicateInstitutions(institutions); + + assertThat(result).hasSize(2); + } + + @Test + void userInfoResponse_parsesInstbrugereAlias() throws Exception { + String json = """ + {"instbrugere":[{"instnr":"12345","instnavn":"Test Skole","ansat_roller":["Lærer"]}]} + """; + + UserInfoResponse response = new ObjectMapper().readValue(json, UserInfoResponse.class); + + assertThat(response.getInstBrugere()).hasSize(1); + assertThat(response.getInstBrugere().get(0).getInTnr()).isEqualTo("12345"); + assertThat(response.getInstBrugere().get(0).getAnsatRoller()).containsExactly("Lærer"); + } + + @Test + void isEmployeeAktorgruppe_recognizesEmployeeValues() { + assertThat(UniLoginProvider.isEmployeeAktorgruppe("Medarbejder")).isTrue(); + assertThat(UniLoginProvider.isEmployeeAktorgruppe("EMPLOYEE")).isTrue(); + assertThat(UniLoginProvider.isEmployeeAktorgruppe("ANSAT")).isTrue(); + assertThat(UniLoginProvider.isEmployeeAktorgruppe("Elev")).isFalse(); + assertThat(UniLoginProvider.isEmployeeAktorgruppe(null)).isFalse(); + } + + @Test + void fixA_emptyRolesWithMedarbejderAktorgruppe_yieldsCompatibleEmployeeRole() { + Set roles = UniLoginProvider.ensureEmployeeLoginRole(Set.of(), "Medarbejder"); + assertThat(UniLoginProvider.hasFind2LearnCompatibleEmployeeRole(roles)).isTrue(); + assertThat(roleStrings(roles)).contains("Medarbejder@EMPLOYEE"); + } + + private static List roleStrings(Set roles) { + return roles.stream().map(UserRole::toString).sorted().collect(Collectors.toList()); + } +} diff --git a/sso/src/test/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTraceTest.java b/sso/src/test/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTraceTest.java new file mode 100644 index 0000000..0c62ea9 --- /dev/null +++ b/sso/src/test/java/dk/acto/fafnir/sso/provider/unilogin/UniLoginFlowTraceTest.java @@ -0,0 +1,44 @@ +package dk.acto.fafnir.sso.provider.unilogin; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class UniLoginFlowTraceTest { + + @Test + void instnrList_extractsNonBlankIds() { + List institutions = List.of( + new Institution("12345", "Skole A", List.of()), + new Institution("", "Blank", List.of()), + new Institution("67890", "Skole B", List.of()) + ); + + assertThat(UniLoginFlowTrace.instnrList(institutions)).containsExactly("12345", "67890"); + } + + @Test + void instnrList_returnsEmptyForNullOrEmpty() { + assertThat(UniLoginFlowTrace.instnrList(null)).isEmpty(); + assertThat(UniLoginFlowTrace.instnrList(List.of())).isEmpty(); + } + + @Test + void sessionId_returnsPrefixOrDash() { + assertThat(UniLoginFlowTrace.sessionId(null)).isEqualTo("-"); + } + + @Test + void tokenPrefix_returnsPrefixOrDash() { + assertThat(UniLoginFlowTrace.tokenPrefix(null)).isEqualTo("-"); + assertThat(UniLoginFlowTrace.tokenPrefix("")).isEqualTo("-"); + assertThat(UniLoginFlowTrace.tokenPrefix("AbCdEfGhIj")).isEqualTo("AbCdEfGh"); + } + + @Test + void hasUserInfoInSession_returnsFalseWhenSessionNull() { + assertThat(UniLoginFlowTrace.hasUserInfoInSession(null)).isFalse(); + } +} From 1fb92615c53f4e387fbaed5b0c9fd2074e3cc0f6 Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Mon, 6 Jul 2026 16:03:36 +0200 Subject: [PATCH 3/5] Filter single invalid institution during UniLogin deduplication Removes a bypass that allowed a list with a single institution to be returned without ID validation. This ensures institutions with null or blank IDs are always filtered, regardless of list size. --- .../dk/acto/fafnir/sso/provider/UniLoginProvider.java | 3 --- .../acto/fafnir/sso/provider/UniLoginRoleMappingTest.java | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java index 413610c..1337b5d 100644 --- a/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java +++ b/sso/src/main/java/dk/acto/fafnir/sso/provider/UniLoginProvider.java @@ -1037,9 +1037,6 @@ static List deduplicateInstitutions(List institutions) if (institutions == null || institutions.isEmpty()) { return Collections.emptyList(); } - if (institutions.size() == 1) { - return institutions; - } Map byId = new LinkedHashMap<>(); for (Institution inst : institutions) { diff --git a/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java b/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java index 9d4cb32..e6e3023 100644 --- a/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java +++ b/sso/src/test/java/dk/acto/fafnir/sso/provider/UniLoginRoleMappingTest.java @@ -68,6 +68,14 @@ void deduplicateInstitutions_mergesSameInstnr() { assertThat(result.get(0).roles).containsExactlyInAnyOrder("LÆRER@UNKNOWN", "Medarbejder"); } + @Test + void deduplicateInstitutions_filtersSingleInvalidId() { + assertThat(UniLoginProvider.deduplicateInstitutions(List.of(new Institution(null, "Skole A", List.of())))) + .isEmpty(); + assertThat(UniLoginProvider.deduplicateInstitutions(List.of(new Institution(" ", "Skole A", List.of())))) + .isEmpty(); + } + @Test void deduplicateInstitutions_keepsDistinctInstnr() { List institutions = List.of( From ec163bae9804e72ebd172c5ea23d2ac313bf3a76 Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Mon, 6 Jul 2026 16:15:47 +0200 Subject: [PATCH 4/5] Vendor UniLogin WSDL files for improved build stability Embeds WSDL definitions for WsiBruger and WsiInst services directly into the repository. This ensures consistent and reproducible builds, reducing reliance on external WSDL endpoints and enabling offline compilation. The build script now only downloads WSDLs if they are not already present. --- .gitignore | 2 - sso/build.gradle.kts | 2 + sso/src/main/resources/wsdl/wsibruger_v6.wsdl | 220 +++++++++++++ sso/src/main/resources/wsdl/wsiinst_v5.wsdl | 299 ++++++++++++++++++ 4 files changed, 521 insertions(+), 2 deletions(-) create mode 100644 sso/src/main/resources/wsdl/wsibruger_v6.wsdl create mode 100644 sso/src/main/resources/wsdl/wsiinst_v5.wsdl diff --git a/.gitignore b/.gitignore index 9c47ca2..0f2d112 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,6 @@ *.iml out/ -WSDL -resources/wsdl/ dk/acto/auth/generated/ # Gradle diff --git a/sso/build.gradle.kts b/sso/build.gradle.kts index 83be2b9..5c4a22f 100644 --- a/sso/build.gradle.kts +++ b/sso/build.gradle.kts @@ -93,11 +93,13 @@ dependencies { tasks.register("downloadWSDLWsiBruger") { src("https://wsibruger.unilogin.dk/wsibruger-v6/ws?WSDL") dest("src/main/resources/wsdl/wsibruger_v6.wsdl") + onlyIf { !dest.exists() } } tasks.register("downloadWSDLWsiInst") { src("https://wsiinst.unilogin.dk/wsiinst-v5/ws?WSDL") dest("src/main/resources/wsdl/wsiinst_v5.wsdl") + onlyIf { !dest.exists() } } tasks.register("genWsiBruger") { diff --git a/sso/src/main/resources/wsdl/wsibruger_v6.wsdl b/sso/src/main/resources/wsdl/wsibruger_v6.wsdl new file mode 100644 index 0000000..952941c --- /dev/null +++ b/sso/src/main/resources/wsdl/wsibruger_v6.wsdl @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Ping webservicen med simpelt kald. Returnerer "HelloWorld" hvis webservicen svarer. + + + + + + +Ping webservicen med simpelt kald. Returnerer "helloWorldWithDBAndCredentials" hvis webservicen svarer og brugernavn/password er korrekt angivet. + + + + + + + + +Ping webservicen med simpelt kald. Returnerer "HelloWorld" samt tidspunkt fra databasen hvis webservicen og databasen svarer. + + + + + + +Returnerer brugerens kontaktpersoner på pgd institution. + + + + + + + + +For en kontaktperson bruger, returner en liste over de andre brugere (elever) som vedkommende + er kontaktperson for. Hvis instnr er angivet er det begrænset til kontaktperson-relationen indenfor den + pgd institution + + + + + + + + +For en bruger, returner en liste over de institutioner brugeren er tilknyttet + + + + + + + + +Returnerer en liste af institutionsnumre, hvor der er lavet en dataaftale + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/wsiinst_v5.wsdl b/sso/src/main/resources/wsdl/wsiinst_v5.wsdl new file mode 100644 index 0000000..06fba0c --- /dev/null +++ b/sso/src/main/resources/wsdl/wsiinst_v5.wsdl @@ -0,0 +1,299 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Ping webservicen med simpelt kald. Returnerer "HelloWorld" hvis webservicen svarer. + + + + + + +Ping webservicen med simpelt kald. Returnerer "HelloWorldWithDBAndCredentials" hvis webservicen svarer og brugernavn/password er korrekt angivet. + + + + + + + + +Ping webservicen med simpelt kald. Returnerer "HelloWorld" samt tidspunkt fra databasen hvis webservicen og databasen svarer. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Returnerer en liste af institutionsnumre, hvor der er lavet en dataaftale + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From d0fabd45e23d1b0f6ee1b38f90de8f2d5ab34fd0 Mon Sep 17 00:00:00 2001 From: Jonas Hein Date: Mon, 6 Jul 2026 16:30:58 +0200 Subject: [PATCH 5/5] Vendor UniLogin WSDL schema dependencies Embeds referenced XSD files for WsiBruger and WsiInst services directly into the repository. The WSDL definitions are updated to reference these local schema files instead of external URLs. This completes the vendoring of UniLogin service definitions, further enhancing build consistency and resilience against external service changes or network issues affecting schema endpoints. --- sso/build.gradle.kts | 49 ++ .../resources/wsdl/common/schema-common.xsd | 566 ++++++++++++++++++ .../main/resources/wsdl/common/ws-common.xsd | 21 + .../main/resources/wsdl/wsibruger-schema.xsd | 52 ++ sso/src/main/resources/wsdl/wsibruger-ws.xsd | 14 + sso/src/main/resources/wsdl/wsibruger_v6.wsdl | 4 +- .../main/resources/wsdl/wsiinst-schema.xsd | 108 ++++ sso/src/main/resources/wsdl/wsiinst-ws.xsd | 23 + sso/src/main/resources/wsdl/wsiinst_v5.wsdl | 4 +- 9 files changed, 837 insertions(+), 4 deletions(-) create mode 100644 sso/src/main/resources/wsdl/common/schema-common.xsd create mode 100644 sso/src/main/resources/wsdl/common/ws-common.xsd create mode 100644 sso/src/main/resources/wsdl/wsibruger-schema.xsd create mode 100644 sso/src/main/resources/wsdl/wsibruger-ws.xsd create mode 100644 sso/src/main/resources/wsdl/wsiinst-schema.xsd create mode 100644 sso/src/main/resources/wsdl/wsiinst-ws.xsd diff --git a/sso/build.gradle.kts b/sso/build.gradle.kts index 5c4a22f..62b274a 100644 --- a/sso/build.gradle.kts +++ b/sso/build.gradle.kts @@ -102,6 +102,42 @@ tasks.register("downloadWSDLWsiInst") { onlyIf { !dest.exists() } } +tasks.register("downloadXsdWsCommon") { + src("https://wsibruger.unilogin.dk/wsibruger-v6/ws?xsd=common/ws-common.xsd") + dest("src/main/resources/wsdl/common/ws-common.xsd") + onlyIf { !dest.exists() } +} + +tasks.register("downloadXsdWsiBruger") { + src("https://wsibruger.unilogin.dk/wsibruger-v6/ws?xsd=wsibruger-ws.xsd") + dest("src/main/resources/wsdl/wsibruger-ws.xsd") + onlyIf { !dest.exists() } +} + +tasks.register("downloadXsdWsiInst") { + src("https://wsiinst.unilogin.dk/wsiinst-v5/ws?xsd=wsiinst-ws.xsd") + dest("src/main/resources/wsdl/wsiinst-ws.xsd") + onlyIf { !dest.exists() } +} + +tasks.register("downloadXsdSchemaCommon") { + src("https://wsibruger.unilogin.dk/wsibruger-v6/ws?xsd=common/schema-common.xsd") + dest("src/main/resources/wsdl/common/schema-common.xsd") + onlyIf { !dest.exists() } +} + +tasks.register("downloadXsdWsibrugerSchema") { + src("https://wsibruger.unilogin.dk/wsibruger-v6/ws?xsd=wsibruger-schema.xsd") + dest("src/main/resources/wsdl/wsibruger-schema.xsd") + onlyIf { !dest.exists() } +} + +tasks.register("downloadXsdWsiinstSchema") { + src("https://wsiinst.unilogin.dk/wsiinst-v5/ws?xsd=wsiinst-schema.xsd") + dest("src/main/resources/wsdl/wsiinst-schema.xsd") + onlyIf { !dest.exists() } +} + tasks.register("genWsiBruger") { toolOptions { markGenerated.set(true) @@ -110,6 +146,10 @@ tasks.register("genWsiBruger") { encoding.set("UTF-8") } dependsOn(tasks.getByName("downloadWSDLWsiBruger")) + dependsOn(tasks.getByName("downloadXsdWsCommon")) + dependsOn(tasks.getByName("downloadXsdSchemaCommon")) + dependsOn(tasks.getByName("downloadXsdWsiBruger")) + dependsOn(tasks.getByName("downloadXsdWsibrugerSchema")) } tasks.register("genWsiInst") { @@ -120,6 +160,9 @@ tasks.register("genWsiInst") { encoding.set("UTF-8") } dependsOn(tasks.getByName("downloadWSDLWsiInst")) + dependsOn(tasks.getByName("downloadXsdWsCommon")) + dependsOn(tasks.getByName("downloadXsdWsiInst")) + dependsOn(tasks.getByName("downloadXsdWsiinstSchema")) } tasks.compileJava { @@ -130,4 +173,10 @@ tasks.compileJava { tasks.processResources { dependsOn(tasks.getByName("downloadWSDLWsiBruger")) dependsOn(tasks.getByName("downloadWSDLWsiInst")) + dependsOn(tasks.getByName("downloadXsdWsCommon")) + dependsOn(tasks.getByName("downloadXsdSchemaCommon")) + dependsOn(tasks.getByName("downloadXsdWsiBruger")) + dependsOn(tasks.getByName("downloadXsdWsibrugerSchema")) + dependsOn(tasks.getByName("downloadXsdWsiInst")) + dependsOn(tasks.getByName("downloadXsdWsiinstSchema")) } diff --git a/sso/src/main/resources/wsdl/common/schema-common.xsd b/sso/src/main/resources/wsdl/common/schema-common.xsd new file mode 100644 index 0000000..a28fe4e --- /dev/null +++ b/sso/src/main/resources/wsdl/common/schema-common.xsd @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fejlkoder ved autentifikationsfejl. Sættes også som en subcode i den returnerede SOAP-fault. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Trin angives kun for grupper med typen Hovedgruppe. + + + + + + + + + + + + + + + + + + + + + + + + DT er dagtilbud, 1 svarer til 1. klassetrin i grundskolen, + U1 er første trin på en ungdomsuddannelse, og VU er voksenuddannelse. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Basistype for strenge. + Indholdet må ikke være tomt, og overflødig whitespace fjernes (XSD token). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Uverificeret. + + + + + CPR-verificeret. + + + + + + + + + + + Kontaktpersonen må få adgang til almindelige personoplysninger om den tilknyttede elev. + + + + + + + Kontaktpersonen må få adgang til fortrolige og følsomme personoplysninger om eleven. + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/common/ws-common.xsd b/sso/src/main/resources/wsdl/common/ws-common.xsd new file mode 100644 index 0000000..d8aeaad --- /dev/null +++ b/sso/src/main/resources/wsdl/common/ws-common.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/wsibruger-schema.xsd b/sso/src/main/resources/wsdl/wsibruger-schema.xsd new file mode 100644 index 0000000..b5ffbcd --- /dev/null +++ b/sso/src/main/resources/wsdl/wsibruger-schema.xsd @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/wsibruger-ws.xsd b/sso/src/main/resources/wsdl/wsibruger-ws.xsd new file mode 100644 index 0000000..69564bc --- /dev/null +++ b/sso/src/main/resources/wsdl/wsibruger-ws.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/wsibruger_v6.wsdl b/sso/src/main/resources/wsdl/wsibruger_v6.wsdl index 952941c..803bdc2 100644 --- a/sso/src/main/resources/wsdl/wsibruger_v6.wsdl +++ b/sso/src/main/resources/wsdl/wsibruger_v6.wsdl @@ -2,9 +2,9 @@ - + - + diff --git a/sso/src/main/resources/wsdl/wsiinst-schema.xsd b/sso/src/main/resources/wsdl/wsiinst-schema.xsd new file mode 100644 index 0000000..987d7eb --- /dev/null +++ b/sso/src/main/resources/wsdl/wsiinst-schema.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/wsiinst-ws.xsd b/sso/src/main/resources/wsdl/wsiinst-ws.xsd new file mode 100644 index 0000000..fae6b5b --- /dev/null +++ b/sso/src/main/resources/wsdl/wsiinst-ws.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sso/src/main/resources/wsdl/wsiinst_v5.wsdl b/sso/src/main/resources/wsdl/wsiinst_v5.wsdl index 06fba0c..407313b 100644 --- a/sso/src/main/resources/wsdl/wsiinst_v5.wsdl +++ b/sso/src/main/resources/wsdl/wsiinst_v5.wsdl @@ -2,9 +2,9 @@ - + - +