SAML IDP Initiated login

This commit is contained in:
Bill Burke 2015-07-15 20:08:55 -04:00
parent d74d93a522
commit 57cfbb3770
12 changed files with 1987 additions and 1876 deletions

View file

@ -204,6 +204,13 @@
</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" 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;
@ -55,6 +59,7 @@ import java.util.UUID;
* @version $Revision: 1 $
*/
public class SamlProtocol implements LoginProtocol {
public static final String SAML_IDP_INITIATED_SSO_URL_NAME = "saml_idp_initiated_sso_url_name";
protected static final Logger logger = Logger.getLogger(SamlProtocol.class);
@ -71,6 +76,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";
@ -134,9 +140,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 +442,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,60 @@ 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);
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"
}
},
{