diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartyWrapper.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartyWrapper.java index 77cbc1e..b560afb 100644 --- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartyWrapper.java +++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/RelyingPartyWrapper.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import it.spid.cie.oidc.callback.RelyingPartyLogoutCallback; import it.spid.cie.oidc.config.RelyingPartyOptions; import it.spid.cie.oidc.exception.OIDCException; import it.spid.cie.oidc.handler.RelyingPartyHandler; @@ -39,6 +40,12 @@ public WellKnownData getWellKnownData(String requestURL, boolean jsonMode) return relyingPartyHandler.getWellKnownData(requestURL, jsonMode); } + public String performLogout(String userKey, RelyingPartyLogoutCallback callback) + throws OIDCException { + + return relyingPartyHandler.performLogout(userKey, callback); + } + @PostConstruct private void postConstruct() throws OIDCException { RelyingPartyOptions options = new RelyingPartyOptions() diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java index 7221ca0..24b29b2 100644 --- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java +++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/controller/SpidController.java @@ -18,7 +18,12 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.view.RedirectView; +import it.spid.cie.oidc.callback.RelyingPartyLogoutCallback; +import it.spid.cie.oidc.model.AuthnRequest; +import it.spid.cie.oidc.model.AuthnToken; import it.spid.cie.oidc.spring.boot.relying.party.RelyingPartyWrapper; +import it.spid.cie.oidc.util.GetterUtil; +import it.spid.cie.oidc.util.Validator; @RestController @RequestMapping("/oidc/rp") @@ -69,6 +74,34 @@ public RedirectView callback( return new RedirectView("echo_attributes"); } + @GetMapping("/logout") + public RedirectView logout( + @RequestParam Map params, + final HttpServletRequest request, HttpServletResponse response) + throws Exception { + + String userKey = GetterUtil.getString(request.getSession().getAttribute("USER")); + + String redirectURL = relyingPartyWrapper.performLogout( + userKey, new RelyingPartyLogoutCallback() { + + @Override + public void logout( + String userKey, AuthnRequest authnRequest, AuthnToken authnToken) { + + request.getSession().removeAttribute("USER"); + request.getSession().removeAttribute("USER_INFO"); + } + + }); + + if (!Validator.isNullOrEmpty(redirectURL)) { + return new RedirectView(redirectURL); + } + + return new RedirectView("landing"); + } + private static Logger logger = LoggerFactory.getLogger(SpidController.class); @Autowired diff --git a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/persistence/H2PersistenceImpl.java b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/persistence/H2PersistenceImpl.java index 3db99df..4bc1580 100644 --- a/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/persistence/H2PersistenceImpl.java +++ b/examples/relying-party-spring-boot/src/main/java/it/spid/cie/oidc/spring/boot/relying/party/persistence/H2PersistenceImpl.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,10 +27,29 @@ import it.spid.cie.oidc.spring.boot.relying.party.persistence.model.FederationEntityRepository; import it.spid.cie.oidc.spring.boot.relying.party.persistence.model.TrustChainModel; import it.spid.cie.oidc.spring.boot.relying.party.persistence.model.TrustChainRepository; +import it.spid.cie.oidc.util.GetterUtil; @Component public class H2PersistenceImpl implements PersistenceAdapter { + @Override + public AuthnRequest fetchAuthnRequest(String storageId) throws PersistenceException { + try { + long id = GetterUtil.getLong(storageId); + + Optional model = authnRequestRepository.findById(id); + + if (model.isPresent()) { + return model.get().toAuthnRequest(); + } + } + catch (Exception e) { + throw new PersistenceException(e); + } + + return null; + } + @Override public CachedEntityInfo fetchEntityInfo(String subject, String issuer) throws PersistenceException { @@ -162,6 +182,24 @@ public List findAuthnRequests(String state) } } + @Override + public List findAuthnTokens(String userKey) throws PersistenceException { + List result = new ArrayList<>(); + + try { + List models = authnTokenRepository.findUserTokens(userKey); + + for (AuthnTokenModel model : models) { + result.add(model.toAuthnToken()); + } + + return result; + } + catch (Exception e) { + throw new PersistenceException(e); + } + } + @Override public CachedEntityInfo storeEntityInfo(CachedEntityInfo entityInfo) throws PersistenceException { diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/callback/RelyingPartyLogoutCallback.java b/starter-kit/src/main/java/it/spid/cie/oidc/callback/RelyingPartyLogoutCallback.java new file mode 100644 index 0000000..f3d9961 --- /dev/null +++ b/starter-kit/src/main/java/it/spid/cie/oidc/callback/RelyingPartyLogoutCallback.java @@ -0,0 +1,10 @@ +package it.spid.cie.oidc.callback; + +import it.spid.cie.oidc.model.AuthnRequest; +import it.spid.cie.oidc.model.AuthnToken; + +public interface RelyingPartyLogoutCallback { + + public void logout(String userKey, AuthnRequest authnRequest, AuthnToken authnToken); + +} diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java b/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java index 03f833c..4425f69 100644 --- a/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java +++ b/starter-kit/src/main/java/it/spid/cie/oidc/config/RelyingPartyOptions.java @@ -78,6 +78,14 @@ public String getTrustMarks() { return trustMarks; } + public String getLoginURL() { + return loginRedirectURL; + } + + public String getLogoutRedirectURL() { + return logoutRedirectURL; + } + public RelyingPartyOptions setProfileAcr(OIDCProfile profile, String acr) { if (acr != null) { if (OIDCProfile.SPID.equals(profile)) { diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java b/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java index b27ee63..93d40c8 100644 --- a/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java +++ b/starter-kit/src/main/java/it/spid/cie/oidc/handler/RelyingPartyHandler.java @@ -18,6 +18,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import it.spid.cie.oidc.callback.RelyingPartyLogoutCallback; import it.spid.cie.oidc.config.GlobalOptions; import it.spid.cie.oidc.config.OIDCConstants; import it.spid.cie.oidc.config.RelyingPartyOptions; @@ -30,11 +31,11 @@ import it.spid.cie.oidc.helper.OAuth2Helper; import it.spid.cie.oidc.helper.OIDCHelper; import it.spid.cie.oidc.helper.PKCEHelper; +import it.spid.cie.oidc.model.AuthnRequest; +import it.spid.cie.oidc.model.AuthnToken; import it.spid.cie.oidc.model.CachedEntityInfo; import it.spid.cie.oidc.model.EntityConfiguration; import it.spid.cie.oidc.model.FederationEntity; -import it.spid.cie.oidc.model.AuthnRequest; -import it.spid.cie.oidc.model.AuthnToken; import it.spid.cie.oidc.model.TrustChain; import it.spid.cie.oidc.model.TrustChainBuilder; import it.spid.cie.oidc.persistence.PersistenceAdapter; @@ -269,34 +270,49 @@ public WellKnownData getWellKnownData(String requestURL, boolean jsonMode) } } + // TODO: userKey is not enough. We need a more unique element + public String performLogout(String userKey, RelyingPartyLogoutCallback callback) + throws OIDCException { + + try { + return doPerformLogout(userKey, callback); + } + catch (OIDCException e) { + throw e; + } + catch (Exception e) { + throw new OIDCException(e); + } + } + protected JSONObject doGetUserInfo(String state, String code) throws OIDCException { - + if (Validator.isNullOrEmpty(code) || Validator.isNullOrEmpty(state)) { throw new SchemaException.Validation( "Authn response object validation failed"); } - + List authnRequests = persistence.findAuthnRequests(state); - + if (authnRequests.isEmpty()) { throw new RelyingPartyException.Generic("No AuthnRequest"); } - + AuthnRequest authnRequest = ListUtil.getLast(authnRequests); - + AuthnToken authnToken = new AuthnToken() .setAuthnRequestId(authnRequest.getStorageId()) .setCode(code); - + authnToken = persistence.storeOIDCAuthnToken(authnToken); - + // Get clientId configuration. In this situation "clientId" refers this // RelyingParty - + FederationEntity entityConf = persistence.fetchFederationEntity( authnRequest.getClientId(), true); - + if (entityConf == null) { throw new RelyingPartyException.Generic( "RelyingParty %s not found", authnRequest.getClientId()); @@ -305,27 +321,27 @@ else if (!Objects.equals(options.getClientId(), authnRequest.getClientId())) { throw new RelyingPartyException.Generic( "Invalid RelyingParty %s", authnRequest.getClientId()); } - + JSONObject authnData = new JSONObject(authnRequest.getData()); - + JSONObject providerConfiguration = new JSONObject( authnRequest.getProviderConfiguration()); - + JSONObject jsonTokenResponse = oauth2Helper.performAccessTokenRequest( authnData.optString("redirect_uri"), state, code, authnRequest.getProviderId(), entityConf, providerConfiguration.optString("token_endpoint"), authnData.optString("code_verifier")); - + TokenResponse tokenResponse = TokenResponse.of(jsonTokenResponse); - + if (logger.isDebugEnabled()) { logger.debug("TokenResponse=" + tokenResponse.toString()); } - + JWKSet providerJwks = JWTHelper.getJWKSetFromJSON( providerConfiguration.optJSONObject("jwks")); - + try { jwtHelper.verifyJWS(tokenResponse.getAccessToken(), providerJwks); } @@ -333,57 +349,130 @@ else if (!Objects.equals(options.getClientId(), authnRequest.getClientId())) { throw new RelyingPartyException.Authentication( "Authentication token validation error."); } - + try { jwtHelper.verifyJWS(tokenResponse.getIdToken(), providerJwks); } catch (Exception e) { throw new RelyingPartyException.Authentication("ID token validation error."); } - + // Update AuthenticationToken - + authnToken.setAccessToken(tokenResponse.getAccessToken()); authnToken.setIdToken(tokenResponse.getIdToken()); authnToken.setTokenType(tokenResponse.getTokenType()); authnToken.setScope(jsonTokenResponse.optString("scope")); authnToken.setExpiresIn(tokenResponse.getExpiresIn()); - + authnToken = persistence.storeOIDCAuthnToken(authnToken); - + JWKSet entityJwks = JWTHelper.getJWKSetFromJSON(entityConf.getJwks()); - + JSONObject userInfo = oidcHelper.getUserInfo( state, tokenResponse.getAccessToken(), providerConfiguration, true, entityJwks); - + // TODO: userKey from options authnToken.setUserKey(userInfo.optString("https://attributes.spid.gov.it/email")); - + authnToken = persistence.storeOIDCAuthnToken(authnToken); - + return userInfo; } + protected String doPerformLogout( + String userKey, RelyingPartyLogoutCallback callback) + throws Exception { + if (Validator.isNullOrEmpty(userKey)) { + throw new RelyingPartyException.Generic("UserKey null or empty"); + } + + List authnTokens = persistence.findAuthnTokens(userKey); + + if (authnTokens.isEmpty()) { + return options.getLogoutRedirectURL(); + } + + AuthnToken authnToken = ListUtil.getLast(authnTokens); + + AuthnRequest authnRequest = persistence.fetchAuthnRequest( + authnToken.getAuthnRequestId()); + + if (authnRequest == null) { + throw new RelyingPartyException.Generic( + "No AuthnRequest with id " + authnToken.getAuthnRequestId()); + } + + JSONObject providerConfiguration = new JSONObject( + authnRequest.getProviderConfiguration()); + + String revocationUrl = providerConfiguration.optString("revocation_endpoint"); + + // Do local logout + + if (callback != null) { + callback.logout(userKey, authnRequest, authnToken); + } + + if (Validator.isNullOrEmpty(revocationUrl)) { + logger.warn( + "{} doesn't expose the token revocation endpoint.", + authnRequest.getProviderId()); + + return options.getLogoutRedirectURL(); + } + + FederationEntity entityConf = persistence.fetchFederationEntity( + authnRequest.getClientId(), true); + + JWKSet jwkSet = JWTHelper.getJWKSetFromJSON(entityConf.getJwks()); + + authnToken.setRevoked(LocalDateTime.now()); + + authnToken = persistence.storeOIDCAuthnToken(authnToken); + + try { + oauth2Helper.sendRevocationRequest( + authnToken.getAccessToken(), authnRequest.getClientId(), revocationUrl, + entityConf); + } + catch (Exception e) { + logger.error("Token revocation failed: {}", e.getMessage()); + } + + // Revoke older user's authnToken. Evaluate better + + authnTokens = persistence.findAuthnTokens(userKey); + + for (AuthnToken oldToken : authnTokens) { + oldToken.setRevoked(authnToken.getRevoked()); + + persistence.storeOIDCAuthnToken(oldToken); + } + + return options.getLogoutRedirectURL(); + } + protected TrustChain getOrCreateTrustChain( String subject, String trustAnchor, String metadataType, boolean force) throws OIDCException { - + CachedEntityInfo trustAnchorEntity = persistence.fetchEntityInfo( trustAnchor, trustAnchor); - + EntityConfiguration taConf; - + if (trustAnchorEntity == null || trustAnchorEntity.isExpired() || force) { String jwt = EntityHelper.getEntityConfiguration(trustAnchor); - + taConf = new EntityConfiguration(jwt, jwtHelper); - + if (trustAnchorEntity == null) { trustAnchorEntity = CachedEntityInfo.of( trustAnchor, subject, taConf.getExpiresOn(), taConf.getIssuedAt(), taConf.getPayload(), taConf.getJwt()); - + trustAnchorEntity = persistence.storeEntityInfo(trustAnchorEntity); } else { @@ -392,16 +481,16 @@ protected TrustChain getOrCreateTrustChain( trustAnchorEntity.setIssuedAt(taConf.getIssuedAt()); trustAnchorEntity.setStatement(taConf.getPayload()); trustAnchorEntity.setJwt(taConf.getJwt()); - + trustAnchorEntity = persistence.storeEntityInfo(trustAnchorEntity); } } else { taConf = EntityConfiguration.of(trustAnchorEntity, jwtHelper); } - + TrustChain trustChain = persistence.fetchTrustChain(subject, trustAnchor); - + if (trustChain != null && !trustChain.isActive()) { return null; } @@ -410,27 +499,27 @@ protected TrustChain getOrCreateTrustChain( new TrustChainBuilder(subject, metadataType, jwtHelper) .setTrustAnchor(taConf) .start(); - + if (!tcb.isValid()) { String msg = String.format( "Trust Chain for subject %s or trust_anchor %s is not valid", subject, trustAnchor); - + throw new TrustChainException.InvalidTrustChain(msg); } else if (Validator.isNullOrEmpty(tcb.getFinalMetadata())) { String msg = String.format( "Trust chain for subject %s and trust_anchor %s doesn't have any " + "metadata of type '%s'", subject, trustAnchor, metadataType); - + throw new TrustChainException.MissingMetadata(msg); } else { logger.info("KK TCB is valid"); } - + trustChain = persistence.fetchTrustChain(subject, trustAnchor, metadataType); - + if (trustChain == null) { trustChain = new TrustChain() .setSubject(subject) @@ -457,10 +546,10 @@ else if (Validator.isNullOrEmpty(tcb.getFinalMetadata())) { .setTrustMarks(tcb.getVerifiedTrustMarksAsString()) .setStatus("valid"); } - + trustChain = persistence.storeTrustChain(trustChain); } - + return trustChain; } @@ -561,7 +650,7 @@ private String buildURL(String endpoint, JSONObject params) { private JSONObject getRequestedClaims(String profile) { if (OIDCProfile.SPID.equalValue(profile)) { JSONObject result = new JSONObject(); - + JSONObject idToken = new JSONObject() .put( "https://attributes.spid.gov.it/familyName", @@ -569,19 +658,19 @@ private JSONObject getRequestedClaims(String profile) { .put( "https://attributes.spid.gov.it/email", new JSONObject().put("essential", true)); - + JSONObject userInfo = new JSONObject() .put("https://attributes.spid.gov.it/name", new JSONObject()) .put("https://attributes.spid.gov.it/familyName", new JSONObject()) .put("https://attributes.spid.gov.it/email", new JSONObject()) .put("https://attributes.spid.gov.it/fiscalNumber", new JSONObject()); - + result.put("id_token", idToken); result.put("userinfo", userInfo); - + return result; } - + return new JSONObject(); } diff --git a/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java b/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java index 07dfbf3..6c97c6f 100644 --- a/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java +++ b/starter-kit/src/main/java/it/spid/cie/oidc/persistence/PersistenceAdapter.java @@ -11,6 +11,9 @@ public interface PersistenceAdapter { + public AuthnRequest fetchAuthnRequest(String storageId) + throws PersistenceException; + public CachedEntityInfo fetchEntityInfo(String subject, String issuer) throws PersistenceException; @@ -33,6 +36,9 @@ public TrustChain fetchTrustChain( public List findAuthnRequests(String state) throws PersistenceException; + public List findAuthnTokens(String userKey) + throws PersistenceException; + public CachedEntityInfo storeEntityInfo(CachedEntityInfo entityInfo) throws PersistenceException;