Merge pull request #1456 from patriot1burke/master

SAML IDP Initiated Login
This commit is contained in:
Bill Burke 2015-07-15 20:35:01 -04:00
commit ec6e3d80ee
13 changed files with 2011 additions and 1876 deletions

View file

@ -186,4 +186,15 @@
Each realm has a URL where you can view the XML entity descriptor for the IDP. <literal>root/auth/realms/{realm}/protocol/saml/descriptor</literal>
</para>
</section>
<section>
<title>IDP Initiated Login</title>
<para>
IDP Initiated Login is a feature that where you can set up a URL on the Keycloak server that will log you into a specific application/client. To set this up
go to the client page in the admin console of the client you want to set this up for. Specify the <literal>IDP Initiated SSO URL Name</literal>. This is a simple string
with no whitespace in it. After this you can reference your client at the following URL: <literal>root/auth/realms/{realm}/protocol/saml/clients/{url-name}</literal>
</para>
<para>
If your client requires a special relay state, you can also configure this in the admin console.
</para>
</section>
</chapter>

View file

@ -204,6 +204,20 @@
</div>
<kc-tooltip>If configured, this URL will be used for every binding to both the SP's Assertion Consumer and Single Logout Services. This can be individually overiden for each binding and service in the Fine Grain SAML Endpoint Configuration.</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="urlReferenceName">IDP Initiated SSO URL Name</label>
<div class="col-sm-6">
<input ng-model="client.attributes.saml_idp_initiated_sso_url_name" class="form-control" type="text" name="urlReferenceName" id="urlReferenceName" />
</div>
<kc-tooltip>URL fragment name to reference client when you want to do IDP Initiated SSO. Leaving this empty will disable IDP Initiated SSO. The URL you will reference from your browser will be: {server-root}/realms/{realm}/protocol/saml/clients/{client-url-name}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="idpInitiatedRelayState">IDP Initiated SSO Relay State</label>
<div class="col-sm-6">
<input ng-model="client.attributes.saml_idp_initiated_sso_relay_state" class="form-control" type="text" name="idpInitiatedRelayState" id="idpInitiatedRelayState" />
</div>
<kc-tooltip>Relay state you want to send with SAML request when you want to do IDP Initiated SSO.</kc-tooltip>
</div>
<div class="form-group" data-ng-show="!client.bearerOnly && !create && protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="newWebOrigin">Web Origins</label>

View file

@ -16,6 +16,7 @@ bypassKerberos=Your browser is not set up for Kerberos login. Please click cont
kerberosNotSetUp=Kerberos is not set up. You cannot login.
recaptchaFailed=Invalid Recaptcha
recaptchaNotConfigured=Recaptcha is required, but not configured
consentDenied=Consent denied.
registerWithTitle=Registrierung bei {0}
registerWithTitleHtml=Registrierung bei <strong>{0}</strong>

View file

@ -37,6 +37,7 @@ termsTitle=Terms and Conditions
termsTitleHtml=Terms and Conditions
recaptchaFailed=Invalid Recaptcha
recaptchaNotConfigured=Recaptcha is required, but not configured
consentDenied=Consent denied.
noAccount=New user?
username=Username

View file

@ -16,6 +16,7 @@ kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured
recaptchaFailed=Invalid Recaptcha
recaptchaNotConfigured=Recaptcha is required, but not configured
consentDenied=Consent denied.
registerWithTitle=Registrati come {0}
registerWithTitleHtml=Registrati come <strong>{0}</strong>

View file

@ -16,6 +16,7 @@ kerberosNotConfigured=Kerberos Not Configured
kerberosNotConfiguredTitle=Kerberos Not Configured
recaptchaFailed=Invalid Recaptcha
recaptchaNotConfigured=Recaptcha is required, but not configured
consentDenied=Consent denied.
registerWithTitle=Registre-se com {0}
registerWithTitleHtml=Registre-se com <strong>{0}</strong>

View file

View file

