diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java index bc007ae438..407c6597f5 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java @@ -19,6 +19,7 @@ package org.keycloak.broker.provider; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -48,4 +49,13 @@ public abstract class AbstractIdentityProvider // no-op } + @Override + public Object callback(RealmModel realm, Callback callback) { + return null; + } + + @Override + public Response logout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) { + return null; + } } diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index f754eeb771..14d6b5fc2c 100755 --- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -20,16 +20,29 @@ package org.keycloak.broker.provider; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.provider.Provider; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.util.Map; /** * @author Pedro Igor */ public interface IdentityProvider extends Provider { + public interface Callback { + public Response authenticated(Map userNotes, IdentityProviderModel identityProviderConfig, FederatedIdentity federatedIdentity, String code); + } + + /** + * JAXRS callback endpoint + * + * @return + */ + Object callback(RealmModel realm, Callback callback); + /** *

Initiates the authentication process by sending an authentication request to an identity provider. This method is called * only once during the authentication.

@@ -79,6 +92,8 @@ public interface IdentityProvider extends Provi */ Response retrieveToken(FederatedIdentityModel identity); + Response logout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm); + /** * Export a representation of the IdentityProvider in a specific format. For example, a SAML EntityDescriptor * diff --git a/broker/saml/pom.xml b/broker/saml/pom.xml index ddd174cd7b..300df94dd7 100755 --- a/broker/saml/pom.xml +++ b/broker/saml/pom.xml @@ -31,6 +31,23 @@ org.picketlink picketlink-federation + + org.keycloak + keycloak-services + ${project.version} + provided + + + org.keycloak + keycloak-events-api + ${project.version} + provided + + + org.jboss.logging + jboss-logging + provided + diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java new file mode 100755 index 0000000000..62decc4394 --- /dev/null +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -0,0 +1,375 @@ +package org.keycloak.broker.saml; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.VerificationException; +import org.keycloak.broker.provider.AuthenticationResponse; +import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.IdentityProvider; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.saml.SAMLRequestParser; +import org.keycloak.protocol.saml.SamlProtocol; +import org.keycloak.protocol.saml.SamlProtocolUtils; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.EventsManager; +import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.flows.Flows; +import org.picketlink.common.constants.GeneralConstants; +import org.picketlink.common.constants.JBossSAMLConstants; +import org.picketlink.common.constants.JBossSAMLURIConstants; +import org.picketlink.common.exceptions.ProcessingException; +import org.picketlink.common.util.DocumentUtil; +import org.picketlink.common.util.StaxParserUtil; +import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; +import org.picketlink.identity.federation.core.parsers.saml.SAMLParser; +import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder; +import org.picketlink.identity.federation.core.util.JAXPValidationUtil; +import org.picketlink.identity.federation.core.util.XMLEncryptionUtil; +import org.picketlink.identity.federation.core.util.XMLSignatureUtil; +import org.picketlink.identity.federation.saml.v2.assertion.AssertionType; +import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType; +import org.picketlink.identity.federation.saml.v2.assertion.EncryptedAssertionType; +import org.picketlink.identity.federation.saml.v2.assertion.NameIDType; +import org.picketlink.identity.federation.saml.v2.assertion.SubjectType; +import org.picketlink.identity.federation.saml.v2.profiles.sso.ecp.RequestType; +import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; +import org.picketlink.identity.federation.saml.v2.protocol.StatusResponseType; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.xml.namespace.QName; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SAMLEndpoint { + protected static final Logger logger = Logger.getLogger(SAMLEndpoint.class); + protected RealmModel realm; + protected EventBuilder event; + protected SAMLIdentityProviderConfig config; + protected IdentityProvider.Callback callback; + + @Context + private UriInfo uriInfo; + + @Context + private KeycloakSession session; + + @Context + private ClientConnection clientConnection; + + @Context + private HttpRequest request; + + @Context + private HttpHeaders headers; + + + public SAMLEndpoint(RealmModel realm, SAMLIdentityProviderConfig config, IdentityProvider.Callback callback) { + this.realm = realm; + this.config = config; + this.callback = callback; + } + + @GET + public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, + @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, + @QueryParam(GeneralConstants.RELAY_STATE) String relayState) { + return new RedirectBinding().execute(samlRequest, samlResponse, relayState); + } + + + /** + */ + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response postBinding(@FormParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, + @FormParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, + @FormParam(GeneralConstants.RELAY_STATE) String relayState) { + return new PostBinding().execute(samlRequest, samlResponse, relayState); + } + + protected abstract class Binding { + private boolean checkSsl() { + if (uriInfo.getBaseUri().getScheme().equals("https")) { + return true; + } else { + return !realm.getSslRequired().isRequired(clientConnection); + } + } + + protected Response basicChecks(String samlRequest, String samlResponse) { + if (!checkSsl()) { + event.event(EventType.LOGIN); + event.error(Errors.SSL_REQUIRED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.HTTPS_REQUIRED); + } + if (!realm.isEnabled()) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.REALM_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.REALM_NOT_ENABLED); + } + + if (samlRequest == null && samlResponse == null) { + event.event(EventType.LOGIN); + event.error(Errors.INVALID_REQUEST); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST ); + + } + return null; + } + + protected abstract String getBindingType(); + protected abstract void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException; + protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest); + protected abstract SAMLDocumentHolder extractResponseDocument(String response); + protected PublicKey getIDPKey() { + X509Certificate certificate = null; + try { + certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(config.getSigningCertificate().replaceAll("\\s", "")); + } catch (ProcessingException e) { + throw new RuntimeException(e); + } + return certificate.getPublicKey(); + } + + public Response execute(String samlRequest, String samlResponse, String relayState) { + event = new EventsManager(realm, session, clientConnection).createEventBuilder(); + Response response = basicChecks(samlRequest, samlResponse); + if (response != null) return response; + if (samlRequest != null) throw new RuntimeException("NOT IMPLEMETED");//return handleSamlRequest(samlRequest, relayState); + else return handleSamlResponse(samlResponse, relayState); + } + + protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder holder, ResponseType responseType, String relayState) { + if (config.isValidateSignature()) { + try { + verifySignature(holder); + } catch (VerificationException e) { + logger.error("validation failed", e); + event.event(EventType.LOGIN); + event.error(Errors.INVALID_SIGNATURE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUESTER); + } + } + + try { + AssertionType assertion = getAssertion(responseType); + SubjectType subject = assertion.getSubject(); + SubjectType.STSubType subType = subject.getSubType(); + NameIDType subjectNameID = (NameIDType) subType.getBaseID(); + Map notes = new HashMap<>(); + notes.put("SAML_FEDERATED_SUBJECT", subjectNameID.getValue()); + if (subjectNameID.getFormat() != null) notes.put("SAML_FEDERATED_SUBJECT_NAMEFORMAT", subjectNameID.getFormat().toString()); + FederatedIdentity identity = new FederatedIdentity(subjectNameID.getValue()); + + identity.setUsername(subjectNameID.getValue()); + + if (subjectNameID.getFormat().toString().equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) { + identity.setEmail(subjectNameID.getValue()); + } + + if (config.isStoreToken()) { + identity.setToken(samlResponse); + } + + AuthnStatementType authn = null; + for (Object statement : assertion.getStatements()) { + if (statement instanceof AuthnStatementType) { + authn = (AuthnStatementType)statement; + break; + } + } + if (authn != null && authn.getSessionIndex() != null) { + notes.put("SAML_FEDERATED_SESSION_INDEX", authn.getSessionIndex()); + } + return callback.authenticated(notes, config, identity, relayState); + + } catch (Exception e) { + throw new IdentityBrokerException("Could not process response from SAML identity provider.", e); + } + + + } + + private AssertionType getAssertion(ResponseType responseType) throws ProcessingException { + List assertions = responseType.getAssertions(); + + if (assertions.isEmpty()) { + throw new IdentityBrokerException("No assertion from response."); + } + + ResponseType.RTChoiceType rtChoiceType = assertions.get(0); + EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion(); + + if (encryptedAssertion != null) { + decryptAssertion(responseType, realm.getPrivateKey()); + + } + return responseType.getAssertions().get(0).getAssertion(); + } + + public Response handleSamlResponse(String samlResponse, String relayState) { + SAMLDocumentHolder holder = extractResponseDocument(samlResponse); + StatusResponseType statusResponse = (StatusResponseType)holder.getSamlObject(); + // validate destination + if (!uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) { + event.event(EventType.IDENTITY_PROVIDER_RESPONSE); + event.error(Errors.INVALID_SAML_RESPONSE); + event.detail(Details.REASON, "invalid_destination"); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUEST); + } + if (statusResponse instanceof ResponseType) { + return handleLoginResponse(samlResponse, holder, (ResponseType)statusResponse, relayState); + + } else { + // todo need to check that it is actually a LogoutResponse + return handleLogoutResponse(holder, statusResponse, relayState); + } + //throw new RuntimeException("Unknown response type"); + + } + + protected Response handleLogoutResponse(SAMLDocumentHolder holder, StatusResponseType responseType, String relayState) { + if (config.isValidateSignature()) { + try { + verifySignature(holder); + } catch (VerificationException e) { + logger.error("logout response validation failed", e); + event.event(EventType.LOGOUT); + event.error(Errors.INVALID_SIGNATURE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REQUESTER); + } + } + if (relayState == null) { + logger.error("no valid user session"); + event.event(EventType.LOGOUT); + event.error(Errors.USER_SESSION_NOT_FOUND); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE); + } + UserSessionModel userSession = session.sessions().getUserSession(realm, relayState); + if (userSession == null) { + logger.error("no valid user session"); + event.event(EventType.LOGOUT); + event.error(Errors.USER_SESSION_NOT_FOUND); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE); + } + if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) { + logger.error("usersession in different state"); + event.event(EventType.LOGOUT); + event.error(Errors.USER_SESSION_NOT_FOUND); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE); + } + return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers); + } + + + protected ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ProcessingException { + SAML2Response saml2Response = new SAML2Response(); + + try { + Document doc = saml2Response.convert(responseType); + Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get())); + + if (enc == null) { + throw new IdentityBrokerException("No encrypted assertion found."); + } + + String oldID = enc.getAttribute(JBossSAMLConstants.ID.get()); + Document newDoc = DocumentUtil.createDocument(); + Node importedNode = newDoc.importNode(enc, true); + newDoc.appendChild(importedNode); + + Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey); + SAMLParser parser = new SAMLParser(); + + JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement); + AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil + .getNodeAsStream(decryptedDocumentElement))); + + responseType.replaceAssertion(oldID, new ResponseType.RTChoiceType(assertion)); + + return responseType; + } catch (Exception e) { + throw new IdentityBrokerException("Could not decrypt assertion.", e); + } + } + + + } + + protected class PostBinding extends Binding { + @Override + protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException { + SamlProtocolUtils.verifyDocumentSignature(documentHolder.getSamlDocument(), getIDPKey()); + } + + @Override + protected SAMLDocumentHolder extractRequestDocument(String samlRequest) { + return SAMLRequestParser.parseRequestPostBinding(samlRequest); + } + @Override + protected SAMLDocumentHolder extractResponseDocument(String response) { + return SAMLRequestParser.parseResponsePostBinding(response); + } + + @Override + protected String getBindingType() { + return SamlProtocol.SAML_POST_BINDING; + } + } + + protected class RedirectBinding extends Binding { + @Override + protected void verifySignature(SAMLDocumentHolder documentHolder) throws VerificationException { + PublicKey publicKey = getIDPKey(); + SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo); + } + + + + @Override + protected SAMLDocumentHolder extractRequestDocument(String samlRequest) { + return SAMLRequestParser.parseRequestRedirectBinding(samlRequest); + } + + @Override + protected SAMLDocumentHolder extractResponseDocument(String response) { + return SAMLRequestParser.parseRequestRedirectBinding(response); + } + + @Override + protected String getBindingType() { + return SamlProtocol.SAML_REDIRECT_BINDING; + } + + } + +} diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java index 7104cb271b..7a9bc3b1fc 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -23,13 +23,19 @@ import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; import org.keycloak.broker.provider.FederatedIdentity; import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.saml.SAML2AuthnRequestBuilder; +import org.keycloak.protocol.saml.SAML2LogoutRequestBuilder; import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder; +import org.keycloak.services.managers.EventsManager; import org.picketlink.common.constants.JBossSAMLConstants; import org.picketlink.common.constants.JBossSAMLURIConstants; +import org.picketlink.common.exceptions.ConfigurationException; +import org.picketlink.common.exceptions.ParsingException; import org.picketlink.common.exceptions.ProcessingException; import org.picketlink.common.util.DocumentUtil; import org.picketlink.common.util.StaxParserUtil; @@ -63,6 +69,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.xml.namespace.QName; +import java.io.IOException; import java.net.URLDecoder; import java.security.KeyPair; import java.security.PrivateKey; @@ -84,6 +91,11 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider" + getConfig().getNameIDPolicyFormat() + "\n" + " \n" + // todo single logout service description -// " \n" + + " \n" + " \n"; if (getConfig().isWantAuthnRequestsSigned()) { descriptor += diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java old mode 100644 new mode 100755 index 98ebb28c4d..10ef46441b --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -39,6 +39,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel { getConfig().put("singleSignOnServiceUrl", singleSignOnServiceUrl); } + public String getSingleLogoutServiceUrl() { + return getConfig().get("singleLogoutServiceUrl"); + } + + public void setSingleLogoutServiceUrl(String singleLogoutServiceUrl) { + getConfig().put("singleLogoutServiceUrl", singleLogoutServiceUrl); + } + public boolean isValidateSignature() { return Boolean.valueOf(getConfig().get("validateSignature")); } diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java index 2370a16215..559b4152f8 100755 --- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java @@ -56,7 +56,7 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory } @Override - public Map parseConfig(InputStream inputStream) { + public Map parseConfig(InputStream inputStream) { try { Object parsedObject = new SAMLParser().parse(inputStream); EntityDescriptorType entityType; @@ -90,6 +90,18 @@ public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory singleSignOnServiceUrl = endpoint.getLocation().toString(); } } + String singleLogoutServiceUrl = null; + for (EndpointType endpoint : idpDescriptor.getSingleLogoutService()) { + if (postBinding && endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())) { + singleLogoutServiceUrl = endpoint.getLocation().toString(); + break; + } else if (!postBinding && endpoint.getBinding().toString().equals(JBossSAMLURIConstants.SAML_HTTP_REDIRECT_BINDING.get())){ + singleLogoutServiceUrl = endpoint.getLocation().toString(); + break; + } + + } + samlIdentityProviderConfig.setSingleLogoutServiceUrl(singleLogoutServiceUrl); samlIdentityProviderConfig.setSingleSignOnServiceUrl(singleSignOnServiceUrl); samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned()); samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned()); diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index 282b5e4512..a02dd9cfc4 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -26,6 +26,7 @@ public interface Errors { String INVALID_REDIRECT_URI = "invalid_redirect_uri"; String INVALID_CODE = "invalid_code"; String INVALID_TOKEN = "invalid_token"; + String INVALID_SAML_RESPONSE = "invalid_saml_response"; String INVALID_SAML_AUTHN_REQUEST = "invalid_authn_request"; String INVALID_SAML_LOGOUT_REQUEST = "invalid_logout_request"; String INVALID_SAML_LOGOUT_RESPONSE = "invalid_logout_response"; diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html index e17ef151c6..4e1d88c4b9 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html @@ -44,6 +44,13 @@ +
+ +
+ +
+ +
diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java index e5d12e8ebe..cd494ee1a7 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java @@ -43,8 +43,14 @@ public class SALM2LoginResponseBuilder { protected String requestID; protected String authMethod; protected String requestIssuer; + protected String sessionIndex; + public SALM2LoginResponseBuilder sessionIndex(String sessionIndex) { + this.sessionIndex = sessionIndex; + return this; + } + public SALM2LoginResponseBuilder destination(String destination) { this.destination = destination; return this; @@ -135,8 +141,8 @@ public class SALM2LoginResponseBuilder { AuthnStatementType authnStatement = StatementUtil.createAuthnStatement(XMLTimeUtil.getIssueInstant(), authContextRef); - - authnStatement.setSessionIndex(assertion.getID()); + if (sessionIndex != null) authnStatement.setSessionIndex(sessionIndex); + else authnStatement.setSessionIndex(assertion.getID()); assertion.addStatement(authnStatement); } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java index 1d76e2f933..5cf301fc0e 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java @@ -19,6 +19,7 @@ import java.net.URI; public class SAML2LogoutRequestBuilder extends SAML2BindingBuilder { protected String userPrincipal; protected String userPrincipalFormat; + protected String sessionIndex; public SAML2LogoutRequestBuilder userPrincipal(String nameID, String nameIDformat) { this.userPrincipal = nameID; @@ -26,6 +27,11 @@ public class SAML2LogoutRequestBuilder extends SAML2BindingBuilder encodedParams = uriInformation.getQueryParameters(false); + String request = encodedParams.getFirst(GeneralConstants.SAML_REQUEST_KEY); + String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); + String signature = encodedParams.getFirst(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY); + String decodedAlgorithm = uriInformation.getQueryParameters(true).getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); + + if (request == null) throw new VerificationException("SAMLRequest as null"); + if (algorithm == null) throw new VerificationException("SigAlg as null"); + if (signature == null) throw new VerificationException("Signature as null"); + + // Shibboleth doesn't sign the document for redirect binding. + // todo maybe a flag? + + + UriBuilder builder = UriBuilder.fromPath("/") + .queryParam(GeneralConstants.SAML_REQUEST_KEY, request); + if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) { + builder.queryParam(GeneralConstants.RELAY_STATE, encodedParams.getFirst(GeneralConstants.RELAY_STATE)); + } + builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm); + String rawQuery = builder.build().getRawQuery(); + + try { + byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature); + + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.getFromXmlMethod(decodedAlgorithm); + Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg + validator.initVerify(publicKey); + validator.update(rawQuery.getBytes("UTF-8")); + if (!validator.verify(decodedSignature)) { + throw new VerificationException("Invalid query param signature"); + } + } catch (Exception e) { + throw new VerificationException(e); + } + } + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index 3e7cd2f300..3aa016cb11 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -358,7 +358,6 @@ public class SamlService { if (relayState != null) userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState); userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID()); userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding); - userSession.setNote(SamlProtocol.SAML_LOGOUT_ISSUER, logoutRequest.getIssuer().getValue()); userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL); // remove client from logout requests for (ClientSessionModel clientSession : userSession.getClientSessions()) { @@ -446,47 +445,12 @@ public class SamlService { if (!"true".equals(client.getAttribute("saml.client.signature"))) { return; } - MultivaluedMap encodedParams = uriInfo.getQueryParameters(false); - String request = encodedParams.getFirst(GeneralConstants.SAML_REQUEST_KEY); - String algorithm = encodedParams.getFirst(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY); - String signature = encodedParams.getFirst(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY); - - if (request == null) throw new VerificationException("SAMLRequest as null"); - if (algorithm == null) throw new VerificationException("SigAlg as null"); - if (signature == null) throw new VerificationException("Signature as null"); - - // Shibboleth doesn't sign the document for redirect binding. - // todo maybe a flag? - // SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument()); - PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client); - - - UriBuilder builder = UriBuilder.fromPath("/") - .queryParam(GeneralConstants.SAML_REQUEST_KEY, request); - if (encodedParams.containsKey(GeneralConstants.RELAY_STATE)) { - builder.queryParam(GeneralConstants.RELAY_STATE, encodedParams.getFirst(GeneralConstants.RELAY_STATE)); - } - builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, algorithm); - String rawQuery = builder.build().getRawQuery(); - - try { - byte[] decodedSignature = RedirectBindingUtil.urlBase64Decode(signature); - - SignatureAlgorithm signatureAlgorithm = SamlProtocol.getSignatureAlgorithm(client); - Signature validator = signatureAlgorithm.createSignature(); // todo plugin signature alg - validator.initVerify(publicKey); - validator.update(rawQuery.getBytes("UTF-8")); - if (!validator.verify(decodedSignature)) { - throw new VerificationException("Invalid query param signature"); - } - } catch (Exception e) { - throw new VerificationException(e); - } - - + SamlProtocolUtils.verifyRedirectSignature(publicKey, uriInfo); } + + @Override protected SAMLDocumentHolder extractRequestDocument(String samlRequest) { return SAMLRequestParser.parseRequestRedirectBinding(samlRequest); diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SignatureAlgorithm.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SignatureAlgorithm.java index 6e9f47e422..e169201eae 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SignatureAlgorithm.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SignatureAlgorithm.java @@ -1,6 +1,8 @@ package org.keycloak.protocol.saml; import java.security.Signature; +import java.util.HashMap; +import java.util.Map; /** * @author Bill Burke @@ -16,6 +18,29 @@ public enum SignatureAlgorithm { private final String xmlSignatureDigestMethod; private final String javaSignatureAlgorithm; + private static final Map signatureMethodMap = new HashMap<>(); + private static final Map signatureDigestMethodMap = new HashMap<>(); + + static { + signatureMethodMap.put(RSA_SHA1.getXmlSignatureMethod(), RSA_SHA1); + signatureMethodMap.put(RSA_SHA256.getXmlSignatureMethod(), RSA_SHA256); + signatureMethodMap.put(RSA_SHA512.getXmlSignatureMethod(), RSA_SHA512); + signatureMethodMap.put(DSA_SHA1.getXmlSignatureMethod(), DSA_SHA1); + + signatureDigestMethodMap.put(RSA_SHA1.getXmlSignatureDigestMethod(), RSA_SHA1); + signatureDigestMethodMap.put(RSA_SHA256.getXmlSignatureDigestMethod(), RSA_SHA256); + signatureDigestMethodMap.put(RSA_SHA512.getXmlSignatureDigestMethod(), RSA_SHA512); + signatureDigestMethodMap.put(DSA_SHA1.getXmlSignatureDigestMethod(), DSA_SHA1); + } + + public static SignatureAlgorithm getFromXmlMethod(String xml) { + return signatureMethodMap.get(xml); + } + + public static SignatureAlgorithm getFromXmlDigest(String xml) { + return signatureDigestMethodMap.get(xml); + } + SignatureAlgorithm(String xmlSignatureMethod, String xmlSignatureDigestMethod, String javaSignatureAlgorithm) { this.xmlSignatureMethod = xmlSignatureMethod; this.xmlSignatureDigestMethod = xmlSignatureDigestMethod; diff --git a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java index 0290771d7a..be1711b544 100755 --- a/services/src/main/java/org/keycloak/protocol/LoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/LoginProtocol.java @@ -1,5 +1,6 @@ package org.keycloak.protocol; +import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -24,6 +25,8 @@ public interface LoginProtocol extends Provider { LoginProtocol setHttpHeaders(HttpHeaders headers); + LoginProtocol setEventBuilder(EventBuilder event); + Response cancelLogin(ClientSessionModel clientSession); Response invalidSessionError(ClientSessionModel clientSession); Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index 1c473f9506..cdbef96267 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -24,6 +24,9 @@ package org.keycloak.protocol.oidc; import org.jboss.logging.Logger; import org.jboss.resteasy.client.core.executors.ApacheHttpClient4Executor; import org.keycloak.OAuth2Constants; +import org.keycloak.events.Details; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; import org.keycloak.models.ApplicationModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; @@ -54,6 +57,7 @@ public class OIDCLoginProtocol implements LoginProtocol { public static final String CLIENT_ID_PARAM = "client_id"; public static final String PROMPT_PARAM = "prompt"; public static final String LOGIN_HINT_PARAM = "login_hint"; + public static final String LOGOUT_REDIRECT_URI = "OIDC_LOGOUT_REDIRECT_URI"; private static final Logger log = Logger.getLogger(OIDCLoginProtocol.class); @@ -65,11 +69,14 @@ public class OIDCLoginProtocol implements LoginProtocol { protected HttpHeaders headers; - public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { + protected EventBuilder event; + + public OIDCLoginProtocol(KeycloakSession session, RealmModel realm, UriInfo uriInfo, HttpHeaders headers, EventBuilder event) { this.session = session; this.realm = realm; this.uriInfo = uriInfo; this.headers = headers; + this.event = event; } public OIDCLoginProtocol(){ @@ -100,6 +107,12 @@ public class OIDCLoginProtocol implements LoginProtocol { return this; } + @Override + public OIDCLoginProtocol setEventBuilder(EventBuilder event) { + this.event = event; + return this; + } + @Override public Response cancelLogin(ClientSessionModel clientSession) { String redirect = clientSession.getRedirectUri(); @@ -168,7 +181,19 @@ public class OIDCLoginProtocol implements LoginProtocol { @Override public Response finishLogout(UserSessionModel userSession) { - throw new RuntimeException("NOT IMPLEMENTED"); + String redirectUri = userSession.getNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI); + event.event(EventType.LOGOUT); + if (redirectUri != null) { + event.detail(Details.REDIRECT_URI, redirectUri); + } + event.user(userSession.getUser()).session(userSession).success(); + + + if (redirectUri != null) { + return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build(); + } else { + return Response.ok().build(); + } } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java old mode 100644 new mode 100755 index 54e40096d8..19ecd44104 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -37,6 +37,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setAuthorizationEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "auth").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setTokenEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "token").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setUserinfoEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "issueUserInfo").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); + config.setLogoutEndpoint(uriBuilder.clone().path(OIDCLoginProtocolService.class, "logout").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setJwksUri(uriBuilder.clone().path(OIDCLoginProtocolService.class, "certs").build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString()); config.setIdTokenSigningAlgValuesSupported(DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index b19480c00a..dd06346449 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -254,7 +254,7 @@ public class AuthorizationEndpoint { if (httpAuthOutput.getResponse() != null) return httpAuthOutput.getResponse(); if (prompt != null && prompt.equals("none")) { - OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers); + OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event); return oauth.cancelLogin(clientSession); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java old mode 100644 new mode 100755 index a47aa6a2c1..ad5b302c79 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java @@ -78,23 +78,28 @@ public class LogoutEndpoint { */ @GET @NoCache - public Response logout(final @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri) { - event.event(EventType.LOGOUT); + public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri) { if (redirectUri != null) { - event.detail(Details.REDIRECT_URI, redirectUri); + String validatedUri = RedirectUtils.verifyRealmRedirectUri(uriInfo, redirectUri, realm); + if (validatedUri == null) { + event.event(EventType.LOGOUT); + event.detail(Details.REDIRECT_URI, redirectUri); + event.error(Errors.INVALID_REDIRECT_URI); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI); + } + redirectUri = validatedUri; } + // authenticate identity cookie, but ignore an access token timeout as we're logging out anyways. AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false); if (authResult != null) { - logout(authResult.getSession()); + if (redirectUri != null) authResult.getSession().setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirectUri); + authResult.getSession().setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, OIDCLoginProtocol.LOGIN_PROTOCOL); + return AuthenticationManager.browserLogout(session, realm, authResult.getSession(), uriInfo, clientConnection, headers); } if (redirectUri != null) { - String validatedRedirect = RedirectUtils.verifyRealmRedirectUri(uriInfo, redirectUri, realm); - if (validatedRedirect == null) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_REDIRECT_URI); - } - return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build(); + return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build(); } else { return Response.ok().build(); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java old mode 100644 new mode 100755 index 0760b64c9e..0e3d4f3490 --- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java @@ -23,6 +23,9 @@ public class OIDCConfigurationRepresentation { @JsonProperty("userinfo_endpoint") private String userinfoEndpoint; + @JsonProperty("end_session_endpoint") + private String logoutEndpoint; + @JsonProperty("jwks_uri") private String jwksUri; @@ -81,6 +84,14 @@ public class OIDCConfigurationRepresentation { this.jwksUri = jwksUri; } + public String getLogoutEndpoint() { + return logoutEndpoint; + } + + public void setLogoutEndpoint(String logoutEndpoint) { + this.logoutEndpoint = logoutEndpoint; + } + public List getGrantTypesSupported() { return grantTypesSupported; } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index f37b83781d..c540baa446 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -6,6 +6,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; +import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; @@ -26,6 +27,7 @@ import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.flows.Flows; @@ -141,9 +143,6 @@ public class AuthenticationManager { } } - if (redirectClients.size() == 0) { - return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers); - } for (ClientSessionModel nextRedirectClient : redirectClients) { String authMethod = nextRedirectClient.getAuthMethod(); LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); @@ -164,18 +163,26 @@ public class AuthenticationManager { } } + String brokerId = userSession.getNote(IdentityBrokerService.BROKER_PROVIDER_ID); + if (brokerId != null) { + IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId); + Response response = identityProvider.logout(userSession, uriInfo, realm); + if (response != null) return response; + } return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers); } - protected static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { + public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { expireIdentityCookie(realm, uriInfo, connection); expireRememberMeCookie(realm, uriInfo, connection); userSession.setState(UserSessionModel.State.LOGGED_OUT); String method = userSession.getNote(KEYCLOAK_LOGOUT_PROTOCOL); + EventBuilder event = new EventsManager(realm, session, connection).createEventBuilder(); LoginProtocol protocol = session.getProvider(LoginProtocol.class, method); protocol.setRealm(realm) .setHttpHeaders(headers) - .setUriInfo(uriInfo); + .setUriInfo(uriInfo) + .setEventBuilder(event); Response response = protocol.finishLogout(userSession); session.sessions().removeUserSession(realm, userSession); return response; diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index 1d24fda5d0..6eb5707342 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -19,6 +19,7 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.ClientConnection; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; @@ -76,9 +77,10 @@ import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE; * @author Pedro Igor */ @Path("/broker") -public class IdentityBrokerService { +public class IdentityBrokerService implements IdentityProvider.Callback { private static final Logger LOGGER = Logger.getLogger(IdentityBrokerService.class); + public static final String BROKER_PROVIDER_ID = "BROKER_PROVIDER_ID"; private final RealmModel realmModel; @@ -121,8 +123,8 @@ public class IdentityBrokerService { } try { - ClientSessionCode clientSessionCode = parseClientSessionCode(code, providerId); - IdentityProvider identityProvider = getIdentityProvider(providerId); + ClientSessionCode clientSessionCode = parseClientSessionCode(code); + IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId); AuthenticationResponse authenticationResponse = identityProvider.handleRequest(createAuthenticationRequest(providerId, clientSessionCode)); Response response = authenticationResponse.getResponse(); @@ -155,6 +157,17 @@ public class IdentityBrokerService { return handleResponse(providerId); } + @Path("{provider_id}/endpoint") + public Object getEndpoint(@PathParam("provider_id") String providerId) { + IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId); + Object callback = identityProvider.callback(realmModel, this); + ResteasyProviderFactory.getInstance().injectProperties(callback); + //resourceContext.initResource(brokerService); + return callback; + + + } + @Path("{provider_id}/token") @OPTIONS public Response retrieveTokenPreflight() { @@ -195,7 +208,7 @@ public class IdentityBrokerService { .createOAuthGrant(null), clientModel); } - IdentityProvider identityProvider = getIdentityProvider(providerId); + IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId); IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(providerId); if (identityProviderConfig.isStoreToken()) { @@ -233,6 +246,79 @@ public class IdentityBrokerService { return getToken(providerId, true); } + public Response authenticated(Map userNotes, IdentityProviderModel identityProviderConfig, FederatedIdentity federatedIdentity, String code) { + ClientSessionCode clientCode = null; + try { + clientCode = parseClientSessionCode(code); + } catch (Exception e) { + return redirectToErrorPage(Messages.IDENTITY_PROVIDER_AUTHENTICATION_FAILED, e, identityProviderConfig.getProviderId()); + + } + String providerId = identityProviderConfig.getAlias(); + if (!identityProviderConfig.isStoreToken()) { + if (isDebugEnabled()) { + LOGGER.debugf("Token will not be stored for identity provider [%s].", providerId); + } + federatedIdentity.setToken(null); + } + + federatedIdentity.setIdentityProviderId(providerId); + ClientSessionModel clientSession = clientCode.getClientSession(); + FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, federatedIdentity.getId(), + federatedIdentity.getUsername(), federatedIdentity.getToken()); + + this.event.event(EventType.IDENTITY_PROVIDER_LOGIN) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) + .detail(Details.IDENTITY_PROVIDER_IDENTITY, federatedIdentity.getUsername()); + + UserModel federatedUser = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, this.realmModel); + + // Check if federatedUser is already authenticated (this means linking social into existing federatedUser account) + if (clientSession.getUserSession() != null) { + UserSessionModel userSession = clientSession.getUserSession(); + for (Map.Entry entry : userNotes.entrySet()) { + userSession.setNote(entry.getKey(), entry.getValue()); + } + return performAccountLinking(clientSession, providerId, federatedIdentityModel, federatedUser); + } + + if (federatedUser == null) { + try { + federatedUser = createUser(federatedIdentity); + + if (identityProviderConfig.isUpdateProfileFirstLogin()) { + if (isDebugEnabled()) { + LOGGER.debugf("Identity provider requires update profile action.", federatedUser); + } + federatedUser.addRequiredAction(UPDATE_PROFILE); + } + } catch (Exception e) { + return redirectToLoginPage(e, clientCode); + } + } + + updateFederatedIdentity(federatedIdentity, federatedUser); + + UserSessionModel userSession = this.session.sessions() + .createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false); + + this.event.user(federatedUser); + this.event.session(userSession); + + TokenManager.attachClientSession(userSession, clientSession); + for (Map.Entry entry : userNotes.entrySet()) { + userSession.setNote(entry.getKey(), entry.getValue()); + } + userSession.setNote(BROKER_PROVIDER_ID, providerId); + + if (isDebugEnabled()) { + LOGGER.debugf("Performing local authentication for user [%s].", federatedUser); + } + + return AuthenticationManager.nextActionAfterAuthentication(this.session, userSession, clientSession, this.clientConnection, this.request, + this.uriInfo, event); + } + private Response handleResponse(String providerId) { if (isDebugEnabled()) { LOGGER.debugf("Handling authentication response from identity provider [%s].", providerId); @@ -242,7 +328,7 @@ public class IdentityBrokerService { IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(providerId); try { - IdentityProvider identityProvider = getIdentityProvider(providerId); + IdentityProvider identityProvider = getIdentityProvider(session, realmModel, providerId); String relayState = identityProvider.getRelayState(createAuthenticationRequest(providerId, null)); if (relayState == null) { @@ -253,7 +339,7 @@ public class IdentityBrokerService { LOGGER.debugf("Relay state is valid: [%s].", relayState); } - ClientSessionCode clientSessionCode = parseClientSessionCode(relayState, providerId); + ClientSessionCode clientSessionCode = parseClientSessionCode(relayState); AuthenticationResponse authenticationResponse = identityProvider.handleResponse(createAuthenticationRequest(providerId, clientSessionCode)); Response response = authenticationResponse.getResponse(); @@ -386,7 +472,7 @@ public class IdentityBrokerService { } } - private ClientSessionCode parseClientSessionCode(String code, String providerId) { + private ClientSessionCode parseClientSessionCode(String code) { ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel); if (clientCode != null && clientCode.isValid(AUTHENTICATE)) { @@ -465,11 +551,11 @@ public class IdentityBrokerService { return Flows.errors().error(message, Status.BAD_REQUEST); } - private IdentityProvider getIdentityProvider(String alias) { - IdentityProviderModel identityProviderModel = this.realmModel.getIdentityProviderByAlias(alias); + public static IdentityProvider getIdentityProvider(KeycloakSession session, RealmModel realm, String alias) { + IdentityProviderModel identityProviderModel = realm.getIdentityProviderByAlias(alias); if (identityProviderModel != null) { - IdentityProviderFactory providerFactory = getIdentityProviderFactory(identityProviderModel); + IdentityProviderFactory providerFactory = getIdentityProviderFactory(session, identityProviderModel); if (providerFactory == null) { throw new IdentityBrokerException("Could not find factory for identity provider [" + alias + "]."); @@ -481,12 +567,12 @@ public class IdentityBrokerService { throw new IdentityBrokerException("Identity Provider [" + alias + "] not found."); } - private IdentityProviderFactory getIdentityProviderFactory(IdentityProviderModel model) { + private static IdentityProviderFactory getIdentityProviderFactory(KeycloakSession session, IdentityProviderModel model) { Map availableProviders = new HashMap(); List allProviders = new ArrayList(); - allProviders.addAll(this.session.getKeycloakSessionFactory().getProviderFactories(IdentityProvider.class)); - allProviders.addAll(this.session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class)); + allProviders.addAll(session.getKeycloakSessionFactory().getProviderFactories(IdentityProvider.class)); + allProviders.addAll(session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class)); for (ProviderFactory providerFactory : allProviders) { availableProviders.put(providerFactory.getId(), (IdentityProviderFactory) providerFactory); diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 247a430f94..0feccb47ec 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -189,7 +189,8 @@ public class RealmsResource { IdentityBrokerService brokerService = new IdentityBrokerService(realm); ResteasyProviderFactory.getInstance().injectProperties(brokerService); - + //resourceContext.initResource(brokerService); + brokerService.init(); return brokerService; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index fb782990b6..4743826502 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -157,7 +157,7 @@ public class AccountTest { }); } - @Test @Ignore + //@Test @Ignore public void runit() throws Exception { Thread.sleep(10000000); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index ae5c9bd387..7ea83480ff 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -546,7 +546,7 @@ public abstract class AbstractIdentityProviderTest { this.loginPage.clickSocial(getProviderId()); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); - + System.out.println(this.driver.getCurrentUrl()); // log in to identity provider this.loginPage.login(username, "password"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java index 77836655bd..c77e57f3b3 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.broker; import org.junit.ClassRule; +import org.junit.Test; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -60,7 +61,8 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderT try { SAML2Request saml2Request = new SAML2Request(); ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); + //.getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); assertNotNull(responseType); assertFalse(responseType.getAssertions().isEmpty()); @@ -68,4 +70,16 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderT fail("Could not parse token."); } } + + @Override + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile() { + super.testSuccessfulAuthenticationWithoutUpdateProfile(); + } + + @Override + @Test + public void testTokenStorageAndRetrievalByOAuthClient() { + super.testTokenStorageAndRetrievalByOAuthClient(); + } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java index e387097316..1d8b264de7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java @@ -60,7 +60,7 @@ public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractIdentityP try { SAML2Request saml2Request = new SAML2Request(); ResponseType responseType = (ResponseType) saml2Request - .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(pageSource, "UTF-8"))); + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(pageSource)); assertNotNull(responseType); assertFalse(responseType.getAssertions().isEmpty()); diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json index db027d8da7..844bd5efc3 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json +++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml-with-signature.json @@ -11,7 +11,7 @@ "name": "http://localhost:8081/auth/realms/realm-with-broker", "enabled": true, "redirectUris": [ - "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-signed-idp" + "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-signed-idp/endpoint" ], "attributes": { "saml.assertion.signature": "true", diff --git a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json index 5757b2887b..73d2b2d9bc 100755 --- a/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json +++ b/testsuite/integration/src/test/resources/broker-test/test-broker-realm-with-saml.json @@ -11,7 +11,7 @@ "name": "http://localhost:8081/auth/realms/realm-with-broker", "enabled": true, "redirectUris": [ - "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic" + "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint" ], "attributes": { "saml.authnstatement": "true" diff --git a/testsuite/integration/src/test/resources/saml/testsaml.json b/testsuite/integration/src/test/resources/saml/testsaml.json index cac873b807..ed67040dd8 100755 --- a/testsuite/integration/src/test/resources/saml/testsaml.json +++ b/testsuite/integration/src/test/resources/saml/testsaml.json @@ -45,10 +45,10 @@ ], "attributes": { "saml.authnstatement": "true", - "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post", - "saml_single_logout_service_url_post": "http://localhost:8081/sales-post", - "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post" + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post/" } }, { @@ -61,10 +61,10 @@ "http://localhost:8081/sales-post-sig/*" ], "attributes": { - "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig", - "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig", - "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig", + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig/", "saml.server.signature": "true", "saml.signature.algorithm": "RSA_SHA256", "saml.client.signature": "true", @@ -84,10 +84,10 @@ "http://localhost:8081/sales-post-sig-transient/*" ], "attributes": { - "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-transient", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-transient", - "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-transient", - "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-transient", + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-transient/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-transient/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-transient/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-transient/", "saml.server.signature": "true", "saml.signature.algorithm": "RSA_SHA256", "saml.client.signature": "true", @@ -106,10 +106,10 @@ "http://localhost:8081/sales-post-sig-persistent/*" ], "attributes": { - "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-persistent", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-persistent", - "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-persistent", - "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-persistent", + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-persistent/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-persistent/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-persistent/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-persistent/", "saml.server.signature": "true", "saml.signature.algorithm": "RSA_SHA256", "saml.client.signature": "true", @@ -131,10 +131,10 @@ "attributes": { "saml_force_name_id_format": "true", "saml_name_id_format": "email", - "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-email", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-email", - "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-email", - "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-email", + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-sig-email/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-sig-email/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-sig-email/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-sig-email/", "saml.server.signature": "true", "saml.signature.algorithm": "RSA_SHA256", "saml.client.signature": "true", @@ -148,8 +148,8 @@ "enabled": true, "protocol": "saml", "fullScopeAllowed": true, - "baseUrl": "http://localhost:8081/bad-realm-sales-post-sig", - "adminUrl": "http://localhost:8081/bad-realm-sales-post-sig", + "baseUrl": "http://localhost:8081/bad-realm-sales-post-sig/", + "adminUrl": "http://localhost:8081/bad-realm-sales-post-sig/", "redirectUris": [ "http://localhost:8081/bad-realm-sales-post-sig/*" ], @@ -166,8 +166,8 @@ "enabled": true, "protocol": "saml", "fullScopeAllowed": true, - "baseUrl": "http://localhost:8081/bad-client-sales-post-sig", - "adminUrl": "http://localhost:8081/bad-client-sales-post-sig", + "baseUrl": "http://localhost:8081/bad-client-sales-post-sig/", + "adminUrl": "http://localhost:8081/bad-client-sales-post-sig/", "redirectUris": [ "http://localhost:8081/bad-client-sales-post-sig/*" ], @@ -189,10 +189,10 @@ "http://localhost:8081/sales-post-enc/*" ], "attributes": { - "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-enc", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-enc", - "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-enc", - "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-enc", + "saml_assertion_consumer_url_post": "http://localhost:8081/sales-post-enc/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post-enc/", + "saml_single_logout_service_url_post": "http://localhost:8081/sales-post-enc/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post-enc/", "saml.server.signature": "true", "saml.signature.algorithm": "RSA_SHA512", "saml.client.signature": "true", @@ -213,7 +213,7 @@ "redirectUris": [ "http://localhost:8081/employee-sig/*" ], - "adminUrl": "http://localhost:8081/employee-sig", + "adminUrl": "http://localhost:8081/employee-sig/", "attributes": { "saml.server.signature": "true", "saml.client.signature": "true", @@ -279,15 +279,15 @@ "protocol": "saml", "fullScopeAllowed": true, "frontchannelLogout": true, - "baseUrl": "http://localhost:8081/employee-sig-front", + "baseUrl": "http://localhost:8081/employee-sig-front/", "redirectUris": [ "http://localhost:8081/employee-sig-front/*" ], "attributes": { - "saml_assertion_consumer_url_post": "http://localhost:8081/employee-sig-front", - "saml_assertion_consumer_url_redirect": "http://localhost:8081/employee-sig-front", - "saml_single_logout_service_url_post": "http://localhost:8081/employee-sig-front", - "saml_single_logout_service_url_redirect": "http://localhost:8081/employee-sig-front", + "saml_assertion_consumer_url_post": "http://localhost:8081/employee-sig-front/", + "saml_assertion_consumer_url_redirect": "http://localhost:8081/employee-sig-front/", + "saml_single_logout_service_url_post": "http://localhost:8081/employee-sig-front/", + "saml_single_logout_service_url_redirect": "http://localhost:8081/employee-sig-front/", "saml.server.signature": "true", "saml.client.signature": "true", "saml.signature.algorithm": "RSA_SHA1",