saml: move refactor files from server to api module - Move interfaces and classes from server to api module - This can be then used for pluggable api authenticators
Signed-off-by: Rohit Yadav <rohit.ya...@shapeblue.com> Project: http://git-wip-us.apache.org/repos/asf/cloudstack/repo Commit: http://git-wip-us.apache.org/repos/asf/cloudstack/commit/f1d85834 Tree: http://git-wip-us.apache.org/repos/asf/cloudstack/tree/f1d85834 Diff: http://git-wip-us.apache.org/repos/asf/cloudstack/diff/f1d85834 Branch: refs/heads/saml2 Commit: f1d858340f7faf7c56c6c68c7ef543599fd248a0 Parents: ffa09b5 Author: Rohit Yadav <rohit.ya...@shapeblue.com> Authored: Sun Aug 24 15:51:29 2014 +0200 Committer: Rohit Yadav <rohit.ya...@shapeblue.com> Committed: Sun Aug 24 15:51:29 2014 +0200 ---------------------------------------------------------------------- .../apache/cloudstack/api/ApiServerService.java | 45 +++ .../api/auth/APIAuthenticationManager.java | 24 ++ .../api/auth/APIAuthenticationType.java | 21 ++ .../cloudstack/api/auth/APIAuthenticator.java | 41 +++ .../cloudstack/SAML2UserAuthenticator.java | 65 ---- .../command/SAML2LoginAPIAuthenticatorCmd.java | 352 +++++++++++++++++++ .../command/SAML2LogoutAPIAuthenticatorCmd.java | 73 ++++ .../cloudstack/saml/SAML2UserAuthenticator.java | 65 ++++ server/src/com/cloud/api/ApiServerService.java | 45 --- .../api/auth/APIAuthenticationManager.java | 24 -- .../cloud/api/auth/APIAuthenticationType.java | 21 -- .../com/cloud/api/auth/APIAuthenticator.java | 41 --- .../api/auth/SAML2LoginAPIAuthenticatorCmd.java | 350 ------------------ .../auth/SAML2LogoutAPIAuthenticatorCmd.java | 71 ---- 14 files changed, 621 insertions(+), 617 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/api/src/org/apache/cloudstack/api/ApiServerService.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/ApiServerService.java b/api/src/org/apache/cloudstack/api/ApiServerService.java new file mode 100644 index 0000000..9c0cfa3 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/ApiServerService.java @@ -0,0 +1,45 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.api; + +import com.cloud.exception.CloudAuthenticationException; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; + +import javax.servlet.http.HttpSession; +import java.util.Map; + +public interface ApiServerService { + public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId) throws ServerApiException; + + public Long fetchDomainId(String domainUUID); + + public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress, + Map<String, Object[]> requestParameters) throws CloudAuthenticationException; + + public void logoutUser(long userId); + + public boolean verifyUser(Long userId); + + public String getSerializedApiError(int errorCode, String errorText, Map<String, Object[]> apiCommandParams, String responseType); + + public String getSerializedApiError(ServerApiException ex, Map<String, Object[]> apiCommandParams, String responseType); + + public String handleRequest(Map params, String responseType, StringBuilder auditTrailSb) throws ServerApiException; + + public Class<?> getCmdClass(String cmdName); +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java new file mode 100644 index 0000000..5d4d664 --- /dev/null +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationManager.java @@ -0,0 +1,24 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.api.auth; + +import com.cloud.utils.component.PluggableService; + +public interface APIAuthenticationManager extends PluggableService { + public APIAuthenticator getAPIAuthenticator(String name); +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java new file mode 100644 index 0000000..e8c7f0f --- /dev/null +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticationType.java @@ -0,0 +1,21 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.api.auth; + +public enum APIAuthenticationType { + LOGIN_API, LOGOUT_API +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java ---------------------------------------------------------------------- diff --git a/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java new file mode 100644 index 0000000..20fe61f --- /dev/null +++ b/api/src/org/apache/cloudstack/api/auth/APIAuthenticator.java @@ -0,0 +1,41 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.api.auth; + +import org.apache.cloudstack.api.ServerApiException; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Map; + +/* +* APIAuthenticator is an interface that defines method that +* a class should implement that help with Authentication and accepts +* a command string and an array of parameters. This should be used only +* in the Servlet that is doing authentication. +* +* @param command The API command name such as login, logout etc +* @param params An array of HTTP parameters +* @param session HttpSession object +* */ +public interface APIAuthenticator { + public String authenticate(String command, Map<String, Object[]> params, + HttpSession session, String remoteAddress, String responseType, + StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException; + public APIAuthenticationType getAPIType(); + +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java deleted file mode 100644 index 4d4f1d3..0000000 --- a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/SAML2UserAuthenticator.java +++ /dev/null @@ -1,65 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one or more -// contributor license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright ownership. -// The ASF licenses this file to You 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.apache.cloudstack; - -import com.cloud.server.auth.DefaultUserAuthenticator; -import com.cloud.server.auth.UserAuthenticator; -import com.cloud.user.User; -import com.cloud.user.UserAccount; -import com.cloud.user.dao.UserAccountDao; -import com.cloud.user.dao.UserDao; -import com.cloud.utils.Pair; -import org.apache.log4j.Logger; - -import javax.ejb.Local; -import javax.inject.Inject; -import java.util.Map; - -@Local(value = {UserAuthenticator.class}) -public class SAML2UserAuthenticator extends DefaultUserAuthenticator { - public static final Logger s_logger = Logger.getLogger(SAML2UserAuthenticator.class); - - @Inject - private UserAccountDao _userAccountDao; - @Inject - private UserDao _userDao; - - @Override - public Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, Map<String, Object[]> requestParameters) { - if (s_logger.isDebugEnabled()) { - s_logger.debug("Trying SAML2 auth for user: " + username); - } - final UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); - if (userAccount == null) { - s_logger.debug("Unable to find user with " + username + " in domain " + domainId); - return new Pair<Boolean, ActionOnFailedAuthentication>(false, null); - } else { - User user = _userDao.getUser(userAccount.getId()); - // TODO: check SAMLRequest, signature etc. from requestParameters - if (user != null && user.getUuid().startsWith("saml")) { - return new Pair<Boolean, ActionOnFailedAuthentication>(true, null); - } - } - // Deny all by default - return new Pair<Boolean, ActionOnFailedAuthentication>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT); - } - - @Override - public String encode(final String password) { - // TODO: Complete method - StringBuilder sb = new StringBuilder(32); - return sb.toString(); - } -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java new file mode 100644 index 0000000..611c69b --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LoginAPIAuthenticatorCmd.java @@ -0,0 +1,352 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.api.command; + +import org.apache.cloudstack.api.ApiServerService; +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.exception.CloudAuthenticationException; +import com.cloud.user.Account; +import com.cloud.user.User; +import com.cloud.utils.HttpUtils; +import com.cloud.utils.db.EntityManager; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.response.LoginCmdResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.log4j.Logger; +import org.joda.time.DateTime; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.common.SAMLVersion; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; +import org.opensaml.saml2.core.AuthnContextClassRef; +import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; +import org.opensaml.saml2.core.AuthnRequest; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.NameIDPolicy; +import org.opensaml.saml2.core.NameIDType; +import org.opensaml.saml2.core.RequestedAuthnContext; +import org.opensaml.saml2.core.Response; +import org.opensaml.saml2.core.StatusCode; +import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; +import org.opensaml.saml2.core.impl.AuthnRequestBuilder; +import org.opensaml.saml2.core.impl.IssuerBuilder; +import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; +import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.XMLObject; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.io.Unmarshaller; +import org.opensaml.xml.io.UnmarshallerFactory; +import org.opensaml.xml.io.UnmarshallingException; +import org.opensaml.xml.signature.Signature; +import org.opensaml.xml.util.Base64; +import org.opensaml.xml.util.XMLHelper; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.inject.Inject; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.stream.FactoryConfigurationError; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.math.BigInteger; +import java.net.URLEncoder; +import java.security.SecureRandom; +import java.util.List; +import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; + +@APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) +public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(SAML2LoginAPIAuthenticatorCmd.class.getName()); + private static final String s_name = "loginresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + @Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true) + private String idpUrl; + + @Inject + ApiServerService _apiServer; + @Inject + EntityManager _entityMgr; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getIdpUrl() { + return idpUrl; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + public String buildAuthnRequestUrl(String consumerUrl, String identityProviderUrl) { + String randomId = new BigInteger(130, new SecureRandom()).toString(32); + String spId = "org.apache.cloudstack"; + String redirectUrl = ""; + try { + DefaultBootstrap.bootstrap(); + AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl); + redirectUrl = identityProviderUrl + "?SAMLRequest=" + encodeAuthnRequest(authnRequest); + } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { + s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); + } + return redirectUrl; + } + + private AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) { + // Issuer object + IssuerBuilder issuerBuilder = new IssuerBuilder(); + Issuer issuer = issuerBuilder.buildObject(); + issuer.setValue(spId); + + // NameIDPolicy + NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); + NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); + nameIdPolicy.setFormat(NameIDType.PERSISTENT); + nameIdPolicy.setSPNameQualifier("Apache CloudStack"); + nameIdPolicy.setAllowCreate(true); + + // AuthnContextClass + AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); + AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( + SAMLConstants.SAML20_NS, + "AuthnContextClassRef", "saml"); + authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); + + // AuthnContex + RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); + RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); + requestedAuthnContext + .setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM); + requestedAuthnContext.getAuthnContextClassRefs().add( + authnContextClassRef); + + // Creation of AuthRequestObject + AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); + AuthnRequest authnRequest = authRequestBuilder.buildObject(); + authnRequest.setID(authnId); + authnRequest.setDestination(idpUrl); + authnRequest.setVersion(SAMLVersion.VERSION_20); + authnRequest.setForceAuthn(true); + authnRequest.setIsPassive(false); + authnRequest.setIssuer(issuer); + authnRequest.setIssueInstant(new DateTime()); + authnRequest.setProviderName("Apache CloudStack"); + authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); + authnRequest.setAssertionConsumerServiceURL(consumerUrl); + authnRequest.setNameIDPolicy(nameIdPolicy); + authnRequest.setRequestedAuthnContext(requestedAuthnContext); + + return authnRequest; + } + + private String encodeAuthnRequest(AuthnRequest authnRequest) + throws MarshallingException, IOException { + Marshaller marshaller = Configuration.getMarshallerFactory() + .getMarshaller(authnRequest); + Element authDOM = marshaller.marshall(authnRequest); + StringWriter requestWriter = new StringWriter(); + XMLHelper.writeNode(authDOM, requestWriter); + String requestMessage = requestWriter.toString(); + Deflater deflater = new Deflater(Deflater.DEFLATED, true); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater); + deflaterOutputStream.write(requestMessage.getBytes()); + deflaterOutputStream.close(); + String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); + encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim(); + return encodedRequestMessage; + } + + public Response processSAMLResponse(String responseMessage) { + XMLObject responseObject = null; + try { + responseObject = this.unmarshall(responseMessage); + + } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { + s_logger.error("SAMLResponse processing error: " + e.getMessage()); + } + return (Response) responseObject; + } + + private XMLObject unmarshall(String responseMessage) + throws ConfigurationException, ParserConfigurationException, + SAXException, IOException, UnmarshallingException { + try { + DefaultBootstrap.bootstrap(); + } catch (ConfigurationException | FactoryConfigurationError e) { + s_logger.error("SAML response message decoding error: " + e.getMessage()); + } + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); + byte[] base64DecodedResponse = Base64.decode(responseMessage); + Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse)); + Element element = document.getDocumentElement(); + UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); + Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); + return unmarshaller.unmarshall(element); + } + + @Override + public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { + try { + if (!params.containsKey("SAMLResponse")) { + final String[] idps = (String[])params.get("idpurl"); + String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=samlsso", idps[0]); + resp.sendRedirect(redirectUrl); + return ""; + } else { + final String samlResponse = ((String[])params.get("SAMLResponse"))[0]; + Response processedSAMLResponse = processSAMLResponse(samlResponse); + String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); + if (!statusCode.equals(StatusCode.SUCCESS_URI)) { + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Identity Provider send a non-successful authentication status code", + params, responseType)); + } + + Signature sig = processedSAMLResponse.getSignature(); + //SignatureValidator validator = new SignatureValidator(credential); + //validator.validate(sig); + + String uniqueUserId = null; + String accountName = "admin"; //GET from config, try, fail + Long domainId = 1L; // GET from config, try, fail + String username = null; + String password = ""; + String firstName = ""; + String lastName = ""; + String timeZone = ""; + String email = ""; + + Assertion assertion = processedSAMLResponse.getAssertions().get(0); + NameID nameId = assertion.getSubject().getNameID(); + + if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) { + username = nameId.getValue(); + uniqueUserId = "saml-" + username; + if (nameId.getFormat().equals(NameIDType.EMAIL)) { + email = username; + } + } + + String issuer = assertion.getIssuer().getValue(); + String audience = assertion.getConditions().getAudienceRestrictions().get(0).getAudiences().get(0).getAudienceURI(); + AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0); + List<Attribute> attributes = attributeStatement.getAttributes(); + + // Try capturing standard LDAP attributes + for (Attribute attribute: attributes) { + String attributeName = attribute.getName(); + String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent(); + if (attributeName.equalsIgnoreCase("uid") && uniqueUserId == null) { + username = attributeValue; + uniqueUserId = "saml-" + username; + } else if (attributeName.equalsIgnoreCase("givenName")) { + firstName = attributeValue; + } else if (attributeName.equalsIgnoreCase(("sn"))) { + lastName = attributeValue; + } else if (attributeName.equalsIgnoreCase("mail")) { + email = attributeValue; + } + } + + User user = _entityMgr.findByUuid(User.class, uniqueUserId); + if (user == null && uniqueUserId != null && username != null + && accountName != null && domainId != null) { + CallContext.current().setEventDetails("UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName); + user = _accountService.createUser(username, password, firstName, lastName, email, timeZone, accountName, domainId, uniqueUserId); + } + + if (user != null) { + try { + if (_apiServer.verifyUser(user.getId())) { + LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, user.getPassword(), domainId, null, remoteAddress, params); + resp.addCookie(new Cookie("userid", loginResponse.getUserId())); + resp.addCookie(new Cookie("domainid", loginResponse.getDomainId())); + resp.addCookie(new Cookie("role", loginResponse.getType())); + resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8))); + resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName())); + resp.sendRedirect("http://localhost:8080/client"); + return ApiResponseSerializer.toSerializedString(loginResponse, responseType); + + } + } catch (final CloudAuthenticationException ignored) { + } + } + } + } catch (IOException e) { + auditTrailSb.append("SP initiated SAML authentication using HTTP redirection failed:"); + auditTrailSb.append(e.getMessage()); + } + throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), + "Unable to authenticate or retrieve user while performing SAML based SSO", + params, responseType)); + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java new file mode 100644 index 0000000..b82f2c8 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/api/command/SAML2LogoutAPIAuthenticatorCmd.java @@ -0,0 +1,73 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.apache.cloudstack.api.command; + +import com.cloud.api.response.ApiResponseSerializer; +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.response.LogoutCmdResponse; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Map; + +@APICommand(name = "samlslo", description = "SAML Single Log Out API", responseObject = LogoutCmdResponse.class, entityType = {}) +public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { + public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName()); + private static final String s_name = "logoutresponse"; + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.ACCOUNT_TYPE_NORMAL; + } + + @Override + public void execute() throws ServerApiException { + // We should never reach here + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { + auditTrailSb.append("=== Logging out ==="); + // TODO: check global config and do either local or global log out + LogoutCmdResponse response = new LogoutCmdResponse(); + response.setDescription("success"); + response.setResponseName(getCommandName()); + return ApiResponseSerializer.toSerializedString(response, responseType); + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGOUT_API; + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java ---------------------------------------------------------------------- diff --git a/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java new file mode 100644 index 0000000..1a37f65 --- /dev/null +++ b/plugins/user-authenticators/saml2/src/org/apache/cloudstack/saml/SAML2UserAuthenticator.java @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You 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.apache.cloudstack.saml; + +import com.cloud.server.auth.DefaultUserAuthenticator; +import com.cloud.server.auth.UserAuthenticator; +import com.cloud.user.User; +import com.cloud.user.UserAccount; +import com.cloud.user.dao.UserAccountDao; +import com.cloud.user.dao.UserDao; +import com.cloud.utils.Pair; +import org.apache.log4j.Logger; + +import javax.ejb.Local; +import javax.inject.Inject; +import java.util.Map; + +@Local(value = {UserAuthenticator.class}) +public class SAML2UserAuthenticator extends DefaultUserAuthenticator { + public static final Logger s_logger = Logger.getLogger(SAML2UserAuthenticator.class); + + @Inject + private UserAccountDao _userAccountDao; + @Inject + private UserDao _userDao; + + @Override + public Pair<Boolean, ActionOnFailedAuthentication> authenticate(String username, String password, Long domainId, Map<String, Object[]> requestParameters) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Trying SAML2 auth for user: " + username); + } + final UserAccount userAccount = _userAccountDao.getUserAccount(username, domainId); + if (userAccount == null) { + s_logger.debug("Unable to find user with " + username + " in domain " + domainId); + return new Pair<Boolean, ActionOnFailedAuthentication>(false, null); + } else { + User user = _userDao.getUser(userAccount.getId()); + // TODO: check SAMLRequest, signature etc. from requestParameters + if (user != null && user.getUuid().startsWith("saml")) { + return new Pair<Boolean, ActionOnFailedAuthentication>(true, null); + } + } + // Deny all by default + return new Pair<Boolean, ActionOnFailedAuthentication>(false, ActionOnFailedAuthentication.INCREMENT_INCORRECT_LOGIN_ATTEMPT_COUNT); + } + + @Override + public String encode(final String password) { + // TODO: Complete method + StringBuilder sb = new StringBuilder(32); + return sb.toString(); + } +} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/server/src/com/cloud/api/ApiServerService.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/ApiServerService.java b/server/src/com/cloud/api/ApiServerService.java deleted file mode 100644 index aa3b8f7..0000000 --- a/server/src/com/cloud/api/ApiServerService.java +++ /dev/null @@ -1,45 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 com.cloud.api; - -import com.cloud.exception.CloudAuthenticationException; -import org.apache.cloudstack.api.ResponseObject; -import org.apache.cloudstack.api.ServerApiException; - -import javax.servlet.http.HttpSession; -import java.util.Map; - -public interface ApiServerService { - public boolean verifyRequest(Map<String, Object[]> requestParameters, Long userId) throws ServerApiException; - - public Long fetchDomainId(String domainUUID); - - public ResponseObject loginUser(HttpSession session, String username, String password, Long domainId, String domainPath, String loginIpAddress, - Map<String, Object[]> requestParameters) throws CloudAuthenticationException; - - public void logoutUser(long userId); - - public boolean verifyUser(Long userId); - - public String getSerializedApiError(int errorCode, String errorText, Map<String, Object[]> apiCommandParams, String responseType); - - public String getSerializedApiError(ServerApiException ex, Map<String, Object[]> apiCommandParams, String responseType); - - public String handleRequest(Map params, String responseType, StringBuilder auditTrailSb) throws ServerApiException; - - public Class<?> getCmdClass(String cmdName); -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/server/src/com/cloud/api/auth/APIAuthenticationManager.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/auth/APIAuthenticationManager.java b/server/src/com/cloud/api/auth/APIAuthenticationManager.java deleted file mode 100644 index 7fd4da9..0000000 --- a/server/src/com/cloud/api/auth/APIAuthenticationManager.java +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 com.cloud.api.auth; - -import com.cloud.utils.component.PluggableService; - -public interface APIAuthenticationManager extends PluggableService { - public APIAuthenticator getAPIAuthenticator(String name); -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/server/src/com/cloud/api/auth/APIAuthenticationType.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/auth/APIAuthenticationType.java b/server/src/com/cloud/api/auth/APIAuthenticationType.java deleted file mode 100644 index bdd37ec..0000000 --- a/server/src/com/cloud/api/auth/APIAuthenticationType.java +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 com.cloud.api.auth; - -public enum APIAuthenticationType { - LOGIN_API, LOGOUT_API -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/server/src/com/cloud/api/auth/APIAuthenticator.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/auth/APIAuthenticator.java b/server/src/com/cloud/api/auth/APIAuthenticator.java deleted file mode 100644 index 90cd7ec..0000000 --- a/server/src/com/cloud/api/auth/APIAuthenticator.java +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 com.cloud.api.auth; - -import org.apache.cloudstack.api.ServerApiException; - -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.util.Map; - -/* -* APIAuthenticator is an interface that defines method that -* a class should implement that help with Authentication and accepts -* a command string and an array of parameters. This should be used only -* in the Servlet that is doing authentication. -* -* @param command The API command name such as login, logout etc -* @param params An array of HTTP parameters -* @param session HttpSession object -* */ -public interface APIAuthenticator { - public String authenticate(String command, Map<String, Object[]> params, - HttpSession session, String remoteAddress, String responseType, - StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException; - public APIAuthenticationType getAPIType(); - -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java deleted file mode 100644 index ce97cfd..0000000 --- a/server/src/com/cloud/api/auth/SAML2LoginAPIAuthenticatorCmd.java +++ /dev/null @@ -1,350 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 com.cloud.api.auth; - -import com.cloud.api.ApiServerService; -import com.cloud.api.response.ApiResponseSerializer; -import com.cloud.exception.CloudAuthenticationException; -import com.cloud.user.Account; -import com.cloud.user.User; -import com.cloud.utils.HttpUtils; -import com.cloud.utils.db.EntityManager; -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.Parameter; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.LoginCmdResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.log4j.Logger; -import org.joda.time.DateTime; -import org.opensaml.Configuration; -import org.opensaml.DefaultBootstrap; -import org.opensaml.common.SAMLVersion; -import org.opensaml.common.xml.SAMLConstants; -import org.opensaml.saml2.core.Assertion; -import org.opensaml.saml2.core.Attribute; -import org.opensaml.saml2.core.AttributeStatement; -import org.opensaml.saml2.core.AuthnContextClassRef; -import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; -import org.opensaml.saml2.core.AuthnRequest; -import org.opensaml.saml2.core.Issuer; -import org.opensaml.saml2.core.NameID; -import org.opensaml.saml2.core.NameIDPolicy; -import org.opensaml.saml2.core.NameIDType; -import org.opensaml.saml2.core.RequestedAuthnContext; -import org.opensaml.saml2.core.Response; -import org.opensaml.saml2.core.StatusCode; -import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder; -import org.opensaml.saml2.core.impl.AuthnRequestBuilder; -import org.opensaml.saml2.core.impl.IssuerBuilder; -import org.opensaml.saml2.core.impl.NameIDPolicyBuilder; -import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder; -import org.opensaml.xml.ConfigurationException; -import org.opensaml.xml.XMLObject; -import org.opensaml.xml.io.Marshaller; -import org.opensaml.xml.io.MarshallingException; -import org.opensaml.xml.io.Unmarshaller; -import org.opensaml.xml.io.UnmarshallerFactory; -import org.opensaml.xml.io.UnmarshallingException; -import org.opensaml.xml.signature.Signature; -import org.opensaml.xml.util.Base64; -import org.opensaml.xml.util.XMLHelper; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.xml.sax.SAXException; - -import javax.inject.Inject; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.stream.FactoryConfigurationError; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.StringWriter; -import java.math.BigInteger; -import java.net.URLEncoder; -import java.security.SecureRandom; -import java.util.List; -import java.util.Map; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; - -@APICommand(name = "samlsso", description = "SP initiated SAML Single Sign On", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {}) -public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { - public static final Logger s_logger = Logger.getLogger(SAML2LoginAPIAuthenticatorCmd.class.getName()); - private static final String s_name = "loginresponse"; - - ///////////////////////////////////////////////////// - //////////////// API parameters ///////////////////// - ///////////////////////////////////////////////////// - @Parameter(name = ApiConstants.IDP_URL, type = CommandType.STRING, description = "Identity Provider SSO HTTP-Redirect binding URL", required = true) - private String idpUrl; - - @Inject - ApiServerService _apiServer; - @Inject - EntityManager _entityMgr; - - ///////////////////////////////////////////////////// - /////////////////// Accessors /////////////////////// - ///////////////////////////////////////////////////// - - public String getIdpUrl() { - return idpUrl; - } - - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// - - @Override - public String getCommandName() { - return s_name; - } - - @Override - public long getEntityOwnerId() { - return Account.ACCOUNT_TYPE_NORMAL; - } - - @Override - public void execute() throws ServerApiException { - // We should never reach here - throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); - } - - public String buildAuthnRequestUrl(String consumerUrl, String identityProviderUrl) { - String randomId = new BigInteger(130, new SecureRandom()).toString(32); - String spId = "org.apache.cloudstack"; - String redirectUrl = ""; - try { - DefaultBootstrap.bootstrap(); - AuthnRequest authnRequest = this.buildAuthnRequestObject(randomId, spId, identityProviderUrl, consumerUrl); - redirectUrl = identityProviderUrl + "?SAMLRequest=" + encodeAuthnRequest(authnRequest); - } catch (ConfigurationException | FactoryConfigurationError | MarshallingException | IOException e) { - s_logger.error("SAML AuthnRequest message building error: " + e.getMessage()); - } - return redirectUrl; - } - - private AuthnRequest buildAuthnRequestObject(String authnId, String spId, String idpUrl, String consumerUrl) { - // Issuer object - IssuerBuilder issuerBuilder = new IssuerBuilder(); - Issuer issuer = issuerBuilder.buildObject(); - issuer.setValue(spId); - - // NameIDPolicy - NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); - NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); - nameIdPolicy.setFormat(NameIDType.PERSISTENT); - nameIdPolicy.setSPNameQualifier("Apache CloudStack"); - nameIdPolicy.setAllowCreate(true); - - // AuthnContextClass - AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); - AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject( - SAMLConstants.SAML20_NS, - "AuthnContextClassRef", "saml"); - authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); - - // AuthnContex - RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); - RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); - requestedAuthnContext - .setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM); - requestedAuthnContext.getAuthnContextClassRefs().add( - authnContextClassRef); - - // Creation of AuthRequestObject - AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); - AuthnRequest authnRequest = authRequestBuilder.buildObject(); - authnRequest.setID(authnId); - authnRequest.setDestination(idpUrl); - authnRequest.setVersion(SAMLVersion.VERSION_20); - authnRequest.setForceAuthn(true); - authnRequest.setIsPassive(false); - authnRequest.setIssuer(issuer); - authnRequest.setIssueInstant(new DateTime()); - authnRequest.setProviderName("Apache CloudStack"); - authnRequest.setProtocolBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); //SAML2_ARTIFACT_BINDING_URI); - authnRequest.setAssertionConsumerServiceURL(consumerUrl); - authnRequest.setNameIDPolicy(nameIdPolicy); - authnRequest.setRequestedAuthnContext(requestedAuthnContext); - - return authnRequest; - } - - private String encodeAuthnRequest(AuthnRequest authnRequest) - throws MarshallingException, IOException { - Marshaller marshaller = Configuration.getMarshallerFactory() - .getMarshaller(authnRequest); - Element authDOM = marshaller.marshall(authnRequest); - StringWriter requestWriter = new StringWriter(); - XMLHelper.writeNode(authDOM, requestWriter); - String requestMessage = requestWriter.toString(); - Deflater deflater = new Deflater(Deflater.DEFLATED, true); - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater); - deflaterOutputStream.write(requestMessage.getBytes()); - deflaterOutputStream.close(); - String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES); - encodedRequestMessage = URLEncoder.encode(encodedRequestMessage, "UTF-8").trim(); - return encodedRequestMessage; - } - - public Response processSAMLResponse(String responseMessage) { - XMLObject responseObject = null; - try { - responseObject = this.unmarshall(responseMessage); - - } catch (ConfigurationException | ParserConfigurationException | SAXException | IOException | UnmarshallingException e) { - s_logger.error("SAMLResponse processing error: " + e.getMessage()); - } - return (Response) responseObject; - } - - private XMLObject unmarshall(String responseMessage) - throws ConfigurationException, ParserConfigurationException, - SAXException, IOException, UnmarshallingException { - try { - DefaultBootstrap.bootstrap(); - } catch (ConfigurationException | FactoryConfigurationError e) { - s_logger.error("SAML response message decoding error: " + e.getMessage()); - } - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setNamespaceAware(true); - DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); - byte[] base64DecodedResponse = Base64.decode(responseMessage); - Document document = docBuilder.parse(new ByteArrayInputStream(base64DecodedResponse)); - Element element = document.getDocumentElement(); - UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); - Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); - return unmarshaller.unmarshall(element); - } - - @Override - public String authenticate(final String command, final Map<String, Object[]> params, final HttpSession session, final String remoteAddress, final String responseType, final StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { - try { - if (!params.containsKey("SAMLResponse")) { - final String[] idps = (String[])params.get("idpurl"); - String redirectUrl = buildAuthnRequestUrl("http://localhost:8080/client/api?command=samlsso", idps[0]); - resp.sendRedirect(redirectUrl); - return ""; - } else { - final String samlResponse = ((String[])params.get("SAMLResponse"))[0]; - Response processedSAMLResponse = processSAMLResponse(samlResponse); - String statusCode = processedSAMLResponse.getStatus().getStatusCode().getValue(); - if (!statusCode.equals(StatusCode.SUCCESS_URI)) { - throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), - "Identity Provider send a non-successful authentication status code", - params, responseType)); - } - - Signature sig = processedSAMLResponse.getSignature(); - //SignatureValidator validator = new SignatureValidator(credential); - //validator.validate(sig); - - String uniqueUserId = null; - String accountName = "admin"; //GET from config, try, fail - Long domainId = 1L; // GET from config, try, fail - String username = null; - String password = ""; - String firstName = ""; - String lastName = ""; - String timeZone = ""; - String email = ""; - - Assertion assertion = processedSAMLResponse.getAssertions().get(0); - NameID nameId = assertion.getSubject().getNameID(); - - if (nameId.getFormat().equals(NameIDType.PERSISTENT) || nameId.getFormat().equals(NameIDType.EMAIL)) { - username = nameId.getValue(); - uniqueUserId = "saml-" + username; - if (nameId.getFormat().equals(NameIDType.EMAIL)) { - email = username; - } - } - - String issuer = assertion.getIssuer().getValue(); - String audience = assertion.getConditions().getAudienceRestrictions().get(0).getAudiences().get(0).getAudienceURI(); - AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0); - List<Attribute> attributes = attributeStatement.getAttributes(); - - // Try capturing standard LDAP attributes - for (Attribute attribute: attributes) { - String attributeName = attribute.getName(); - String attributeValue = attribute.getAttributeValues().get(0).getDOM().getTextContent(); - if (attributeName.equalsIgnoreCase("uid") && uniqueUserId == null) { - username = attributeValue; - uniqueUserId = "saml-" + username; - } else if (attributeName.equalsIgnoreCase("givenName")) { - firstName = attributeValue; - } else if (attributeName.equalsIgnoreCase(("sn"))) { - lastName = attributeValue; - } else if (attributeName.equalsIgnoreCase("mail")) { - email = attributeValue; - } - } - - User user = _entityMgr.findByUuid(User.class, uniqueUserId); - if (user == null && uniqueUserId != null && username != null - && accountName != null && domainId != null) { - CallContext.current().setEventDetails("UserName: " + username + ", FirstName :" + password + ", LastName: " + lastName); - user = _accountService.createUser(username, password, firstName, lastName, email, timeZone, accountName, domainId, uniqueUserId); - } - - if (user != null) { - try { - if (_apiServer.verifyUser(user.getId())) { - LoginCmdResponse loginResponse = (LoginCmdResponse) _apiServer.loginUser(session, username, user.getPassword(), domainId, null, remoteAddress, params); - resp.addCookie(new Cookie("userid", loginResponse.getUserId())); - resp.addCookie(new Cookie("domainid", loginResponse.getDomainId())); - resp.addCookie(new Cookie("role", loginResponse.getType())); - resp.addCookie(new Cookie("username", URLEncoder.encode(loginResponse.getUsername(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("sessionKey", URLEncoder.encode(loginResponse.getSessionKey(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("account", URLEncoder.encode(loginResponse.getAccount(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("timezone", URLEncoder.encode(loginResponse.getTimeZone(), HttpUtils.UTF_8))); - resp.addCookie(new Cookie("userfullname", loginResponse.getFirstName() + "%20" + loginResponse.getLastName())); - resp.sendRedirect("http://localhost:8080/client"); - return ApiResponseSerializer.toSerializedString(loginResponse, responseType); - - } - } catch (final CloudAuthenticationException ignored) { - } - } - } - } catch (IOException e) { - auditTrailSb.append("SP initiated SAML authentication using HTTP redirection failed:"); - auditTrailSb.append(e.getMessage()); - } - throw new ServerApiException(ApiErrorCode.ACCOUNT_ERROR, _apiServer.getSerializedApiError(ApiErrorCode.ACCOUNT_ERROR.getHttpCode(), - "Unable to authenticate or retrieve user while performing SAML based SSO", - params, responseType)); - } - - @Override - public APIAuthenticationType getAPIType() { - return APIAuthenticationType.LOGIN_API; - } -} http://git-wip-us.apache.org/repos/asf/cloudstack/blob/f1d85834/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java ---------------------------------------------------------------------- diff --git a/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java b/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java deleted file mode 100644 index 9119588..0000000 --- a/server/src/com/cloud/api/auth/SAML2LogoutAPIAuthenticatorCmd.java +++ /dev/null @@ -1,71 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you 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 com.cloud.api.auth; - -import com.cloud.api.response.ApiResponseSerializer; -import com.cloud.user.Account; -import org.apache.cloudstack.api.APICommand; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.BaseCmd; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.LogoutCmdResponse; -import org.apache.log4j.Logger; - -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.util.Map; - -@APICommand(name = "samlslo", description = "SAML Single Log Out API", responseObject = LogoutCmdResponse.class, entityType = {}) -public class SAML2LogoutAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator { - public static final Logger s_logger = Logger.getLogger(SAML2LogoutAPIAuthenticatorCmd.class.getName()); - private static final String s_name = "logoutresponse"; - - ///////////////////////////////////////////////////// - /////////////// API Implementation/////////////////// - ///////////////////////////////////////////////////// - - @Override - public String getCommandName() { - return s_name; - } - - @Override - public long getEntityOwnerId() { - return Account.ACCOUNT_TYPE_NORMAL; - } - - @Override - public void execute() throws ServerApiException { - // We should never reach here - throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); - } - - @Override - public String authenticate(String command, Map<String, Object[]> params, HttpSession session, String remoteAddress, String responseType, StringBuilder auditTrailSb, final HttpServletResponse resp) throws ServerApiException { - auditTrailSb.append("=== Logging out ==="); - // TODO: check global config and do either local or global log out - LogoutCmdResponse response = new LogoutCmdResponse(); - response.setDescription("success"); - response.setResponseName(getCommandName()); - return ApiResponseSerializer.toSerializedString(response, responseType); - } - - @Override - public APIAuthenticationType getAPIType() { - return APIAuthenticationType.LOGOUT_API; - } -}