@ -40,13 +40,17 @@ import org.w3c.dom.Document;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@ -71,6 +75,7 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_NAME_ID_FORMAT_ATTRIBUTE = "saml_name_id_format";
public static final String LOGIN_PROTOCOL = "saml";
public static final String SAML_BINDING = "saml_binding";
public static final String SAML_IDP_INITIATED_LOGIN = "saml_idp_initiated_login";
public static final String SAML_POST_BINDING = "post";
public static final String SAML_REDIRECT_BINDING = "get";
public static final String SAML_SERVER_SIGNATURE = "saml.server.signature";
@ -89,6 +94,8 @@ public class SamlProtocol implements LoginProtocol {
public static final String SAML_NAME_ID_FORMAT = "SAML_NAME_ID_FORMAT";
public static final String SAML_DEFAULT_NAMEID_FORMAT = JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get();
public static final String SAML_PERSISTENT_NAME_ID_FOR = "saml.persistent.name.id.for";
public static final String SAML_IDP_INITIATED_SSO_RELAY_STATE = "saml_idp_initiated_sso_relay_state";
public static final String SAML_IDP_INITIATED_SSO_URL_NAME = "saml_idp_initiated_sso_url_name";
protected KeycloakSession session;
@ -134,9 +141,19 @@ public class SamlProtocol implements LoginProtocol {
@Override
public Response cancelLogin(ClientSessionModel clientSession) {
Response error = getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
session.sessions().removeClientSession(realm, clientSession);
return error;
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
UriBuilder builder = RealmsResource.protocolUrl(uriInfo).path(SamlService.class, "idpInitiatedSSO");
Map<String, String> params = new HashMap<>();
params.put("realm", realm.getName());
params.put("protocol", LOGIN_PROTOCOL);
params.put("client", clientSession.getClient().getAttribute(SAML_IDP_INITIATED_SSO_URL_NAME));
session.sessions().removeClientSession(realm, clientSession);
URI redirect = builder.buildFromMap(params);
return Response.status(302).location(redirect).build();
} else {
session.sessions().removeClientSession(realm, clientSession);
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
}
protected String getResponseIssuer(RealmModel realm) {
@ -426,7 +443,11 @@ public class SamlProtocol implements LoginProtocol {
@Override
public Response consentDenied(ClientSessionModel clientSession) {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
if ("true".equals(clientSession.getClient().getAttribute(SAML_IDP_INITIATED_LOGIN))) {
return ErrorPage.error(session, Messages.CONSENT_DENIED);
} else {
return getErrorResponse(clientSession, JBossSAMLURIConstants.STATUS_REQUEST_DENIED.get());
}
}
public static String getLogoutServiceUrl(UriInfo uriInfo, ClientModel client, String bindingType) {

View file

@ -3,6 +3,7 @@ package org.keycloak.protocol.saml;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.ClientConnection;
import org.keycloak.VerificationException;
import org.keycloak.authentication.AuthenticationProcessor;
@ -42,6 +43,7 @@ import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
@ -245,8 +247,8 @@ public class SamlService {
} else {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
}
if (redirect == null && client instanceof ClientModel) {
redirect = ((ClientModel) client).getManagementUrl();
if (redirect == null) {
redirect = client.getManagementUrl();
}
}
@ -283,40 +285,8 @@ public class SamlService {
return newBrowserAuthentication(clientSession);
}
private Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
logger.debug("Automatically redirect to identity provider: " + providerId);
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(uriInfo.getBaseUri(), providerId, realm.getName(), accessCode))
.build();
}
protected Response newBrowserAuthentication(ClientSessionModel clientSession) {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isAuthenticateByDefault()) {
return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() );
}
}
AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
try {
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
private String getBindingType(AuthnRequestType requestAbstractType) {
@ -515,6 +485,42 @@ public class SamlService {
}
private Response buildRedirectToIdentityProvider(String providerId, String accessCode) {
logger.debug("Automatically redirect to identity provider: " + providerId);
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(uriInfo.getBaseUri(), providerId, realm.getName(), accessCode))
.build();
}
protected Response newBrowserAuthentication(ClientSessionModel clientSession) {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isAuthenticateByDefault()) {
return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() );
}
}
AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
try {
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
/**
*/
@GET
@ -552,4 +558,65 @@ public class SamlService {
}
@GET
@Path("clients/{client}")
@Produces(MediaType.TEXT_HTML)
public Response idpInitiatedSSO(@PathParam("client") String clientUrlName) {
event.event(EventType.LOGIN);
ClientModel client = null;
for (ClientModel c : realm.getClients()) {
String urlName = c.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_URL_NAME);
if (urlName == null) continue;
if (urlName.equals(clientUrlName)) {
client = c;
break;
}
}
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, Messages.CLIENT_NOT_FOUND);
}
if (client.getManagementUrl() == null
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) == null) {
logger.error("SAML assertion consumer url not set up");
event.error(Errors.INVALID_REDIRECT_URI);
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
String bindingType = SamlProtocol.SAML_POST_BINDING;
if (client.getManagementUrl() == null
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null
&& client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) {
bindingType = SamlProtocol.SAML_REDIRECT_BINDING;
}
String redirect = null;
if (bindingType.equals(SamlProtocol.SAML_REDIRECT_BINDING)) {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
} else {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
}
if (redirect == null) {
redirect = client.getManagementUrl();
}
ClientSessionModel clientSession = session.sessions().createClientSession(realm, client);
clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret());
clientSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
clientSession.setNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
clientSession.setRedirectUri(redirect);
String relayState = client.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_RELAY_STATE);
if (relayState != null && !relayState.trim().equals("")) {
clientSession.setNote(GeneralConstants.RELAY_STATE, relayState);
}
return newBrowserAuthentication(clientSession);
}
}

View file

@ -179,4 +179,6 @@ public class Messages {
public static final String IDENTITY_PROVIDER_LOGIN_FAILURE = "identityProviderLoginFailure";
public static final String FAILED_LOGOUT = "failedLogout";
public static final String CONSENT_DENIED="consentDenied";
}

View file

@ -143,6 +143,10 @@ public class SamlBindingTest {
Assert.assertTrue(driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/demo/protocol/saml"));
}
//@Test
public void ideTesting() throws Exception {
Thread.sleep(100000000);
}
@Test
public void testPostSimpleLoginLogout() {
@ -155,6 +159,17 @@ public class SamlBindingTest {
driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
checkLoggedOut("http://localhost:8081/sales-post/");
}
@Test
public void testPostSimpleLoginLogoutIdpInitiated() {
driver.navigate().to("http://localhost:8081/auth/realms/demo/protocol/saml/clients/sales-post");
loginPage.login("bburke", "password");
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/sales-post/");
System.out.println(driver.getPageSource());
Assert.assertTrue(driver.getPageSource().contains("bburke"));
driver.navigate().to("http://localhost:8081/sales-post?GLO=true");
checkLoggedOut("http://localhost:8081/sales-post/");
}
@Test
public void testPostSignedLoginLogout() {
driver.navigate().to("http://localhost:8081/sales-post-sig/");

View file

@ -47,7 +47,8 @@
"saml_assertion_consumer_url_post": "http://localhost:8081/sales-post/",
"saml_assertion_consumer_url_redirect": "http://localhost:8081/sales-post/",
"saml_single_logout_service_url_post": "http://localhost:8081/sales-post/",
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post/"
"saml_single_logout_service_url_redirect": "http://localhost:8081/sales-post/",
"saml_idp_initiated_sso_url_name": "sales-post"
}
},
{