Merge pull request #775 from patriot1burke/master

saml redirect binding
This commit is contained in:
Bill Burke 2014-10-17 16:54:56 -04:00
commit 8b05e2ebf8
12 changed files with 976 additions and 726 deletions

View file

@ -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";

View file

@ -42,7 +42,7 @@ public enum EventType {
SEND_RESET_PASSWORD_ERROR,
SOCIAL_LOGIN,
SOCIAL_LOGIN_ERROR,
INVALID_SIGNATURE_ERROR,
REGISTER_NODE,
UNREGISTER_NODE
}

View file

@ -2,14 +2,12 @@ package org.keycloak.protocol.saml;
import org.picketlink.common.PicketLinkLogger;
import org.picketlink.common.PicketLinkLoggerFactory;
import org.picketlink.common.constants.GeneralConstants;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.common.util.DocumentUtil;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
@ -19,14 +17,8 @@ import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@ -42,7 +34,7 @@ import static org.picketlink.common.util.StringUtil.isNotNull;
* @author Anil.Saldhana@redhat.com
* @author bburke@redhat.com
*/
public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilder<SALM2PostBindingLoginResponseBuilder> {
public class SALM2LoginResponseBuilder extends SAML2BindingBuilder<SALM2LoginResponseBuilder> {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
protected List<String> roles = new LinkedList<String>();
@ -55,65 +47,66 @@ public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilde
protected Map<String, Object> attributes = new HashMap<String, Object>();
public SALM2PostBindingLoginResponseBuilder attributes(Map<String, Object> attributes) {
public SALM2LoginResponseBuilder attributes(Map<String, Object> attributes) {
this.attributes = attributes;
return this;
}
public SALM2PostBindingLoginResponseBuilder attribute(String name, Object value) {
public SALM2LoginResponseBuilder attribute(String name, Object value) {
this.attributes.put(name, value);
return this;
}
public SALM2PostBindingLoginResponseBuilder requestID(String requestID) {
public SALM2LoginResponseBuilder requestID(String requestID) {
this.requestID =requestID;
return this;
}
public SALM2PostBindingLoginResponseBuilder requestIssuer(String requestIssuer) {
public SALM2LoginResponseBuilder requestIssuer(String requestIssuer) {
this.requestIssuer =requestIssuer;
return this;
}
public SALM2PostBindingLoginResponseBuilder roles(List<String> roles) {
public SALM2LoginResponseBuilder roles(List<String> roles) {
this.roles = roles;
return this;
}
public SALM2PostBindingLoginResponseBuilder roles(String... roles) {
public SALM2LoginResponseBuilder roles(String... roles) {
for (String role : roles) {
this.roles.add(role);
}
return this;
}
public SALM2PostBindingLoginResponseBuilder authMethod(String authMethod) {
public SALM2LoginResponseBuilder authMethod(String authMethod) {
this.authMethod = authMethod;
return this;
}
public SALM2PostBindingLoginResponseBuilder userPrincipal(String userPrincipal) {
public SALM2LoginResponseBuilder userPrincipal(String userPrincipal) {
this.userPrincipal = userPrincipal;
return this;
}
public SALM2PostBindingLoginResponseBuilder multiValuedRoles(boolean multiValuedRoles) {
public SALM2LoginResponseBuilder multiValuedRoles(boolean multiValuedRoles) {
this.multiValuedRoles = multiValuedRoles;
return this;
}
public SALM2PostBindingLoginResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) {
public SALM2LoginResponseBuilder disableAuthnStatement(boolean disableAuthnStatement) {
this.disableAuthnStatement = disableAuthnStatement;
return this;
}
public Response buildLoginResponse() throws ConfigurationException, ProcessingException, IOException {
Document responseDoc = getResponse();
return buildResponse(responseDoc);
public BindingBuilder binding() throws ConfigurationException, ProcessingException {
Document samlResponseDocument = buildDocument();
return new BindingBuilder(samlResponseDocument);
}
public Document getResponse() throws ConfigurationException, ProcessingException {
public Document buildDocument() throws ConfigurationException, ProcessingException {
Document samlResponseDocument = null;
ResponseType responseType = null;
@ -175,7 +168,6 @@ public class SALM2PostBindingLoginResponseBuilder extends SAML2PostBindingBuilde
}
encryptAndSign(samlResponseDocument);
return samlResponseDocument;
}

View file

@ -9,6 +9,7 @@ import org.picketlink.common.util.DocumentUtil;
import org.picketlink.identity.federation.core.util.XMLEncryptionUtil;
import org.picketlink.identity.federation.core.wstrust.WSTrustUtil;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.picketlink.identity.federation.web.util.RedirectBindingUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
@ -17,11 +18,14 @@ import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.xml.namespace.QName;
import java.io.IOException;
import java.net.URI;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import static org.picketlink.common.util.StringUtil.isNotNull;
@ -30,12 +34,11 @@ import static org.picketlink.common.util.StringUtil.isNotNull;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
public class SAML2BindingBuilder<T extends SAML2BindingBuilder> {
protected KeyPair signingKeyPair;
protected X509Certificate signingCertificate;
protected boolean signed;
protected String signatureDigestMethod;
protected String signatureMethod;
protected SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RSA_SHA1;
protected String relayState;
protected String destination;
protected String responseIssuer;
@ -70,6 +73,11 @@ public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
return (T)this;
}
public T signatureAlgorithm(SignatureAlgorithm alg) {
this.signatureAlgorithm = alg;
return (T)this;
}
public T encrypt(PublicKey publicKey) {
encrypt = true;
encryptionPublicKey = publicKey;
@ -86,16 +94,6 @@ public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
return (T)this;
}
public T signatureDigestMethod(String method) {
this.signatureDigestMethod = method;
return (T)this;
}
public T signatureMethod(String method) {
this.signatureMethod = method;
return (T)this;
}
public T destination(String destination) {
this.destination = destination;
return (T)this;
@ -111,6 +109,37 @@ public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
return (T)this;
}
public class BindingBuilder {
protected Document document;
public BindingBuilder(Document document) {
this.document = document;
}
public Document getDocument() {
return document;
}
public Response postResponse() throws ConfigurationException, ProcessingException, IOException {
return buildResponse(document);
}
public URI redirectResponseUri() throws ConfigurationException, ProcessingException, IOException {
return generateRedirectUri("SAMLResponse", document);
}
public Response redirectResponse() throws ProcessingException, ConfigurationException, IOException {
URI uri = redirectResponseUri();
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
return Response.status(302).location(uri)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
}
}
private String getSAMLNSPrefix(Document samlResponseDocument) {
Node assertionElement = samlResponseDocument.getDocumentElement()
.getElementsByTagNameNS(JBossSAMLURIConstants.ASSERTION_NSURI.get(), JBossSAMLConstants.ASSERTION.get()).item(0);
@ -155,15 +184,25 @@ public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
}
protected void signDocument(Document samlDocument) throws ProcessingException {
SamlProtocolUtils.signDocument(samlDocument, signingKeyPair, signatureMethod, signatureDigestMethod, signingCertificate);
SamlProtocolUtils.signDocument(samlDocument, signingKeyPair, signatureAlgorithm.getXmlSignatureMethod(), signatureAlgorithm.getXmlSignatureDigestMethod(), signingCertificate);
}
protected Response buildResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
String str = buildHtmlPostResponse(responseDoc);
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
}
protected String buildHtmlPostResponse(Document responseDoc) throws ProcessingException, ConfigurationException, IOException {
byte[] responseBytes = DocumentUtil.getDocumentAsString(responseDoc).getBytes("UTF-8");
String samlResponse = PostBindingUtil.base64Encode(new String(responseBytes));
if (destination == null) {
throw SALM2PostBindingLoginResponseBuilder.logger.nullValueError("Destination is null");
throw SALM2LoginResponseBuilder.logger.nullValueError("Destination is null");
}
StringBuilder builder = new StringBuilder();
@ -190,13 +229,42 @@ public class SAML2PostBindingBuilder<T extends SAML2PostBindingBuilder> {
builder.append("</FORM></BODY></HTML>");
String str = builder.toString();
CacheControl cacheControl = new CacheControl();
cacheControl.setNoCache(true);
return Response.ok(str, MediaType.TEXT_HTML_TYPE)
.header("Pragma", "no-cache")
.header("Cache-Control", "no-cache, no-store").build();
return builder.toString();
}
protected String base64Encoded(Document document) throws ConfigurationException, ProcessingException, IOException {
byte[] responseBytes = org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil.getDocumentAsString(document).getBytes("UTF-8");
return RedirectBindingUtil.deflateBase64URLEncode(responseBytes);
}
protected URI generateRedirectUri(String samlParameterName, Document document) throws ConfigurationException, ProcessingException, IOException {
UriBuilder builder = UriBuilder.fromUri(destination)
.replaceQuery(null)
.queryParam(samlParameterName, base64Encoded(document));
if (relayState != null) {
builder.queryParam("RelayState", relayState);
}
if (signed) {
builder.queryParam(GeneralConstants.SAML_SIG_ALG_REQUEST_KEY, signatureAlgorithm.getJavaSignatureAlgorithm());
URI uri = builder.build();
String rawQuery = uri.getRawQuery();
Signature signature = signatureAlgorithm.createSignature();
byte[] sig = new byte[0];
try {
signature.initSign(signingKeyPair.getPrivate());
signature.update(rawQuery.getBytes("UTF-8"));
sig = signature.sign();
} catch (Exception e) {
throw new ProcessingException(e);
}
String encodedSig = RedirectBindingUtil.base64URLEncode(sig);
builder.queryParam(GeneralConstants.SAML_SIGNATURE_REQUEST_KEY, encodedSig);
}
return builder.build();
}
}

View file

@ -12,17 +12,13 @@ import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.io.StringWriter;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2PostBindingErrorResponseBuilder extends SAML2PostBindingBuilder<SAML2PostBindingErrorResponseBuilder> {
public class SAML2ErrorResponseBuilder extends SAML2BindingBuilder<SAML2ErrorResponseBuilder> {
public Document getErrorResponse(String status) throws ProcessingException {
public Document buildDocument(String status) throws ProcessingException {
Document samlResponse = null;
ResponseType responseType = null;
@ -49,7 +45,11 @@ public class SAML2PostBindingErrorResponseBuilder extends SAML2PostBindingBuilde
return samlResponse;
}
public Response buildErrorResponse(String status) throws ConfigurationException, ProcessingException, IOException {
Document doc = getErrorResponse(status);
return buildResponse(doc);
}}
public BindingBuilder binding(String status) throws ConfigurationException, ProcessingException {
Document samlResponseDocument = buildDocument(status);
return new BindingBuilder(samlResponseDocument);
}
}

View file

@ -2,37 +2,25 @@ package org.keycloak.protocol.saml;
import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ParsingException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request;
import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response;
import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator;
import org.picketlink.identity.federation.core.saml.v2.factories.JBossSAMLAuthnResponseFactory;
import org.picketlink.identity.federation.core.saml.v2.holders.IDPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.IssuerInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.holders.SPInfoHolder;
import org.picketlink.identity.federation.core.saml.v2.util.DocumentUtil;
import org.picketlink.identity.federation.core.saml.v2.util.XMLTimeUtil;
import org.picketlink.identity.federation.core.sts.PicketLinkCoreSTS;
import org.picketlink.identity.federation.saml.v2.assertion.NameIDType;
import org.picketlink.identity.federation.saml.v2.protocol.LogoutRequestType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.picketlink.identity.federation.web.util.PostBindingUtil;
import org.w3c.dom.Document;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.net.URI;
import java.security.KeyPair;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAML2PostBindingLogoutResponseBuilder extends SAML2PostBindingBuilder<SAML2PostBindingLogoutResponseBuilder> {
public class SAML2LogoutRequestBuilder extends SAML2BindingBuilder<SAML2LogoutRequestBuilder> {
protected String userPrincipal;
public SAML2PostBindingLogoutResponseBuilder userPrincipal(String userPrincipal) {
public SAML2LogoutRequestBuilder userPrincipal(String userPrincipal) {
this.userPrincipal = userPrincipal;
return this;
}

View file

@ -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)) {

View file

@ -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 {

View file

@ -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;
}
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;
}
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");
}
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");
}
SAML2Object samlObject = documentHolder.getSamlObject();
RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject;
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.findClient(issuer);
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");
}
}
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);
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 (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();
}
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();
}
}
private void logout(UserSessionModel userSession) {
authManager.logout(session, realm, userSession, uriInfo, clientConnection);
event.user(userSession.getUser()).session(userSession).success();
}
private boolean checkSsl() {
if (uriInfo.getBaseUri().getScheme().equals("https")) {
return true;
} else {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
}
protected class PostBindingProtocol extends BindingProtocol {
@Override
protected void verifySignature(SAMLDocumentHolder documentHolder, ClientModel client) throws VerificationException {
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
}
@Override
protected SAMLDocumentHolder extractDocument(String samlRequest) {
return SAMLRequestParser.parsePostBinding(samlRequest);
}
@Override
protected String getBindingType() {
return SalmProtocol.SAML_POST_BINDING;
}
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 RedirectBindingProtocol extends BindingProtocol {
@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);
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");
SamlProtocolUtils.verifyDocumentSignature(client, documentHolder.getSamlDocument());
PublicKey publicKey = SamlProtocolUtils.getPublicKey(client);
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);
}
}
@Override
protected SAMLDocumentHolder extractDocument(String samlRequest) {
return SAMLRequestParser.parseRedirectBinding(samlRequest);
}
@Override
protected String getBindingType() {
return SalmProtocol.SAML_GET_BINDING;
}
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);
}
}
/**
*/
@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);
}
/**
*/
@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");
}
if (samlRequest == null && samlResponse == null) {
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);
return new PostBindingProtocol().execute(samlRequest, samlResponse, relayState);
}
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");
}
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");
}
SAML2Object samlObject = documentHolder.getSamlObject();
RequestAbstractType requestAbstractType = (RequestAbstractType)samlObject;
String issuer = requestAbstractType.getIssuer().getValue();
ClientModel client = realm.findClient(issuer);
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Unknown login requester.");
}
if (!client.isEnabled()) {
event.error(Errors.CLIENT_DISABLED);
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, "Login requester not enabled.");
}
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");
}
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");
}
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.");
}
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");
}
}
protected Response loginRequest(String relayState, AuthnRequestType requestAbstractType, ClientModel client) {
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.");
}
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());
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 (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();
}
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();
}
}
private void logout(UserSessionModel userSession) {
authManager.logout(session, realm, userSession, uriInfo, clientConnection);
event.user(userSession.getUser()).session(userSession).success();
}
private boolean checkSsl() {
if (uriInfo.getBaseUri().getScheme().equals("https")) {
return true;
} else {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
}

View file

@ -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);
}
}
}

View file

@ -157,11 +157,13 @@ public class AccountTest {
});
}
/*
@Test
@Ignore
public void runit() throws Exception {
Thread.sleep(10000000);
}
*/
@Test
public void returnToAppFromQueryParam() {

View file

@ -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" : {