/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.saml;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.assertion.BaseIDAbstractType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.assertion.SubjectType;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
import org.keycloak.dom.saml.v2.protocol.NameIDPolicyType;
import org.keycloak.dom.saml.v2.protocol.RequestAbstractType;
import org.keycloak.dom.saml.v2.protocol.StatusResponseType;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.protocol.saml.profile.ecp.SamlEcpProfileService;
import org.keycloak.saml.SAML2LogoutResponseBuilder;
import org.keycloak.saml.SAMLRequestParser;
import org.keycloak.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil;
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.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
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.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.rotation.HardcodedKeyLocator;
import org.keycloak.rotation.KeyLocator;
import org.keycloak.saml.SPMetadataDescriptor;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* Resource class for the saml connect token service
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SamlService extends AuthorizationEndpointBase {
protected static final Logger logger = Logger.getLogger(SamlService.class);
public SamlService(RealmModel realm, EventBuilder event) {
super(realm, event);
}
public abstract class BindingProtocol {
// this is to support back button on browser
// if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
// and we want to turn it off.
protected boolean redirectToAuthentication;
protected Response basicChecks(String samlRequest, String samlResponse) {
if (!checkSsl()) {
event.event(EventType.LOGIN);
event.error(Errors.SSL_REQUIRED);
return ErrorPage.error(session, Messages.HTTPS_REQUIRED);
}
if (!realm.isEnabled()) {
event.event(EventType.LOGIN_ERROR);
event.error(Errors.REALM_DISABLED);
return ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
}
if (samlRequest == null && samlResponse == null) {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
return null;
}
protected Response handleSamlResponse(String samlResponse, String relayState) {
event.event(EventType.LOGOUT);
SAMLDocumentHolder holder = extractResponseDocument(samlResponse);
StatusResponseType statusResponse = (StatusResponseType) holder.getSamlObject();
// validate destination
if (statusResponse.getDestination() != null && !uriInfo.getAbsolutePath().toString().equals(statusResponse.getDestination())) {
event.detail(Details.REASON, "invalid_destination");
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
if (authResult == null) {
logger.warn("Unknown saml response.");
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
// assume this is a logout response
UserSessionModel userSession = authResult.getSession();
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
logger.warn("Unknown saml response.");
logger.warn("UserSession is not tagged as logging out.");
event.event(EventType.LOGOUT);
event.error(Errors.INVALID_SAML_LOGOUT_RESPONSE);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
logger.debug("logout response");
Response response = authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
event.success();
return response;
}
protected Response handleSamlRequest(String samlRequest, String relayState) {
SAMLDocumentHolder documentHolder = extractRequestDocument(samlRequest);
if (documentHolder == null) {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
SAML2Object samlObject = documentHolder.getSamlObject();
RequestAbstractType requestAbstractType = (RequestAbstractType) samlObject;
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.getClientByClientId(issuer);
if (client == null) {
event.event(EventType.LOGIN);
event.client(issuer);
event.error(Errors.CLIENT_NOT_FOUND);
return ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER);
}
if (!client.isEnabled()) {
event.event(EventType.LOGIN);
event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED);
}
if (client.isBearerOnly()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.BEARER_ONLY);
}
if (!client.isStandardFlowEnabled()) {
event.event(EventType.LOGIN);
event.error(Errors.NOT_ALLOWED);
return ErrorPage.error(session, Messages.STANDARD_FLOW_DISABLED);
}
session.getContext().setClient(client);
try {
verifySignature(documentHolder, client);
} catch (VerificationException e) {
SamlService.logger.error("request validation failed", e);
event.event(EventType.LOGIN);
event.error(Errors.INVALID_SIGNATURE);
return ErrorPage.error(session, Messages.INVALID_REQUESTER);
}
logger.debug("verified request");
if (samlObject instanceof AuthnRequestType) {
logger.debug("** login request");
event.event(EventType.LOGIN);
// Get the SAML Request Message
AuthnRequestType authn = (AuthnRequestType) samlObject;
return loginRequest(relayState, authn, client);
} else if (samlObject instanceof LogoutRequestType) {
logger.debug("** logout request");
event.event(EventType.LOGOUT);
LogoutRequestType logout = (LogoutRequestType) samlObject;
return logoutRequest(logout, client, relayState);
} else {
event.event(EventType.LOGIN);
event.error(Errors.INVALID_TOKEN);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
}
protected abstract void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException;
protected abstract SAMLDocumentHolder extractRequestDocument(String samlRequest);
protected abstract SAMLDocumentHolder extractResponseDocument(String response);
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
SamlClient samlClient = new SamlClient(client);
// validate destination
if (requestAbstractType.getDestination() != null && !uriInfo.getAbsolutePath().equals(requestAbstractType.getDestination())) {
event.detail(Details.REASON, "invalid_destination");
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
String bindingType = getBindingType(requestAbstractType);
if (samlClient.forcePostBinding())
bindingType = SamlProtocol.SAML_POST_BINDING;
String redirect;
URI redirectUri = requestAbstractType.getAssertionConsumerServiceURL();
if (redirectUri != null && ! "null".equals(redirectUri.toString())) { // "null" is for testing purposes
redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri.toString(), realm, client);
} else {
if (bindingType.equals(SamlProtocol.SAML_POST_BINDING)) {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE);
} else {
redirect = client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE);
}
if (redirect == null) {
redirect = client.getManagementUrl();
}
}
if (redirect == null) {
event.error(Errors.INVALID_REDIRECT_URI);
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, relayState);
if (checks.response != null) {
return checks.response;
}
AuthenticationSessionModel authSession = checks.authSession;
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
authSession.setRedirectUri(redirect);
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
authSession.setClientNote(SamlProtocol.SAML_BINDING, bindingType);
authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
authSession.setClientNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
// Handle NameIDPolicy from SP
NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy();
final URI nameIdFormatUri = nameIdPolicy == null ? null : nameIdPolicy.getFormat();
if (nameIdFormatUri != null && ! samlClient.forceNameIDFormat()) {
String nameIdFormat = nameIdFormatUri.toString();
// TODO: Handle AllowCreate too, relevant for persistent NameID.
if (isSupportedNameIdFormat(nameIdFormat)) {
authSession.setClientNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
} else {
event.detail(Details.REASON, "unsupported_nameid_format");
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
return ErrorPage.error(session, Messages.UNSUPPORTED_NAME_ID_FORMAT);
}
}
//Reading subject/nameID in the saml request
SubjectType subject = requestAbstractType.getSubject();
if (subject != null) {
SubjectType.STSubType subType = subject.getSubType();
if (subType != null) {
BaseIDAbstractType baseID = subject.getSubType().getBaseID();
if (baseID != null && baseID instanceof NameIDType) {
NameIDType nameID = (NameIDType) baseID;
authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
}
}
}
return newBrowserAuthentication(authSession, requestAbstractType.isIsPassive(), redirectToAuthentication);
}
protected String getBindingType(AuthnRequestType requestAbstractType) {
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
if (requestedProtocolBinding != null) {
if (JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get().equals(requestedProtocolBinding.toString())) {
return SamlProtocol.SAML_POST_BINDING;
} else {
return SamlProtocol.SAML_REDIRECT_BINDING;
}
}
return getBindingType();
}
private boolean isSupportedNameIdFormat(String nameIdFormat) {
if (nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_TRANSIENT.get()) || nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get())
|| nameIdFormat.equals(JBossSAMLURIConstants.NAMEID_FORMAT_UNSPECIFIED.get())) {
return true;
}
return false;
}
protected abstract String getBindingType();
protected Response logoutRequest(LogoutRequestType logoutRequest, ClientModel client, String relayState) {
SamlClient samlClient = new SamlClient(client);
// validate destination
if (logoutRequest.getDestination() != null && !uriInfo.getAbsolutePath().equals(logoutRequest.getDestination())) {
event.detail(Details.REASON, "invalid_destination");
event.error(Errors.INVALID_SAML_LOGOUT_REQUEST);
return ErrorPage.error(session, Messages.INVALID_REQUEST);
}
// authenticate identity cookie, but ignore an access token timeout as we're logging out anyways.
AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, false);
if (authResult != null) {
String logoutBinding = getBindingType();
String postBindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, SamlProtocol.SAML_POST_BINDING);
if (samlClient.forcePostBinding() && postBindingUri != null && ! postBindingUri.trim().isEmpty())
logoutBinding = SamlProtocol.SAML_POST_BINDING;
boolean postBinding = Objects.equals(SamlProtocol.SAML_POST_BINDING, logoutBinding);
String bindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
UserSessionModel userSession = authResult.getSession();
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING_URI, bindingUri);
if (samlClient.requiresRealmSignature()) {
userSession.setNote(SamlProtocol.SAML_LOGOUT_SIGNATURE_ALGORITHM, samlClient.getSignatureAlgorithm().toString());
}
if (relayState != null)
userSession.setNote(SamlProtocol.SAML_LOGOUT_RELAY_STATE, relayState);
userSession.setNote(SamlProtocol.SAML_LOGOUT_REQUEST_ID, logoutRequest.getID());
userSession.setNote(SamlProtocol.SAML_LOGOUT_BINDING, logoutBinding);
userSession.setNote(SamlProtocol.SAML_LOGOUT_ADD_EXTENSIONS_ELEMENT_WITH_KEY_INFO, Boolean.toString((! postBinding) && samlClient.addExtensionsElementWithKeyInfo()));
userSession.setNote(SamlProtocol.SAML_SERVER_SIGNATURE_KEYINFO_KEY_NAME_TRANSFORMER, samlClient.getXmlSigKeyInfoKeyNameTransformer().name());
userSession.setNote(SamlProtocol.SAML_LOGOUT_CANONICALIZATION, samlClient.getCanonicalizationMethod());
userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, SamlProtocol.LOGIN_PROTOCOL);
// remove client from logout requests
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession != null) {
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
}
logger.debug("browser Logout");
return authManager.browserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
} else if (logoutRequest.getSessionIndex() != null) {
for (String sessionIndex : logoutRequest.getSessionIndex()) {
AuthenticatedClientSessionModel clientSession = SamlSessionUtils.getClientSession(session, realm, sessionIndex);
if (clientSession == null)
continue;
UserSessionModel userSession = clientSession.getUserSession();
if (clientSession.getClient().getClientId().equals(client.getClientId())) {
// remove requesting client from logout
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
}
try {
authManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
} catch (Exception e) {
logger.warn("Failure with backchannel logout", e);
}
}
}
// default
String logoutBinding = getBindingType();
String logoutBindingUri = SamlProtocol.getLogoutServiceUrl(uriInfo, client, logoutBinding);
String logoutRelayState = relayState;
SAML2LogoutResponseBuilder builder = new SAML2LogoutResponseBuilder();
builder.logoutRequestID(logoutRequest.getID());
builder.destination(logoutBindingUri);
builder.issuer(RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(logoutRelayState);
boolean postBinding = SamlProtocol.SAML_POST_BINDING.equals(logoutBinding);
if (samlClient.requiresRealmSignature()) {
SignatureAlgorithm algorithm = samlClient.getSignatureAlgorithm();
KeyManager.ActiveRsaKey keys = session.keys().getActiveRsaKey(realm);
binding.signatureAlgorithm(algorithm).signWith(keys.getKid(), keys.getPrivateKey(), keys.getPublicKey(), keys.getCertificate()).signDocument();
if (! postBinding && samlClient.addExtensionsElementWithKeyInfo()) { // Only include extension if REDIRECT binding and signing whole SAML protocol message
builder.addExtension(new KeycloakKeySamlExtensionGenerator(keys.getKid()));
}
}
try {
if (postBinding) {
return binding.postBinding(builder.buildDocument()).response(logoutBindingUri);
} else {
return binding.redirectBinding(builder.buildDocument()).response(logoutBindingUri);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private boolean checkSsl() {
if (uriInfo.getBaseUri().getScheme().equals("https")) {
return true;
} else {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse);
if (response != null)
return response;
if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
}
}
protected class PostBindingProtocol extends BindingProtocol {
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
}
@Override
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestPostBinding(samlRequest);
}
@Override
protected SAMLDocumentHolder extractResponseDocument(String response) {
return SAMLRequestParser.parseResponsePostBinding(response);
}
@Override
protected String getBindingType() {
return SamlProtocol.SAML_POST_BINDING;
}
}
protected class RedirectBindingProtocol extends BindingProtocol {
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
SamlClient samlClient = new SamlClient(client);
if (!samlClient.requiresClientSignature()) {
return;
}
PublicKey publicKey = SamlProtocolUtils.getSignatureValidationKey(client);
KeyLocator clientKeyLocator = new HardcodedKeyLocator(publicKey);
SamlProtocolUtils.verifyRedirectSignature(documentHolder, clientKeyLocator, uriInfo, GeneralConstants.SAML_REQUEST_KEY);
}
@Override
protected SAMLDocumentHolder extractRequestDocument(String samlRequest) {
return SAMLRequestParser.parseRequestRedirectBinding(samlRequest);
}
@Override
protected SAMLDocumentHolder extractResponseDocument(String response) {
return SAMLRequestParser.parseResponseRedirectBinding(response);
}
@Override
protected String getBindingType() {
return SamlProtocol.SAML_REDIRECT_BINDING;
}
}
protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication) {
SamlProtocol samlProtocol = new SamlProtocol().setEventBuilder(event).setHttpHeaders(headers).setRealm(realm).setSession(session).setUriInfo(uriInfo);
return newBrowserAuthentication(authSession, isPassive, redirectToAuthentication, samlProtocol);
}
protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication, SamlProtocol samlProtocol) {
return handleBrowserAuthenticationRequest(authSession, samlProtocol, isPassive, redirectToAuthentication);
}
/**
*/
@GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
logger.debug("SAML GET");
CacheControlUtil.noBackButtonCacheControlHeader();
return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState);
}
/**
*/
@POST
@NoCache
@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) {
logger.debug("SAML POST");
PostBindingProtocol postBindingProtocol = new PostBindingProtocol();
// this is to support back button on browser
// if true, we redirect to authenticate URL otherwise back button behavior has bad side effects
// and we want to turn it off.
postBindingProtocol.redirectToAuthentication = true;
return postBindingProtocol.execute(samlRequest, samlResponse, relayState);
}
@GET
@Path("descriptor")
@Produces(MediaType.APPLICATION_XML)
@NoCache
public String getDescriptor() throws Exception {
return getIDPMetadataDescriptor(uriInfo, session, realm);
}
public static String getIDPMetadataDescriptor(UriInfo uriInfo, KeycloakSession session, RealmModel realm) throws IOException {
InputStream is = SamlService.class.getResourceAsStream("/idp-metadata-template.xml");
String template = StreamUtil.readString(is);
Properties props = new Properties();
props.put("idp.entityID", RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString());
props.put("idp.sso.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
props.put("idp.sso.HTTP-Redirect", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
props.put("idp.sls.HTTP-POST", RealmsResource.protocolUrl(uriInfo).build(realm.getName(), SamlProtocol.LOGIN_PROTOCOL).toString());
StringBuilder keysString = new StringBuilder();
Set<RsaKeyMetadata> keys = new TreeSet<>((o1, o2) -> o1.getStatus() == o2.getStatus() // Status can be only PASSIVE OR ACTIVE, push PASSIVE to end of list
? (int) (o2.getProviderPriority() - o1.getProviderPriority())
: (o1.getStatus() == KeyMetadata.Status.PASSIVE ? 1 : -1));
keys.addAll(session.keys().getRsaKeys(realm, false));
for (RsaKeyMetadata key : keys) {
addKeyInfo(keysString, key, KeyTypes.SIGNING.value());
}
props.put("idp.signing.certificates", keysString.toString());
return StringPropertyReplacer.replaceProperties(template, props);
}
private static void addKeyInfo(StringBuilder target, RsaKeyMetadata key, String purpose) {
if (key == null) {
return;
}
target.append(SPMetadataDescriptor.xmlKeyInfo(" ",
key.getKid(), PemUtils.encodeCertificate(key.getCertificate()), purpose, false));
}
@GET
@Path("clients/{client}")
@Produces(MediaType.TEXT_HTML)
public Response idpInitiatedSSO(@PathParam("client") String clientUrlName, @QueryParam("RelayState") String relayState) {
event.event(EventType.LOGIN);
CacheControlUtil.noBackButtonCacheControlHeader();
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.isEnabled()) {
event.error(Errors.CLIENT_DISABLED);
return ErrorPage.error(session, Messages.CLIENT_DISABLED);
}
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);
}
AuthenticationSessionModel authSession = getOrCreateLoginSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
return newBrowserAuthentication(authSession, false, false);
}
/**
* Creates a client session object for SAML IdP-initiated SSO session.
* The session takes the parameters from from client definition,
* namely binding type and redirect URL.
*
* @param session KC session
* @param realm Realm to create client session in
* @param client Client to create client session for
* @param relayState Optional relay state - free field as per SAML specification
* @return
*/
public AuthenticationSessionModel getOrCreateLoginSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
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;
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();
}
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, null);
if (checks.response != null) {
throw new IllegalStateException("Not expected to detect re-sent request for IDP initiated SSO");
}
AuthenticationSessionModel authSession = checks.authSession;
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
authSession.setClientNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
authSession.setClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
authSession.setRedirectUri(redirect);
if (relayState == null) {
relayState = client.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_RELAY_STATE);
}
if (relayState != null && !relayState.trim().equals("")) {
authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
}
return authSession;
}
@Override
protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestRelayState) {
// No support of browser "refresh" or "back" buttons for SAML IDP initiated SSO. So always treat as new request
String idpInitiated = authSession.getClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN);
if (Boolean.parseBoolean(idpInitiated)) {
return true;
}
if (requestRelayState == null) {
return true;
}
// Check if it's different client
if (!clientFromRequest.equals(authSession.getClient())) {
return true;
}
return !requestRelayState.equals(authSession.getClientNote(GeneralConstants.RELAY_STATE));
}
@POST
@NoCache
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
public Response soapBinding(InputStream inputStream) {
SamlEcpProfileService bindingService = new SamlEcpProfileService(realm, event);
ResteasyProviderFactory.getInstance().injectProperties(bindingService);
return bindingService.authenticate(inputStream);
}
}