Merge pull request #1456 from patriot1burke/master
SAML IDP Initiated Login
This commit is contained in:
commit
ec6e3d80ee
13 changed files with 2011 additions and 1876 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
0
model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
Normal file → Executable file
0
model/api/src/main/java/org/keycloak/models/entities/ClientEntity.java
Normal file → Executable 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());
|
||||
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);
|
||||
return error;
|
||||
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,8 +443,12 @@ public class SamlProtocol implements LoginProtocol {
|
|||
|
||||
@Override
|
||||
public Response consentDenied(ClientSessionModel clientSession) {
|
||||
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) {
|
||||
String logoutServiceUrl = null;
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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/");
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue