From c91ecc30335f49e604a1d0abf1153d3bff669200 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 17 Oct 2014 16:48:45 -0400 Subject: [PATCH] saml redirect binding --- .../main/java/org/keycloak/events/Errors.java | 1 + .../java/org/keycloak/events/EventType.java | 3 +- ...er.java => SALM2LoginResponseBuilder.java} | 42 +- ...gBuilder.java => SAML2BindingBuilder.java} | 112 ++- ...er.java => SAML2ErrorResponseBuilder.java} | 20 +- ...er.java => SAML2LogoutRequestBuilder.java} | 16 +- .../keycloak/protocol/saml/SalmProtocol.java | 32 +- .../protocol/saml/SamlProtocolUtils.java | 21 +- .../keycloak/protocol/saml/SamlService.java | 447 ++++++--- .../protocol/saml/SignatureAlgorithm.java | 45 + .../testsuite/account/AccountTest.java | 946 +++++++++--------- .../src/test/resources/testsaml.json | 18 + 12 files changed, 977 insertions(+), 726 deletions(-) rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SALM2PostBindingLoginResponseBuilder.java => SALM2LoginResponseBuilder.java} (74%) rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SAML2PostBindingBuilder.java => SAML2BindingBuilder.java} (64%) rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SAML2PostBindingErrorResponseBuilder.java => SAML2ErrorResponseBuilder.java} (76%) rename saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/{SAML2PostBindingLogoutResponseBuilder.java => SAML2LogoutRequestBuilder.java} (67%) create mode 100755 saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SignatureAlgorithm.java 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 e7d7e1a203..e3933c00db 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -24,6 +24,7 @@ public interface Errors { String INVALID_REDIRECT_URI = "invalid_redirect_uri"; String INVALID_CODE = "invalid_code"; String INVALID_TOKEN = "invalid_token"; + String INVALID_SIGNATURE = "invalid_signature"; String INVALID_REGISTRATION = "invalid_registration"; String INVALID_FORM = "invalid_form"; diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index 917b772aba..049d001178 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -41,6 +41,7 @@ public enum EventType { SEND_RESET_PASSWORD, SEND_RESET_PASSWORD_ERROR, SOCIAL_LOGIN, - SOCIAL_LOGIN_ERROR + SOCIAL_LOGIN_ERROR, + INVALID_SIGNATURE_ERROR } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2PostBindingLoginResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java similarity index 74% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2PostBindingLoginResponseBuilder.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java index e067204da0..6ed495d989 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2PostBindingLoginResponseBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java @@ -2,14 +2,12 @@ package org.keycloak.protocol.saml; import org.picketlink.common.PicketLinkLogger; import org.picketlink.common.PicketLinkLoggerFactory; -import org.picketlink.common.constants.GeneralConstants; import org.picketlink.common.constants.JBossSAMLURIConstants; import org.picketlink.common.exceptions.ConfigurationException; import org.picketlink.common.exceptions.ProcessingException; import org.picketlink.common.util.DocumentUtil; import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator; -import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory; import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder; import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder; import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder; @@ -19,14 +17,8 @@ import org.picketlink.identity.federation.saml.v2.assertion.AssertionType; import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType; import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType; import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; -import org.picketlink.identity.federation.web.util.PostBindingUtil; import org.w3c.dom.Document; -import javax.ws.rs.core.CacheControl; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import java.io.IOException; -import java.io.StringWriter; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -42,7 +34,7 @@ import static org.picketlink.common.util.StringUtil.isNotNull; * @author Anil.Saldhana@redhat.com * @author bburke@redhat.com */ -public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilder { +public class SALM2LoginResponseBuilder extends SAML2BindingBuilder { protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger(); protected List roles = new LinkedList(); @@ -55,65 +47,66 @@ public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilde protected Map attributes = new HashMap(); - public SALM2PostBindingLoginResponseBuilder attributes(Map attributes) { + public SALM2LoginResponseBuilder attributes(Map attributes) { this.attributes = attributes; return this; } - public SALM2PostBindingLoginResponseBuilder attribute(String name, Object value) { + public SALM2LoginResponseBuilder attribute(String name, Object value) { this.attributes.put(name, value); return this; } - public SALM2PostBindingLoginResponseBuilder requestID(String requestID) { + public SALM2LoginResponseBuilder requestID(String requestID) { this.requestID =requestID; return this; } - public SALM2PostBindingLoginResponseBuilder requestIssuer(String requestIssuer) { + public SALM2LoginResponseBuilder requestIssuer(String requestIssuer) { this.requestIssuer =requestIssuer; return this; } - public SALM2PostBindingLoginResponseBuilder roles(List roles) { + public SALM2LoginResponseBuilder roles(List roles) { this.roles = roles; return this; } - public SALM2PostBindingLoginResponseBuilder roles(String... roles) { + public SALM2LoginResponseBuilder roles(String... roles) { for (String role : roles) { this.roles.add(role); } return this; } - public SALM2PostBindingLoginResponseBuilder authMethod(String authMethod) { + public SALM2LoginResponseBuilder authMethod(String authMethod) { this.authMethod = authMethod; return this; } - public SALM2PostBindingLoginResponseBuilder userPrincipal(String userPrincipal) { + public SALM2LoginResponseBuilder userPrincipal(String userPrincipal) { this.userPrincipal = userPrincipal; return this; } - public SALM2PostBindingLoginResponseBuilder multiValuedRoles(boolean multiValuedRoles) { + public SALM2LoginResponseBuilder multiValuedRoles(boolean multiValuedRoles) { this.multiValuedRoles = multiValuedRoles; return this; } - public SALM2PostBindingLoginResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) { + public SALM2LoginResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) { this.disableAuthnStatement = disableAuthnStatement; return this; } - public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException { - Document responseDoc = getResponse(); - return buildResponse(responseDoc); + public BindingBuilder binding() throws ConfigurationException, ProcessingException { + + Document samlResponseDocument = buildDocument(); + + return new BindingBuilder(samlResponseDocument); } - public Document getResponse() throws ConfigurationException, ProcessingException { - + public Document buildDocument() throws ConfigurationException, ProcessingException { Document samlResponseDocument = null; ResponseType responseType = null; @@ -175,7 +168,6 @@ public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilde } encryptAndSign(samlResponseDocument); - return samlResponseDocument; } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java similarity index 64% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java index 07cd939190..ab0686ce7a 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2BindingBuilder.java @@ -9,6 +9,7 @@ import org.picketlink.common.util.DocumentUtil; import org.picketlink.identity.federation.core.util.XMLEncryptionUtil; import org.picketlink.identity.federation.core.wstrust.WSTrustUtil; import org.picketlink.identity.federation.web.util.PostBindingUtil; +import org.picketlink.identity.federation.web.util.RedirectBindingUtil; import org.w3c.dom.Document; import org.w3c.dom.Node; @@ -17,11 +18,14 @@ import javax.crypto.spec.SecretKeySpec; import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; import javax.xml.namespace.QName; import java.io.IOException; +import java.net.URI; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; +import java.security.Signature; import java.security.cert.X509Certificate; import static org.picketlink.common.util.StringUtil.isNotNull; @@ -30,12 +34,11 @@ import static org.picketlink.common.util.StringUtil.isNotNull; * @author Bill Burke * @version $Revision: 1 $ */ -public class SAML2PostBindingBuilder { +public class SAML2BindingBuilder { protected KeyPair signingKeyPair; protected X509Certificate signingCertificate; protected boolean signed; - protected String signatureDigestMethod; - protected String signatureMethod; + protected SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSA_SHA1; protected String relayState; protected String destination; protected String responseIssuer; @@ -70,6 +73,11 @@ public class SAML2PostBindingBuilder { return (T)this; } + public T signatureAlgorithm(SignatureAlgorithm alg) { + this.signatureAlgorithm = alg; + return (T)this; + } + public T encrypt(PublicKey publicKey) { encrypt = true; encryptionPublicKey = publicKey; @@ -86,16 +94,6 @@ public class SAML2PostBindingBuilder { return (T)this; } - public T signatureDigestMethod(String method) { - this.signatureDigestMethod = method; - return (T)this; - } - - public T signatureMethod(String method) { - this.signatureMethod = method; - return (T)this; - } - public T destination(String destination) { this.destination = destination; return (T)this; @@ -111,6 +109,37 @@ public class SAML2PostBindingBuilder { return (T)this; } + public class BindingBuilder { + protected Document document; + + public BindingBuilder(Document document) { + this.document = document; + } + + public Document getDocument() { + return document; + } + public Response postResponse() throws ConfigurationException, ProcessingException, IOException { + return buildResponse(document); + } + + public URI redirectResponseUri() throws ConfigurationException, ProcessingException, IOException { + return generateRedirectUri("SAMLResponse", document); + } + + public Response redirectResponse() throws ProcessingException, ConfigurationException, IOException { + URI uri = redirectResponseUri(); + + CacheControl cacheControl = new CacheControl(); + cacheControl.setNoCache(true); + return Response.status(302).location(uri) + .header("Pragma", "no-cache") + .header("Cache-Control", "no-cache, no-store").build(); + } + + } + + private String getSAMLNSPrefix(Document samlResponseDocument) { Node assertionElement = samlResponseDocument.getDocumentElement() .getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0); @@ -155,15 +184,25 @@ public class SAML2PostBindingBuilder { } protected void signDocument(Document samlDocument) throws ProcessingException { - SamlProtocolUtils.signDocument(samlDocument, signingKeyPair, signatureMethod, signatureDigestMethod, signingCertificate); + SamlProtocolUtils.signDocument(samlDocument, signingKeyPair, signatureAlgorithm.getXmlSignatureMethod(), signatureAlgorithm.getXmlSignatureDigestMethod(), signingCertificate); } protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException { + String str = buildHtmlPostResponse(responseDoc); + + CacheControl cacheControl = new CacheControl(); + cacheControl.setNoCache(true); + return Response.ok(str, MediaType.TEXT_HTML_TYPE) + .header("Pragma", "no-cache") + .header("Cache-Control", "no-cache, no-store").build(); + } + + protected String buildHtmlPostResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException { byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8"); String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes)); if (destination == null) { - throw SALM2PostBindingLoginResponseBuilder.logger.nullValueError("Destination is null"); + throw SALM2LoginResponseBuilder.logger.nullValueError("Destination is null"); } StringBuilder builder = new StringBuilder(); @@ -190,13 +229,42 @@ public class SAML2PostBindingBuilder { builder.append(""); - String str = builder.toString(); - - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoCache(true); - return Response.ok(str, MediaType.TEXT_HTML_TYPE) - .header("Pragma", "no-cache") - .header("Cache-Control", "no-cache, no-store").build(); + return builder.toString(); } + protected String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException { + byte[] responseBytes = org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil.getDocumentAsString(document).getBytes("UTF-8"); + + return RedirectBindingUtil.deflateBase64URLEncode(responseBytes); + } + + + protected URI generateRedirectUri(String samlParameterName, Document document) throws ConfigurationException, ProcessingException, IOException { + UriBuilder builder = UriBuilder.fromUri(destination) + .replaceQuery(null) + .queryParam(samlParameterName, base64Encoded(document)); + if (relayState != null) { + builder.queryParam("RelayState", relayState); + } + + if (signed) { + builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, signatureAlgorithm.getJavaSignatureAlgorithm()); + URI uri = builder.build(); + String rawQuery = uri.getRawQuery(); + Signature signature = signatureAlgorithm.createSignature(); + byte[] sig = new byte[0]; + try { + signature.initSign(signingKeyPair.getPrivate()); + signature.update(rawQuery.getBytes("UTF-8")); + sig = signature.sign(); + } catch (Exception e) { + throw new ProcessingException(e); + } + String encodedSig = RedirectBindingUtil.base64URLEncode(sig); + builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig); + } + return builder.build(); + } + + } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2ErrorResponseBuilder.java similarity index 76% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2ErrorResponseBuilder.java index 727cd63c49..8d7a8158ca 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingErrorResponseBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2ErrorResponseBuilder.java @@ -12,17 +12,13 @@ import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder; import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; import org.w3c.dom.Document; -import javax.ws.rs.core.Response; -import java.io.IOException; -import java.io.StringWriter; - /** * @author Bill Burke * @version $Revision: 1 $ */ -public class SAML2PostBindingErrorResponseBuilder extends SAML2PostBindingBuilder { +public class SAML2ErrorResponseBuilder extends SAML2BindingBuilder { - public Document getErrorResponse(String status) throws ProcessingException { + public Document buildDocument(String status) throws ProcessingException { Document samlResponse = null; ResponseType responseType = null; @@ -49,7 +45,11 @@ public class SAML2PostBindingErrorResponseBuilder extends SAML2PostBindingBuilde return samlResponse; } - public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException { - Document doc = getErrorResponse(status); - return buildResponse(doc); - }} + public BindingBuilder binding(String status) throws ConfigurationException, ProcessingException { + + Document samlResponseDocument = buildDocument(status); + + return new BindingBuilder(samlResponseDocument); + } + +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java similarity index 67% rename from saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java rename to saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java index d663237189..39c23c7709 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2PostBindingLogoutResponseBuilder.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2LogoutRequestBuilder.java @@ -2,37 +2,25 @@ package org.keycloak.protocol.saml; 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.identity.federation.api.saml.v2.request.SAML2Request; -import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; -import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator; -import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory; -import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder; -import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder; -import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder; import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil; import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil; import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS; import org.picketlink.identity.federation.saml.v2.assertion.NameIDType; import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType; -import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; import org.picketlink.identity.federation.web.util.PostBindingUtil; import org.w3c.dom.Document; -import javax.ws.rs.core.Response; -import java.io.IOException; import java.net.URI; -import java.security.KeyPair; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class SAML2PostBindingLogoutResponseBuilder extends SAML2PostBindingBuilder { +public class SAML2LogoutRequestBuilder extends SAML2BindingBuilder { protected String userPrincipal; - public SAML2PostBindingLogoutResponseBuilder userPrincipal(String userPrincipal) { + public SAML2LogoutRequestBuilder userPrincipal(String userPrincipal) { this.userPrincipal = userPrincipal; return this; } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java index 8a1222f766..d5c5b4bdbc 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java @@ -38,6 +38,7 @@ public class SalmProtocol implements LoginProtocol { public static final String LOGIN_PROTOCOL = "saml"; public static final String SAML_BINDING = "saml_binding"; public static final String SAML_POST_BINDING = "post"; + public static final String SAML_GET_BINDING = "get"; protected KeycloakSession session; @@ -80,33 +81,34 @@ public class SalmProtocol implements LoginProtocol { } protected Response getErrorResponse(ClientSessionModel clientSession, String status) { - SAML2PostBindingErrorResponseBuilder builder = new SAML2PostBindingErrorResponseBuilder() + SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder() .relayState(clientSession.getNote(GeneralConstants.RELAY_STATE)) .destination(clientSession.getRedirectUri()) .responseIssuer(getResponseIssuer(realm)); try { - return builder.buildErrorResponse(status); + if (isPostBinding(clientSession)) { + return builder.binding(status).postResponse(); + } else { + return builder.binding(status).redirectResponse(); + } } catch (Exception e) { return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); } } + protected boolean isPostBinding(ClientSessionModel clientSession) { + return SalmProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SalmProtocol.SAML_BINDING)); + } + @Override public Response authenticated(UserSessionModel userSession, ClientSessionCode accessCode) { ClientSessionModel clientSession = accessCode.getClientSession(); - if (SalmProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SalmProtocol.SAML_BINDING))) { - return postBinding(userSession, clientSession); - } - throw new RuntimeException("still need to implement redirect binding"); - } - - protected Response postBinding(UserSessionModel userSession, ClientSessionModel clientSession) { String requestID = clientSession.getNote("REQUEST_ID"); String relayState = clientSession.getNote(GeneralConstants.RELAY_STATE); String redirectUri = clientSession.getRedirectUri(); String responseIssuer = getResponseIssuer(realm); - SALM2PostBindingLoginResponseBuilder builder = new SALM2PostBindingLoginResponseBuilder(); + SALM2LoginResponseBuilder builder = new SALM2LoginResponseBuilder(); builder.requestID(requestID) .relayState(relayState) .destination(redirectUri) @@ -138,7 +140,11 @@ public class SalmProtocol implements LoginProtocol { builder.encrypt(publicKey); } try { - return builder.buildLoginResponse(); + if (isPostBinding(clientSession)) { + return builder.binding().postResponse(); + } else { + return builder.binding().redirectResponse(); + } } catch (Exception e) { logger.error("failed", e); return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Failed to process response"); @@ -153,7 +159,7 @@ public class SalmProtocol implements LoginProtocol { return "true".equals(client.getAttribute("samlEncrypt")); } - public void initClaims(SALM2PostBindingLoginResponseBuilder builder, ClientModel model, UserModel user) { + public void initClaims(SALM2LoginResponseBuilder builder, ClientModel model, UserModel user) { if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) { builder.attribute(X500SAMLProfileConstants.EMAIL_ADDRESS.getFriendlyName(), user.getEmail()); } @@ -176,7 +182,7 @@ public class SalmProtocol implements LoginProtocol { ApplicationModel app = (ApplicationModel)client; if (app.getManagementUrl() == null) return; - SAML2PostBindingLogoutResponseBuilder logoutBuilder = new SAML2PostBindingLogoutResponseBuilder() + SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder() .userPrincipal(userSession.getUser().getUsername()) .destination(client.getClientId()); if (requiresRealmSignature(client)) { diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java index 342f089887..e4c586a24d 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlProtocolUtils.java @@ -18,11 +18,22 @@ import java.security.cert.X509Certificate; */ public class SamlProtocolUtils { - public static void verifyPostBindingSignature(ClientModel client, Document document) throws VerificationException { + public static void verifyDocumentSignature(ClientModel client, Document document) throws VerificationException { if (!"true".equals(client.getAttribute("samlClientSignature"))) { return; } SAML2Signature saml2Signature = new SAML2Signature(); + PublicKey publicKey = getPublicKey(client); + try { + if (!saml2Signature.validate(document, publicKey)) { + throw new VerificationException("Invalid signature on document"); + } + } catch (ProcessingException e) { + throw new VerificationException("Error validating signature", e); + } + } + + public static PublicKey getPublicKey(ClientModel client) throws VerificationException { String publicKeyPem = client.getAttribute(ClientModel.PUBLIC_KEY); if (publicKeyPem == null) throw new VerificationException("Client does not have a public key."); PublicKey publicKey = null; @@ -31,13 +42,7 @@ public class SamlProtocolUtils { } catch (Exception e) { throw new VerificationException("Could not decode public key", e); } - try { - if (!saml2Signature.validate(document, publicKey)) { - throw new VerificationException("Invalid signature on document"); - } - } catch (ProcessingException e) { - throw new VerificationException("Error validating signature", e); - } + return publicKey; } public static void signDocument(Document samlDocument, KeyPair signingKeyPair, String signatureMethod, String signatureDigestMethod, X509Certificate signingCertificate) throws ProcessingException { 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 708010e2e2..cb27bf90b0 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 @@ -26,12 +26,13 @@ import org.picketlink.identity.federation.saml.v2.SAML2Object; import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType; import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType; import org.picketlink.identity.federation.saml.v2.protocol.RequestAbstractType; -import org.w3c.dom.Document; +import org.picketlink.identity.federation.web.util.RedirectBindingUtil; import javax.ws.rs.Consumes; import javax.ws.rs.FormParam; +import javax.ws.rs.GET; import javax.ws.rs.POST; -import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; @@ -41,7 +42,11 @@ import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; +import java.io.IOException; import java.net.URI; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; /** * Resource class for the oauth/openid connect token service @@ -85,172 +90,292 @@ public class SamlService { this.authManager = authManager; } + public abstract class BindingProtocol { + protected Response basicChecks(String samlRequest, String samlResponse) { + if (!checkSsl()) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.SSL_REQUIRED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); + } + if (!realm.isEnabled()) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.REALM_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); + } + + if (samlRequest == null && samlResponse == null) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.INVALID_TOKEN); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + + } + return null; + } + + protected Response handleSamlResponse(String samleResponse, String relayState) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.INVALID_TOKEN); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + } + + protected Response handleSamlRequest(String samlRequest, String relayState) { + SAMLDocumentHolder documentHolder = extractDocument(samlRequest); + if (documentHolder == null) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.INVALID_TOKEN); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + } + + SAML2Object samlObject = documentHolder.getSamlObject(); + + RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject; + String issuer = requestAbstractType.getIssuer().getValue(); + ClientModel client = realm.findClient(issuer); + + if (client == null) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.CLIENT_NOT_FOUND); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); + } + + if (!client.isEnabled()) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.CLIENT_DISABLED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); + } + if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.NOT_ALLOWED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); + } + if (client.isDirectGrantsOnly()) { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.NOT_ALLOWED); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); + } + + try { + verifySignature(documentHolder, client); + } catch (VerificationException e) { + SamlService.logger.error("request validation failed", e); + event.event(EventType.LOGIN_ERROR); + event.error(Errors.INVALID_SIGNATURE); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid requester."); + } + if (samlObject instanceof AuthnRequestType) { + event.event(EventType.LOGIN); + // Get the SAML Request Message + AuthnRequestType authn = (AuthnRequestType) samlObject; + return loginRequest(relayState, authn, client); + } else if (samlObject instanceof LogoutRequestType) { + event.event(EventType.LOGOUT); + LogoutRequestType logout = (LogoutRequestType) samlObject; + return logoutRequest(logout, client); + + } else { + event.event(EventType.LOGIN_ERROR); + event.error(Errors.INVALID_TOKEN); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); + } + } + + protected abstract void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException; + + protected abstract SAMLDocumentHolder extractDocument(String samlRequest); + + protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) { + + URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); + String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); + + if (redirect == null) { + event.error(Errors.INVALID_REDIRECT_URI); + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); + } + + + ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); + clientSession.setAuthMethod(SalmProtocol.LOGIN_PROTOCOL); + clientSession.setRedirectUri(redirect); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setNote(SalmProtocol.SAML_BINDING, getBindingType()); + clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); + clientSession.setNote("REQUEST_ID", requestAbstractType.getID()); + + Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); + if (response != null) return response; + + LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) + .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode()); + + String rememberMeUsername = AuthenticationManager.getRememberMeUsername(realm, headers); + + if (rememberMeUsername != null) { + MultivaluedMap formData = new MultivaluedMapImpl(); + formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); + formData.add("rememberMe", "on"); + + forms.setFormData(formData); + } + + return forms.createLogin(); + } + + protected abstract String getBindingType(); + + protected Response logoutRequest(LogoutRequestType requestAbstractType, ClientModel client) { + // 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()); + } + + String redirectUri = null; + + if (client instanceof ApplicationModel) { + redirectUri = ((ApplicationModel)client).getBaseUrl(); + } + + if (redirectUri != null) { + String validatedRedirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);; + if (validatedRedirect == null) { + return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); + } + return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build(); + } else { + return Response.ok().build(); + } + + } + + private void logout(UserSessionModel userSession) { + authManager.logout(session, realm, userSession, uriInfo, clientConnection); + event.user(userSession.getUser()).session(userSession).success(); + } + + private boolean checkSsl() { + if (uriInfo.getBaseUri().getScheme().equals("https")) { + return true; + } else { + return !realm.getSslRequired().isRequired(clientConnection); + } + } + } + + + protected class PostBindingProtocol extends BindingProtocol { + + + @Override + protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException { + SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument()); + } + + @Override + protected SAMLDocumentHolder extractDocument(String samlRequest) { + return SAMLRequestParser.parsePostBinding(samlRequest); + } + + @Override + protected String getBindingType() { + return SalmProtocol.SAML_POST_BINDING; + } + + + public Response execute(String samlRequest, String samlResponse, String relayState) { + Response response = basicChecks(samlRequest, samlResponse); + if (response != null) return response; + if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); + else return handleSamlResponse(samlResponse, relayState); + } + + } + + protected class RedirectBindingProtocol extends BindingProtocol { + + @Override + protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException { + if (!"true".equals(client.getAttribute("samlClientSignature"))) { + 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"); + + SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument()); + + PublicKey publicKey = SamlProtocolUtils.getPublicKey(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); + + Signature validator = SignatureAlgorithm.RSA_SHA1.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); + } + + + } + + @Override + protected SAMLDocumentHolder extractDocument(String samlRequest) { + return SAMLRequestParser.parseRedirectBinding(samlRequest); + } + + @Override + protected String getBindingType() { + return SalmProtocol.SAML_GET_BINDING; + } + + + public Response execute(String samlRequest, String samlResponse, String relayState) { + Response response = basicChecks(samlRequest, samlResponse); + if (response != null) return response; + if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); + else return handleSamlResponse(samlResponse, relayState); + } + + } + + + /** + */ + @GET + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + 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 RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState); + } + + /** */ - @Path("POST") @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) { - if (!checkSsl()) { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.SSL_REQUIRED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "HTTPS required"); - } - if (!realm.isEnabled()) { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.REALM_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Realm not enabled"); - } - - if (samlRequest == null && samlResponse == null) { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); - - } - - if (samlRequest != null) return handleSamlRequest(samlRequest, relayState); - else return handleSamlResponse(samlResponse, relayState); + return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState); } - protected Response handleSamlResponse(String samleResponse, String relayState) { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); - } - - - protected Response handleSamlRequest(String samlRequest, String relayState) { - SAMLDocumentHolder documentHolder = SAMLRequestParser.parsePostBinding(samlRequest); - if (documentHolder == null) { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); - } - - SAML2Object samlObject = documentHolder.getSamlObject(); - - RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject; - String issuer = requestAbstractType.getIssuer().getValue(); - ClientModel client = realm.findClient(issuer); - - if (client == null) { - event.error(Errors.CLIENT_NOT_FOUND); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester."); - } - - if (!client.isEnabled()) { - event.error(Errors.CLIENT_DISABLED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled."); - } - if ((client instanceof ApplicationModel) && ((ApplicationModel)client).isBearerOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Bearer-only applications are not allowed to initiate browser login"); - } - if (client.isDirectGrantsOnly()) { - event.error(Errors.NOT_ALLOWED); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "direct-grants-only clients are not allowed to initiate browser login"); - } - - try { - SamlProtocolUtils.verifyPostBindingSignature(client, documentHolder.getSamlDocument()); - } catch (VerificationException e) { - logger.error("request validation failed", e); - event.error(Errors.INVALID_CLIENT); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid requester."); - } - if (samlObject instanceof AuthnRequestType) { - event.event(EventType.LOGIN); - // Get the SAML Request Message - AuthnRequestType authn = (AuthnRequestType) samlObject; - return loginRequest(relayState, authn, client); - } else if (samlObject instanceof LogoutRequestType) { - event.event(EventType.LOGOUT); - LogoutRequestType logout = (LogoutRequestType) samlObject; - return logoutRequest(logout, client); - - } else { - event.event(EventType.LOGIN_ERROR); - event.error(Errors.INVALID_TOKEN); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid Request"); - } - } - - protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) { - - URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL(); - String redirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client); - - if (redirect == null) { - event.error(Errors.INVALID_REDIRECT_URI); - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect_uri."); - } - - - ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); - clientSession.setAuthMethod(SalmProtocol.LOGIN_PROTOCOL); - clientSession.setRedirectUri(redirect); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); - clientSession.setNote(SalmProtocol.SAML_BINDING, SalmProtocol.SAML_POST_BINDING); - clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); - clientSession.setNote("REQUEST_ID", requestAbstractType.getID()); - - Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); - if (response != null) return response; - - LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo) - .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode()); - - String rememberMeUsername = AuthenticationManager.getRememberMeUsername(realm, headers); - - if (rememberMeUsername != null) { - MultivaluedMap formData = new MultivaluedMapImpl(); - formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); - formData.add("rememberMe", "on"); - - forms.setFormData(formData); - } - - return forms.createLogin(); - } - - protected Response logoutRequest(LogoutRequestType requestAbstractType, ClientModel client) { - // 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()); - } - - String redirectUri = null; - - if (client instanceof ApplicationModel) { - redirectUri = ((ApplicationModel)client).getBaseUrl(); - } - - if (redirectUri != null) { - String validatedRedirect = OpenIDConnectService.verifyRedirectUri(uriInfo, redirectUri, realm, client);; - if (validatedRedirect == null) { - return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Invalid redirect uri."); - } - return Response.status(302).location(UriBuilder.fromUri(validatedRedirect).build()).build(); - } else { - return Response.ok().build(); - } - - } - - private void logout(UserSessionModel userSession) { - authManager.logout(session, realm, userSession, uriInfo, clientConnection); - event.user(userSession.getUser()).session(userSession).success(); - } - - private boolean checkSsl() { - if (uriInfo.getBaseUri().getScheme().equals("https")) { - return true; - } else { - return !realm.getSslRequired().isRequired(clientConnection); - } - } } 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 new file mode 100755 index 0000000000..177ae9c1d1 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SignatureAlgorithm.java @@ -0,0 +1,45 @@ +package org.keycloak.protocol.saml; + +import java.security.NoSuchAlgorithmException; +import java.security.Signature; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public enum SignatureAlgorithm { + RSA_SHA1("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "http://www.w3.org/2000/09/xmldsig#sha1", "SHA1withRSA"), + RSA_SHA256("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "http://www.w3.org/2001/04/xmlenc#sha256", "SHA256withRSA"), + RSA_SHA512("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "http://www.w3.org/2001/04/xmlenc#sha512", "SHA512withRSA"), + DSA_SHA1("http://www.w3.org/2000/09/xmldsig#dsa-sha1", "http://www.w3.org/2000/09/xmldsig#sha1", "SHA1withDSA") + ; + private final String xmlSignatureMethod; + private final String xmlSignatureDigestMethod; + private final String javaSignatureAlgorithm; + + SignatureAlgorithm(String xmlSignatureMethod, String xmlSignatureDigestMethod, String javaSignatureAlgorithm) { + this.xmlSignatureMethod = xmlSignatureMethod; + this.xmlSignatureDigestMethod = xmlSignatureDigestMethod; + this.javaSignatureAlgorithm = javaSignatureAlgorithm; + } + + public String getXmlSignatureMethod() { + return xmlSignatureMethod; + } + + public String getXmlSignatureDigestMethod() { + return xmlSignatureDigestMethod; + } + + public String getJavaSignatureAlgorithm() { + return javaSignatureAlgorithm; + } + + public Signature createSignature() { + try { + return Signature.getInstance(javaSignatureAlgorithm); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} 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 620f5e2a9c..cffdda8386 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 @@ -1,472 +1,474 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.testsuite.account; - -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.keycloak.events.Details; -import org.keycloak.events.Event; -import org.keycloak.events.EventType; -import org.keycloak.models.ApplicationModel; -import org.keycloak.models.PasswordPolicy; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserCredentialModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.TimeBasedOTP; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.services.managers.RealmManager; -import org.keycloak.services.resources.AccountService; -import org.keycloak.services.resources.RealmsResource; -import org.keycloak.testsuite.AssertEvents; -import org.keycloak.testsuite.OAuthClient; -import org.keycloak.testsuite.pages.AccountLogPage; -import org.keycloak.testsuite.pages.AccountPasswordPage; -import org.keycloak.testsuite.pages.AccountSessionsPage; -import org.keycloak.testsuite.pages.AccountTotpPage; -import org.keycloak.testsuite.pages.AccountUpdateProfilePage; -import org.keycloak.testsuite.pages.AppPage; -import org.keycloak.testsuite.pages.AppPage.RequestType; -import org.keycloak.testsuite.pages.ErrorPage; -import org.keycloak.testsuite.pages.LoginPage; -import org.keycloak.testsuite.pages.RegisterPage; -import org.keycloak.testsuite.rule.KeycloakRule; -import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; -import org.keycloak.testsuite.rule.WebResource; -import org.keycloak.testsuite.rule.WebRule; -import org.openqa.selenium.By; -import org.openqa.selenium.WebDriver; - -import javax.ws.rs.core.UriBuilder; -import java.util.LinkedList; -import java.util.List; - -/** - * @author Stian Thorgersen - */ -public class AccountTest { - - @ClassRule - public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); - - ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP); - - UserModel user2 = manager.getSession().users().addUser(appRealm, "test-user-no-access@localhost"); - user2.setEnabled(true); - for (String r : accountApp.getDefaultRoles()) { - user2.deleteRoleMapping(accountApp.getRole(r)); - } - UserCredentialModel creds = new UserCredentialModel(); - creds.setType(CredentialRepresentation.PASSWORD); - creds.setValue("password"); - user2.updateCredential(creds); - } - }); - - private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8081/auth"); - private static final String ACCOUNT_URL = RealmsResource.accountUrl(BASE.clone()).build("test").toString(); - public static String ACCOUNT_REDIRECT = AccountService.loginRedirectUrl(BASE.clone()).build("test").toString(); - - @Rule - public AssertEvents events = new AssertEvents(keycloakRule); - - @Rule - public WebRule webRule = new WebRule(this); - - @WebResource - protected WebDriver driver; - - @WebResource - protected OAuthClient oauth; - - @WebResource - protected AppPage appPage; - - @WebResource - protected LoginPage loginPage; - - @WebResource - protected RegisterPage registerPage; - - @WebResource - protected AccountPasswordPage changePasswordPage; - - @WebResource - protected AccountUpdateProfilePage profilePage; - - @WebResource - protected AccountTotpPage totpPage; - - @WebResource - protected AccountLogPage logPage; - - @WebResource - protected AccountSessionsPage sessionsPage; - - @WebResource - protected ErrorPage errorPage; - - private TimeBasedOTP totp = new TimeBasedOTP(); - private String userId; - - @Before - public void before() { - oauth.state("mystate"); // keycloak enforces that a state param has been sent by client - userId = keycloakRule.getUser("test", "test-user@localhost").getId(); - } - - @After - public void after() { - keycloakRule.update(new KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { - UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); - - UserCredentialModel cred = new UserCredentialModel(); - cred.setType(CredentialRepresentation.PASSWORD); - cred.setValue("password"); - - user.updateCredential(cred); - } - }); - } - - @Test - @Ignore - public void runit() throws Exception { - Thread.sleep(10000000); - } - - @Test - public void returnToAppFromQueryParam() { - driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); - loginPage.login("test-user@localhost", "password"); - Assert.assertTrue(profilePage.isCurrent()); - profilePage.backToApplication(); - - Assert.assertTrue(appPage.isCurrent()); - - driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app&referrer_uri=http://localhost:8081/app?test"); - Assert.assertTrue(profilePage.isCurrent()); - profilePage.backToApplication(); - - Assert.assertTrue(appPage.isCurrent()); - Assert.assertEquals(appPage.baseUrl + "?test", driver.getCurrentUrl()); - - driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); - Assert.assertTrue(profilePage.isCurrent()); - - driver.findElement(By.linkText("Authenticator")).click(); - Assert.assertTrue(totpPage.isCurrent()); - - driver.findElement(By.linkText("Account")).click(); - Assert.assertTrue(profilePage.isCurrent()); - - profilePage.backToApplication(); - - Assert.assertTrue(appPage.isCurrent()); - - events.clear(); - } - - @Test - public void changePassword() { - changePasswordPage.open(); - loginPage.login("test-user@localhost", "password"); - - String sessionId = events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent().getSessionId(); - - changePasswordPage.changePassword("", "new-password", "new-password"); - - Assert.assertEquals("Please specify password.", profilePage.getError()); - - changePasswordPage.changePassword("password", "new-password", "new-password2"); - - Assert.assertEquals("Password confirmation doesn't match", profilePage.getError()); - - changePasswordPage.changePassword("password", "new-password", "new-password"); - - Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); - - events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); - - changePasswordPage.logout(); - - events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AccountPasswordPage.PATH).assertEvent(); - - loginPage.open(); - loginPage.login("test-user@localhost", "password"); - - Assert.assertEquals("Invalid username or password.", loginPage.getError()); - - events.expectLogin().session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent(); - - loginPage.open(); - loginPage.login("test-user@localhost", "new-password"); - - Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - events.expectLogin().assertEvent(); - } - - @Test - public void changePasswordWithPasswordPolicy() { - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setPasswordPolicy(new PasswordPolicy("length")); - } - }); - - try { - changePasswordPage.open(); - loginPage.login("test-user@localhost", "password"); - - - events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); - - changePasswordPage.changePassword("", "new", "new"); - - Assert.assertEquals("Please specify password.", profilePage.getError()); - - changePasswordPage.changePassword("password", "new-password", "new-password"); - - Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); - - events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); - } finally { - keycloakRule.update(new KeycloakRule.KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setPasswordPolicy(new PasswordPolicy(null)); - } - }); - } - } - - @Test - public void changeProfile() { - profilePage.open(); - loginPage.login("test-user@localhost", "password"); - - events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent(); - - Assert.assertEquals("", profilePage.getFirstName()); - Assert.assertEquals("", profilePage.getLastName()); - Assert.assertEquals("test-user@localhost", profilePage.getEmail()); - - // All fields are required, so there should be an error when something is missing. - profilePage.updateProfile("", "New last", "new@email.com"); - - Assert.assertEquals("Please specify first name", profilePage.getError()); - Assert.assertEquals("", profilePage.getFirstName()); - Assert.assertEquals("New last", profilePage.getLastName()); - Assert.assertEquals("new@email.com", profilePage.getEmail()); - - events.assertEmpty(); - - profilePage.updateProfile("New first", "", "new@email.com"); - - Assert.assertEquals("Please specify last name", profilePage.getError()); - Assert.assertEquals("New first", profilePage.getFirstName()); - Assert.assertEquals("", profilePage.getLastName()); - Assert.assertEquals("new@email.com", profilePage.getEmail()); - - events.assertEmpty(); - - profilePage.updateProfile("New first", "New last", ""); - - Assert.assertEquals("Please specify email", profilePage.getError()); - Assert.assertEquals("New first", profilePage.getFirstName()); - Assert.assertEquals("New last", profilePage.getLastName()); - Assert.assertEquals("", profilePage.getEmail()); - - events.assertEmpty(); - - profilePage.clickCancel(); - - Assert.assertEquals("", profilePage.getFirstName()); - Assert.assertEquals("", profilePage.getLastName()); - Assert.assertEquals("test-user@localhost", profilePage.getEmail()); - - events.assertEmpty(); - - profilePage.updateProfile("New first", "New last", "new@email.com"); - - Assert.assertEquals("Your account has been updated", profilePage.getSuccess()); - Assert.assertEquals("New first", profilePage.getFirstName()); - Assert.assertEquals("New last", profilePage.getLastName()); - Assert.assertEquals("new@email.com", profilePage.getEmail()); - - events.expectAccount(EventType.UPDATE_PROFILE).assertEvent(); - events.expectAccount(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); - } - - @Test - public void setupTotp() { - totpPage.open(); - loginPage.login("test-user@localhost", "password"); - - events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=totp").assertEvent(); - - Assert.assertTrue(totpPage.isCurrent()); - - Assert.assertFalse(driver.getPageSource().contains("Remove Google")); - - // Error with false code - totpPage.configure(totp.generate(totpPage.getTotpSecret() + "123")); - - Assert.assertEquals("Invalid authenticator code", profilePage.getError()); - - totpPage.configure(totp.generate(totpPage.getTotpSecret())); - - Assert.assertEquals("Google authenticator configured.", profilePage.getSuccess()); - - events.expectAccount(EventType.UPDATE_TOTP).assertEvent(); - - Assert.assertTrue(driver.getPageSource().contains("pficon-delete")); - - totpPage.removeTotp(); - - events.expectAccount(EventType.REMOVE_TOTP).assertEvent(); - } - - @Test - public void changeProfileNoAccess() throws Exception { - profilePage.open(); - loginPage.login("test-user-no-access@localhost", "password"); - - events.expectLogin().client("account").user(keycloakRule.getUser("test", "test-user-no-access@localhost").getId()) - .detail(Details.USERNAME, "test-user-no-access@localhost") - .detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent(); - - Assert.assertTrue(errorPage.isCurrent()); - Assert.assertEquals("No access", errorPage.getError()); - } - - @Test - public void viewLog() { - keycloakRule.update(new KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setEventsEnabled(true); - } - }); - - try { - List expectedEvents = new LinkedList(); - - loginPage.open(); - loginPage.clickRegister(); - - registerPage.register("view", "log", "view-log@localhost", "view-log", "password", "password"); - - expectedEvents.add(events.poll()); - expectedEvents.add(events.poll()); - - profilePage.open(); - profilePage.updateProfile("view", "log2", "view-log@localhost"); - - expectedEvents.add(events.poll()); - - logPage.open(); - - Assert.assertTrue(logPage.isCurrent()); - - List> actualEvents = logPage.getEvents(); - - Assert.assertEquals(expectedEvents.size(), actualEvents.size()); - - for (Event e : expectedEvents) { - boolean match = false; - for (List a : logPage.getEvents()) { - if (e.getType().toString().replace('_', ' ').toLowerCase().equals(a.get(1)) && - e.getIpAddress().equals(a.get(2)) && - e.getClientId().equals(a.get(3))) { - match = true; - break; - } - } - if (!match) { - Assert.fail("Event not found " + e.getType()); - } - } - } finally { - keycloakRule.update(new KeycloakSetup() { - @Override - public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - appRealm.setEventsEnabled(false); - } - }); - } - } - - @Test - public void sessions() { - loginPage.open(); - loginPage.clickRegister(); - - registerPage.register("view", "sessions", "view-sessions@localhost", "view-sessions", "password", "password"); - - Event registerEvent = events.expectRegister("view-sessions", "view-sessions@localhost").assertEvent(); - String userId = registerEvent.getUserId(); - - events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); - - sessionsPage.open(); - - Assert.assertTrue(sessionsPage.isCurrent()); - - List> sessions = sessionsPage.getSessions(); - Assert.assertEquals(1, sessions.size()); - Assert.assertEquals("127.0.0.1", sessions.get(0).get(0)); - - // Create second session - WebDriver driver2 = WebRule.createWebDriver(); - try { - OAuthClient oauth2 = new OAuthClient(driver2); - oauth2.state("mystate"); - oauth2.doLogin("view-sessions", "password"); - - Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); - - sessionsPage.open(); - sessions = sessionsPage.getSessions(); - Assert.assertEquals(2, sessions.size()); - - sessionsPage.logoutAll(); - - events.expectLogout(registerEvent.getSessionId()); - events.expectLogout(login2Event.getSessionId()); - } finally { - driver2.close(); - } - } - -} +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.testsuite.account; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.events.Details; +import org.keycloak.events.Event; +import org.keycloak.events.EventType; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.TimeBasedOTP; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.RealmsResource; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.pages.AccountLogPage; +import org.keycloak.testsuite.pages.AccountPasswordPage; +import org.keycloak.testsuite.pages.AccountSessionsPage; +import org.keycloak.testsuite.pages.AccountTotpPage; +import org.keycloak.testsuite.pages.AccountUpdateProfilePage; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.AppPage.RequestType; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.RegisterPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; + +import javax.ws.rs.core.UriBuilder; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Stian Thorgersen + */ +public class AccountTest { + + @ClassRule + public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); + + ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP); + + UserModel user2 = manager.getSession().users().addUser(appRealm, "test-user-no-access@localhost"); + user2.setEnabled(true); + for (String r : accountApp.getDefaultRoles()) { + user2.deleteRoleMapping(accountApp.getRole(r)); + } + UserCredentialModel creds = new UserCredentialModel(); + creds.setType(CredentialRepresentation.PASSWORD); + creds.setValue("password"); + user2.updateCredential(creds); + } + }); + + private static final UriBuilder BASE = UriBuilder.fromUri("http://localhost:8081/auth"); + private static final String ACCOUNT_URL = RealmsResource.accountUrl(BASE.clone()).build("test").toString(); + public static String ACCOUNT_REDIRECT = AccountService.loginRedirectUrl(BASE.clone()).build("test").toString(); + + @Rule + public AssertEvents events = new AssertEvents(keycloakRule); + + @Rule + public WebRule webRule = new WebRule(this); + + @WebResource + protected WebDriver driver; + + @WebResource + protected OAuthClient oauth; + + @WebResource + protected AppPage appPage; + + @WebResource + protected LoginPage loginPage; + + @WebResource + protected RegisterPage registerPage; + + @WebResource + protected AccountPasswordPage changePasswordPage; + + @WebResource + protected AccountUpdateProfilePage profilePage; + + @WebResource + protected AccountTotpPage totpPage; + + @WebResource + protected AccountLogPage logPage; + + @WebResource + protected AccountSessionsPage sessionsPage; + + @WebResource + protected ErrorPage errorPage; + + private TimeBasedOTP totp = new TimeBasedOTP(); + private String userId; + + @Before + public void before() { + oauth.state("mystate"); // keycloak enforces that a state param has been sent by client + userId = keycloakRule.getUser("test", "test-user@localhost").getId(); + } + + @After + public void after() { + keycloakRule.update(new KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { + UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); + + UserCredentialModel cred = new UserCredentialModel(); + cred.setType(CredentialRepresentation.PASSWORD); + cred.setValue("password"); + + user.updateCredential(cred); + } + }); + } + +/* + @Test + @Ignore + public void runit() throws Exception { + Thread.sleep(10000000); + } + */ + + @Test + public void returnToAppFromQueryParam() { + driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); + loginPage.login("test-user@localhost", "password"); + Assert.assertTrue(profilePage.isCurrent()); + profilePage.backToApplication(); + + Assert.assertTrue(appPage.isCurrent()); + + driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app&referrer_uri=http://localhost:8081/app?test"); + Assert.assertTrue(profilePage.isCurrent()); + profilePage.backToApplication(); + + Assert.assertTrue(appPage.isCurrent()); + Assert.assertEquals(appPage.baseUrl + "?test", driver.getCurrentUrl()); + + driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app"); + Assert.assertTrue(profilePage.isCurrent()); + + driver.findElement(By.linkText("Authenticator")).click(); + Assert.assertTrue(totpPage.isCurrent()); + + driver.findElement(By.linkText("Account")).click(); + Assert.assertTrue(profilePage.isCurrent()); + + profilePage.backToApplication(); + + Assert.assertTrue(appPage.isCurrent()); + + events.clear(); + } + + @Test + public void changePassword() { + changePasswordPage.open(); + loginPage.login("test-user@localhost", "password"); + + String sessionId = events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent().getSessionId(); + + changePasswordPage.changePassword("", "new-password", "new-password"); + + Assert.assertEquals("Please specify password.", profilePage.getError()); + + changePasswordPage.changePassword("password", "new-password", "new-password2"); + + Assert.assertEquals("Password confirmation doesn't match", profilePage.getError()); + + changePasswordPage.changePassword("password", "new-password", "new-password"); + + Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); + + events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); + + changePasswordPage.logout(); + + events.expectLogout(sessionId).detail(Details.REDIRECT_URI, AccountPasswordPage.PATH).assertEvent(); + + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + Assert.assertEquals("Invalid username or password.", loginPage.getError()); + + events.expectLogin().session((String) null).error("invalid_user_credentials").removeDetail(Details.CODE_ID).assertEvent(); + + loginPage.open(); + loginPage.login("test-user@localhost", "new-password"); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + events.expectLogin().assertEvent(); + } + + @Test + public void changePasswordWithPasswordPolicy() { + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setPasswordPolicy(new PasswordPolicy("length")); + } + }); + + try { + changePasswordPage.open(); + loginPage.login("test-user@localhost", "password"); + + + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=password").assertEvent(); + + changePasswordPage.changePassword("", "new", "new"); + + Assert.assertEquals("Please specify password.", profilePage.getError()); + + changePasswordPage.changePassword("password", "new-password", "new-password"); + + Assert.assertEquals("Your password has been updated", profilePage.getSuccess()); + + events.expectAccount(EventType.UPDATE_PASSWORD).assertEvent(); + } finally { + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setPasswordPolicy(new PasswordPolicy(null)); + } + }); + } + } + + @Test + public void changeProfile() { + profilePage.open(); + loginPage.login("test-user@localhost", "password"); + + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent(); + + Assert.assertEquals("", profilePage.getFirstName()); + Assert.assertEquals("", profilePage.getLastName()); + Assert.assertEquals("test-user@localhost", profilePage.getEmail()); + + // All fields are required, so there should be an error when something is missing. + profilePage.updateProfile("", "New last", "new@email.com"); + + Assert.assertEquals("Please specify first name", profilePage.getError()); + Assert.assertEquals("", profilePage.getFirstName()); + Assert.assertEquals("New last", profilePage.getLastName()); + Assert.assertEquals("new@email.com", profilePage.getEmail()); + + events.assertEmpty(); + + profilePage.updateProfile("New first", "", "new@email.com"); + + Assert.assertEquals("Please specify last name", profilePage.getError()); + Assert.assertEquals("New first", profilePage.getFirstName()); + Assert.assertEquals("", profilePage.getLastName()); + Assert.assertEquals("new@email.com", profilePage.getEmail()); + + events.assertEmpty(); + + profilePage.updateProfile("New first", "New last", ""); + + Assert.assertEquals("Please specify email", profilePage.getError()); + Assert.assertEquals("New first", profilePage.getFirstName()); + Assert.assertEquals("New last", profilePage.getLastName()); + Assert.assertEquals("", profilePage.getEmail()); + + events.assertEmpty(); + + profilePage.clickCancel(); + + Assert.assertEquals("", profilePage.getFirstName()); + Assert.assertEquals("", profilePage.getLastName()); + Assert.assertEquals("test-user@localhost", profilePage.getEmail()); + + events.assertEmpty(); + + profilePage.updateProfile("New first", "New last", "new@email.com"); + + Assert.assertEquals("Your account has been updated", profilePage.getSuccess()); + Assert.assertEquals("New first", profilePage.getFirstName()); + Assert.assertEquals("New last", profilePage.getLastName()); + Assert.assertEquals("new@email.com", profilePage.getEmail()); + + events.expectAccount(EventType.UPDATE_PROFILE).assertEvent(); + events.expectAccount(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); + } + + @Test + public void setupTotp() { + totpPage.open(); + loginPage.login("test-user@localhost", "password"); + + events.expectLogin().client("account").detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT + "?path=totp").assertEvent(); + + Assert.assertTrue(totpPage.isCurrent()); + + Assert.assertFalse(driver.getPageSource().contains("Remove Google")); + + // Error with false code + totpPage.configure(totp.generate(totpPage.getTotpSecret() + "123")); + + Assert.assertEquals("Invalid authenticator code", profilePage.getError()); + + totpPage.configure(totp.generate(totpPage.getTotpSecret())); + + Assert.assertEquals("Google authenticator configured.", profilePage.getSuccess()); + + events.expectAccount(EventType.UPDATE_TOTP).assertEvent(); + + Assert.assertTrue(driver.getPageSource().contains("pficon-delete")); + + totpPage.removeTotp(); + + events.expectAccount(EventType.REMOVE_TOTP).assertEvent(); + } + + @Test + public void changeProfileNoAccess() throws Exception { + profilePage.open(); + loginPage.login("test-user-no-access@localhost", "password"); + + events.expectLogin().client("account").user(keycloakRule.getUser("test", "test-user-no-access@localhost").getId()) + .detail(Details.USERNAME, "test-user-no-access@localhost") + .detail(Details.REDIRECT_URI, ACCOUNT_REDIRECT).assertEvent(); + + Assert.assertTrue(errorPage.isCurrent()); + Assert.assertEquals("No access", errorPage.getError()); + } + + @Test + public void viewLog() { + keycloakRule.update(new KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setEventsEnabled(true); + } + }); + + try { + List expectedEvents = new LinkedList(); + + loginPage.open(); + loginPage.clickRegister(); + + registerPage.register("view", "log", "view-log@localhost", "view-log", "password", "password"); + + expectedEvents.add(events.poll()); + expectedEvents.add(events.poll()); + + profilePage.open(); + profilePage.updateProfile("view", "log2", "view-log@localhost"); + + expectedEvents.add(events.poll()); + + logPage.open(); + + Assert.assertTrue(logPage.isCurrent()); + + List> actualEvents = logPage.getEvents(); + + Assert.assertEquals(expectedEvents.size(), actualEvents.size()); + + for (Event e : expectedEvents) { + boolean match = false; + for (List a : logPage.getEvents()) { + if (e.getType().toString().replace('_', ' ').toLowerCase().equals(a.get(1)) && + e.getIpAddress().equals(a.get(2)) && + e.getClientId().equals(a.get(3))) { + match = true; + break; + } + } + if (!match) { + Assert.fail("Event not found " + e.getType()); + } + } + } finally { + keycloakRule.update(new KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + appRealm.setEventsEnabled(false); + } + }); + } + } + + @Test + public void sessions() { + loginPage.open(); + loginPage.clickRegister(); + + registerPage.register("view", "sessions", "view-sessions@localhost", "view-sessions", "password", "password"); + + Event registerEvent = events.expectRegister("view-sessions", "view-sessions@localhost").assertEvent(); + String userId = registerEvent.getUserId(); + + events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); + + sessionsPage.open(); + + Assert.assertTrue(sessionsPage.isCurrent()); + + List> sessions = sessionsPage.getSessions(); + Assert.assertEquals(1, sessions.size()); + Assert.assertEquals("127.0.0.1", sessions.get(0).get(0)); + + // Create second session + WebDriver driver2 = WebRule.createWebDriver(); + try { + OAuthClient oauth2 = new OAuthClient(driver2); + oauth2.state("mystate"); + oauth2.doLogin("view-sessions", "password"); + + Event login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent(); + + sessionsPage.open(); + sessions = sessionsPage.getSessions(); + Assert.assertEquals(2, sessions.size()); + + sessionsPage.logoutAll(); + + events.expectLogout(registerEvent.getSessionId()); + events.expectLogout(login2Event.getSessionId()); + } finally { + driver2.close(); + } + } + +} diff --git a/testsuite/integration/src/test/resources/testsaml.json b/testsuite/integration/src/test/resources/testsaml.json index 0dfc6e1758..26cd96af31 100755 --- a/testsuite/integration/src/test/resources/testsaml.json +++ b/testsuite/integration/src/test/resources/testsaml.json @@ -75,6 +75,24 @@ "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb7kwJPkGdU34hicplwfp6/WmNcaLh94TSc7Jyr9Undp5pkyLgb0DE7EIE+6kSs4LsqCb8HDkB0nLD5DXbBJFd8n0WGoKstelvtg6FtVJMnwN7k7yZbfkPECWH9zF70VeOo9vbzrApNRnct8ZhH5fbflRB4JMA9L9R+LbURdoSKQIDAQAB", "X509Certificate": "MIIB1DCCAT0CBgFJGVacCDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1lbmMvMB4XDTE0MTAxNjE0MjA0NloXDTI0MTAxNjE0MjIyNlowMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3QtZW5jLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA2+5MCT5BnVN+IYnKZcH6ev1pjXGi4feE0nOycq/VJ3aeaZMi4G9AxOxCBPupErOC7Kgm/Bw5AdJyw+Q12wSRXfJ9FhqCrLXpb7YOhbVSTJ8De5O8mW35DxAlh/cxe9FXjqPb286wKTUZ3LfGYR+X235UQeCTAPS/Ufi21EXaEikCAwEAATANBgkqhkiG9w0BAQsFAAOBgQBMrfGD9QFfx5v7ld/OAto5rjkTe3R1Qei8XRXfcs83vLaqEzjEtTuLGrJEi55kXuJgBpVmQpnwCCkkjSy0JxbqLDdVi9arfWUxEGmOr01ZHycELhDNaQcFqVMPr5kRHIHgktT8hK2IgCvd3Fy9/JCgUgCPxKfhwecyEOKxUc857g==" } + }, + { + "name": "http://localhost:8080/employee-sig/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/employee-sig", + "adminUrl": "http://localhost:8080/employee-sig", + "redirectUris": [ + "http://localhost:8080/employee-sig/*" + ], + "attributes": { + "samlServerSignature": "true", + "samlClientSignature": "true", + "privateKey": "MIICXQIBAAKBgQC+9kVgPFpshjS2aT2g52lqTv2lqb1jgvXZVk7iFF4LAO6SdCXKXRZI4SuzIRkVNpE1a42V1kQRlaozoFklgvX5sje8tkpa9ylq+bxGXM9RRycqRu2B+oWUV7Aqq7Bs0Xud0WeHQYRcEoCjqsFKGy65qkLRDdT70FTJgpSHts+gDwIDAQABAoGANU1efgc6ojIvwn7Lsf8GAKN9z2D6uS0T3I9nw1k2CtI+xWhgKAUltEANx5lEfBRYIdYclidRpqrk8DYgzASrDYTHXzqVBJfAk1VrAGpqyRq+TNMLUHkXiTiSDOQ6WqhX93UGMmAgQm1RsLa6+fy1BO/B2y85+Yf2OUylsKS6avECQQDslRDiNFdtEjdvyOL20tQ7+W+eKVxVxKAyQ3gFjIIDizELZt+Jq1Wz6XV9NhK1JFtlVugeD1tlW/+K16fEmDYXAkEAzqKoN/JeGb20rfQldAUWdQbb0jrQAYlgoSU/9fYH9YVJT8vnkfhPBTwIw9H9euf1//lRP/jHltHd5ch4230YyQJBAN3rOkoltPiABPZbpuLGgwS7BwOCYrWlWmurtBLoaTCvyVKbrgXybNL1pBrOtR+rufvGWLeRyja65Gs1vY6BBQMCQQCTsNq/MjJj/522f7yNUl2cw4w2lOa7Um+IflFbAcDqkZu2ty0Kvgns2d4B6INeZ5ECpjaWnMA7YkFRzZnkd2NRAkB8lEY56ScnNigoZkkjtEUd2ejdhZPYuS9SKfv9zHwN+I+DE2vVFZz8GPq/iLcMx13PkZaYaJNQ4FtQY/hRLSn5", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+9kVgPFpshjS2aT2g52lqTv2lqb1jgvXZVk7iFF4LAO6SdCXKXRZI4SuzIRkVNpE1a42V1kQRlaozoFklgvX5sje8tkpa9ylq+bxGXM9RRycqRu2B+oWUV7Aqq7Bs0Xud0WeHQYRcEoCjqsFKGy65qkLRDdT70FTJgpSHts+gDwIDAQAB", + "X509Certificate": "MIIB0DCCATkCBgFJH5u0EDANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNodHRwOi8vbG9jYWxob3N0OjgwODAvZW1wbG95ZWUtc2lnLzAeFw0xNDEwMTcxOTMzNThaFw0yNDEwMTcxOTM1MzhaMC4xLDAqBgNVBAMTI2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9lbXBsb3llZS1zaWcvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+9kVgPFpshjS2aT2g52lqTv2lqb1jgvXZVk7iFF4LAO6SdCXKXRZI4SuzIRkVNpE1a42V1kQRlaozoFklgvX5sje8tkpa9ylq+bxGXM9RRycqRu2B+oWUV7Aqq7Bs0Xud0WeHQYRcEoCjqsFKGy65qkLRDdT70FTJgpSHts+gDwIDAQABMA0GCSqGSIb3DQEBCwUAA4GBACKyPLGqMX8GsIrCfJU8eVnpaqzTXMglLVo/nTcfAnWe9UAdVe8N3a2PXpDBvuqNA/DEAhVcQgxdlOTWnB6s8/yLTRuH0bZgb3qGdySif+lU+E7zZ/SiDzavAvn+ABqemnzHcHyhYO+hNRGHvUbW5OAii9Vdjhm8BI32YF1NwhKp" + } } ], "roles" : {