keycloak-aplcache
Changes
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SALM2LoginResponseBuilder.java 42(+17 -25)
saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SAML2ErrorResponseBuilder.java 20(+10 -10)
Details
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 e7d7e1a..e3933c0 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 917b772..049d001 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/SalmProtocol.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SalmProtocol.java
index 8a1222f..d5c5b4b 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 342f089..e4c586a 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 708010e..cb27bf9 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;
}
- /**
- */
- @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");
+ 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;
}
- if (samlRequest == null && samlResponse == 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");
-
}
- if (samlRequest != null) return handleSamlRequest(samlRequest, relayState);
- else return handleSamlResponse(samlResponse, relayState);
- }
+ 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");
+ }
- 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");
- }
+ SAML2Object samlObject = documentHolder.getSamlObject();
+ RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject;
+ String issuer = requestAbstractType.getIssuer().getValue();
+ ClientModel client = realm.findClient(issuer);
- 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");
+ 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");
+ }
}
- SAML2Object samlObject = documentHolder.getSamlObject();
+ 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);
- RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject;
- String issuer = requestAbstractType.getIssuer().getValue();
- ClientModel client = realm.findClient(issuer);
+ 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 (client == null) {
- event.error(Errors.CLIENT_NOT_FOUND);
- return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester.");
+ if (rememberMeUsername != null) {
+ MultivaluedMap<String, String> formData = new MultivaluedMapImpl<String, String>();
+ formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
+ formData.add("rememberMe", "on");
+
+ forms.setFormData(formData);
+ }
+
+ return forms.createLogin();
}
- if (!client.isEnabled()) {
- event.error(Errors.CLIENT_DISABLED);
- return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled.");
+ 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();
+ }
+
}
- 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");
+
+ private void logout(UserSessionModel userSession) {
+ authManager.logout(session, realm, userSession, uriInfo, clientConnection);
+ event.user(userSession.getUser()).session(userSession).success();
}
- 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");
+
+ private boolean checkSsl() {
+ if (uriInfo.getBaseUri().getScheme().equals("https")) {
+ return true;
+ } else {
+ return !realm.getSslRequired().isRequired(clientConnection);
+ }
}
+ }
+
- 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.");
+ protected class PostBindingProtocol extends BindingProtocol {
+
+
+ @Override
+ protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
+ SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
}
- 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");
+
+ @Override
+ protected SAMLDocumentHolder extractDocument(String samlRequest) {
+ return SAMLRequestParser.parsePostBinding(samlRequest);
}
- }
- protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
+ @Override
+ protected String getBindingType() {
+ return SalmProtocol.SAML_POST_BINDING;
+ }
- 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.");
+ 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);
}
+ }
- 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());
+ protected class RedirectBindingProtocol extends BindingProtocol {
- Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
- if (response != null) return response;
+ @Override
+ protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
+ if (!"true".equals(client.getAttribute("samlClientSignature"))) {
+ return;
+ }
+ MultivaluedMap<String, String> 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);
- LoginFormsProvider forms = Flows.forms(session, realm, clientSession.getClient(), uriInfo)
- .setClientSessionCode(new ClientSessionCode(realm, clientSession).getCode());
+ 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");
- String rememberMeUsername = AuthenticationManager.getRememberMeUsername(realm, headers);
+ SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
- if (rememberMeUsername != null) {
- MultivaluedMap<String, String> formData = new MultivaluedMapImpl<String, String>();
- formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
- formData.add("rememberMe", "on");
+ PublicKey publicKey = SamlProtocolUtils.getPublicKey(client);
- forms.setFormData(formData);
- }
- return forms.createLogin();
- }
+ 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);
+ }
+
- 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;
+ @Override
+ protected SAMLDocumentHolder extractDocument(String samlRequest) {
+ return SAMLRequestParser.parseRedirectBinding(samlRequest);
+ }
- if (client instanceof ApplicationModel) {
- redirectUri = ((ApplicationModel)client).getBaseUrl();
+ @Override
+ protected String getBindingType() {
+ return SalmProtocol.SAML_GET_BINDING;
}
- 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();
+
+ 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);
}
}
- private void logout(UserSessionModel userSession) {
- authManager.logout(session, realm, userSession, uriInfo, clientConnection);
- event.user(userSession.getUser()).session(userSession).success();
+
+ /**
+ */
+ @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);
}
- private boolean checkSsl() {
- if (uriInfo.getBaseUri().getScheme().equals("https")) {
- return true;
- } else {
- return !realm.getSslRequired().isRequired(clientConnection);
- }
+
+ /**
+ */
+ @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 PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
}
+
}
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 0000000..177ae9c
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 620f5e2..cffdda8 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
- */
-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<Event> expectedEvents = new LinkedList<Event>();
-
- 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<List<String>> actualEvents = logPage.getEvents();
-
- Assert.assertEquals(expectedEvents.size(), actualEvents.size());
-
- for (Event e : expectedEvents) {
- boolean match = false;
- for (List<String> 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<List<String>> 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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+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<Event> expectedEvents = new LinkedList<Event>();
+
+ 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<List<String>> actualEvents = logPage.getEvents();
+
+ Assert.assertEquals(expectedEvents.size(), actualEvents.size());
+
+ for (Event e : expectedEvents) {
+ boolean match = false;
+ for (List<String> 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<List<String>> 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 0dfc6e1..26cd96a 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" : {