Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 02DE1200C6D for ; Sun, 7 May 2017 19:12:13 +0200 (CEST) Received: by cust-asf.ponee.io (Postfix) id 015BF160BB1; Sun, 7 May 2017 17:12:13 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id C598E160B97 for ; Sun, 7 May 2017 19:12:11 +0200 (CEST) Received: (qmail 94538 invoked by uid 500); 7 May 2017 17:12:11 -0000 Mailing-List: contact commits-help@directory.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@directory.apache.org Delivered-To: mailing list commits@directory.apache.org Received: (qmail 94529 invoked by uid 99); 7 May 2017 17:12:11 -0000 Received: from Unknown (HELO svn01-us-west.apache.org) (209.188.14.144) by apache.org (qpsmtpd/0.29) with ESMTP; Sun, 07 May 2017 17:12:11 +0000 Received: from svn01-us-west.apache.org (localhost [127.0.0.1]) by svn01-us-west.apache.org (ASF Mail Server at svn01-us-west.apache.org) with ESMTP id 62A4F3A0576 for ; Sun, 7 May 2017 17:12:10 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Subject: svn commit: r1794228 - /directory/site/trunk/content/fortress/testimonials.mdtext Date: Sun, 07 May 2017 17:12:09 -0000 To: commits@directory.apache.org From: smckinney@apache.org X-Mailer: svnmailer-1.0.9 Message-Id: <20170507171210.62A4F3A0576@svn01-us-west.apache.org> archived-at: Sun, 07 May 2017 17:12:13 -0000 Author: smckinney Date: Sun May 7 17:12:09 2017 New Revision: 1794228 URL: http://svn.apache.org/viewvc?rev=1794228&view=rev Log: add Modified: directory/site/trunk/content/fortress/testimonials.mdtext Modified: directory/site/trunk/content/fortress/testimonials.mdtext URL: http://svn.apache.org/viewvc/directory/site/trunk/content/fortress/testimonials.mdtext?rev=1794228&r1=1794227&r2=1794228&view=diff ============================================================================== --- directory/site/trunk/content/fortress/testimonials.mdtext (original) +++ directory/site/trunk/content/fortress/testimonials.mdtext Sun May 7 17:12:09 2017 @@ -1,3 +1,380 @@ # Testimonials -insert here.... +## Contributed by Yudhi Karunia Surtan for PT. Global Digital Niaga (blibli.com). + +This document contains an overview of the URL filtering mechanism. + +I created this solution because at the time I was looking an IAM and SSO solution, and there were no open source solution to provide everything that I required. + +Basically, the idea is, I wanted to have a framework where the developer doesn't need to programmatically make authorization calls, use annotation or any other kind of “if condition” statements, in their code. With this solution, I'm can have a declarative mechanism capable of dynamic authorization decisions, even if the user hasn't been logged in or has the the proper role activated. This is because the authorization has been centralized at the server and that server can activate and deactivate user roles that are needed to access the runtime environment. + +I searched for all available open source solutions and finally decided to use Apereo CAS and Apache Fortress as the combined solution. Apereo CAS does the authentication and Apache Fortress will handle the authorization. + +Apereo CAS is very good way to handle the Single Sign-On and Single Sign-Out problems, on the other hand Apereo CAS lacks authorization capaibilities because there are no standardized solutions for the authorization in that space yet. Apache Fortress is good at authorization because it uses standard RBAC. However, Apache Fortress doesn't have an SSO solution yet. That is why I think both can be combined and create a good solution because they complement each other. Unfortunately, there isn't a good documentation resource available to combine both solution into wone which is why I needed to create this to other developers on my team and make their life easier. + +With this solution, I have successfully run inside a production environment since 2015 and have maintained this solution for almost 2 years now, I write this documentation to describe how it works and how you can try something like this as well. + +Here are the technologies stack used within my extended framework: + + * Apereo CAS -> 4.2.x + * Apache Fortress Enmasse (rest) -> 1.0.0 + * Apache Fortress Proxy -> 1.0.0 + * Apache Ignite -> 1.7.0 + * Spring Framework -> 4.2.x-RELEASE + +There are two types of development required, one on server side and other on the client, which is then used by my team for managing security within their own web applications: + + * CAS Server side development: + - Create own implementation for AbstractUsernamePasswordAuthenticationHandler + - Implement Ignite Service Registry for CAS + + * CAS Client side development: + - Create own implementation for WebExpressionVoter + - Create own implementation for CasAuthenticationProvider + +## Code Descriptions + +###Server side development: + + * The Authentication Handler + + The interesting part for this solution is, how we can maintain both Apereo CAS and Apache Fortress sessions. Luckily, CAS is using token for maintaining their session and the token is also designed to have some extended attribute to put on it. Using this knowledge, we can do something with the profile given by CAS Server to the client. Let’s have a look what I’ve done with Apereo CAS and Apache Fortress Session in below source code. + + :::Java + + /* + * Copyright 2017 to PT. Global Digital Niaga(Blibli.com) + * + * Licensed under the Apache License, Version 2.0; 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.gdn.iam.cas; + + import java.io.StringWriter; + import java.security.GeneralSecurityException; + import java.util.HashMap; + import java.util.Map; + + import javax.xml.bind.JAXBContext; + import javax.xml.bind.JAXBException; + import javax.xml.bind.Marshaller; + + import org.apache.directory.fortress.core.AccessMgr; + import org.apache.directory.fortress.core.model.Session; + import org.apache.directory.fortress.core.model.User; + import org.jasig.cas.authentication.HandlerResult; + import org.jasig.cas.authentication.PreventedException; + import org.jasig.cas.authentication.UsernamePasswordCredential; + import org.jasig.cas.authentication.handler.support.AbstractUsernamePasswordAuthenticationHandler; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + + public class IamAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler { + + private static final Logger LOG = LoggerFactory.getLogger(IamAuthenticationHandler.class); + + private AccessMgr accessManager; + private JAXBContext jaxbContext; + private Marshaller marshaller; + + public IamAuthenticationHandler() { + try { + jaxbContext = JAXBContext.newInstance(Session.class); + marshaller = jaxbContext.createMarshaller(); + } catch (JAXBException e) { + LOG.error("cannot bind Session with jaxb context", e); + } + } + + @Override + protected HandlerResult authenticateUsernamePasswordInternal( + UsernamePasswordCredential usernamePasswordCredential) + throws GeneralSecurityException, PreventedException { + String username = usernamePasswordCredential.getUsername(); + String password = usernamePasswordCredential.getPassword(); + Session iamSession = null; + String iamXmlSession = null; + try { + LOG.trace("trying to authenticate username : {}, password : {}", + new Object[] {username, password}); + iamSession = accessManager.createSession(new User(username, password.toCharArray()), false); + LOG.trace("iam session : {}", iamSession); + if (iamSession != null) { + StringWriter writer = new StringWriter(); + marshaller.marshal(iamSession, writer); + iamXmlSession = writer.toString(); + LOG.trace("iam xml session : {}", iamXmlSession); + Map attributes = new HashMap<>(); + attributes.put("iamSession", iamXmlSession); + return createHandlerResult(usernamePasswordCredential, + principalFactory.createPrincipal(username, attributes), null); + } + } catch (org.apache.directory.fortress.core.SecurityException e) { + String errorMessage = "IAM authentication failed for [" + username + "]"; + LOG.trace(errorMessage); + throw new GeneralSecurityException(errorMessage); + } catch (JAXBException e) { + String errorMessage = "cannot marshalling session with value : " + iamSession == null ? "null" + : iamSession.toString(); + LOG.trace(errorMessage); + throw new GeneralSecurityException(errorMessage); + } + LOG.trace("returning default handler"); + return createHandlerResult(usernamePasswordCredential, + principalFactory.createPrincipal(username), null); + } + + public AccessMgr getAccessManager() { + return accessManager; + } + + public void setAccessManager(AccessMgr accessManager) { + this.accessManager = accessManager; + } + + } + + +### Next sections + +at above source code you can see where i construct a new principal by creating new attribute map with value of Apache Fortress Session xml. + + Attribute Populator + In order to populate fortress and pass it to the client we need to override casServiceValidationSuccess.jsp file, locate at WEB-INF/view/jsp/protocol/2.0/, since the default view is not populating the attributes. Here is how i did + +<%@ page session="false" contentType="application/xml; charset=UTF-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> + + + + ${fn:escapeXml(assertion.primaryAuthentication.principal.id)} + + + + + + + + + ${pgtIou} + + + + + ${fn:escapeXml(proxy.principal.id)} + + + + + + +One thing that i love from CAS, even you are correctly extract the attribute at this page (or might be you got hacked at this page). CAS Able to protected the returned attribute by changing the services registry configuration. see HTTPSandIMAPS-10000001.json file. I’ve put ReturnAllAttributeReleasePolicy type for debuging all the attribute returning, you can change it later to make your application more secure. + + Apache Ignite For Ticket Replication + To have a production readiness we need somehow to have a high availability requirement so we can not have a single cas server. That is why we need to have a centralize or distributed ticket repository so our cas able to scale. To scale the ticket repository, i choose Apache Ignite for distributing the ticket. To Implement is very simple, it is also written at Apereo CAS documentation. + +Client side development: + + The Spring Voter + Spring Framework is a great framework, they allowed you to put your own interceptor to have your own implementation. WebExpressionVoter is the class you need to extends in order you want to override the normal spring decision mechanism, usually you will use xml + regex for registering the condition. However, xml + regex is not the approach i want to have for my development team. See below code snippet, to understand what i did for make it more dynamic. + + @Override + @SuppressWarnings("static-access") + public int vote(Authentication authentication, FilterInvocation fi, + Collection attributes) { + Authentication securityContextAuthentication = + SecurityContextHolder.getContext().getAuthentication(); + int result = super.vote(securityContextAuthentication, fi, attributes); + if (System.getenv(IAM_SECURITY_PARAMETER) != null) { + LOG.warn("iam security is disable, enable all access mode is enable"); + return result; + } else { + LOG.debug("authentication = {}", + ToStringBuilder.reflectionToString(securityContextAuthentication)); + LOG.debug("super vote for : {}", result); + if (super.ACCESS_GRANTED == result) { + String requestMethod = fi.getRequest().getMethod().toLowerCase(); + String filterUrl = getFilterUrl(fi.getHttpRequest()); + if (filterUrl == null) { + return result; + } + try { + CasAuthenticationToken casAuthenticationToken = + ((CasAuthenticationToken) securityContextAuthentication); + LOG.debug("assertion : {}", + ToStringBuilder.reflectionToString(casAuthenticationToken.getAssertion())); + String iamSessionXml = (String) casAuthenticationToken.getAssertion().getAttributes() + .get(IAM_SESSION_ATTRIBUTE_KEY); + LOG.debug("iam session xml == {}", iamSessionXml); + Session iamSession = sessionCache.getIfPresent(casAuthenticationToken.getKeyHash()); + if (iamSession == null) { + Unmarshaller unmarshaller = null; + try { + unmarshaller = context.createUnmarshaller(); + } catch (JAXBException ex) { + LOG.warn("cannot create unmarshaller : ", ex); + } + iamSession = (Session) unmarshaller.unmarshal(new StringReader(iamSessionXml)); + sessionCache.put(casAuthenticationToken.getKeyHash(), iamSession); + } + StringBuilder sessionPermissionKeyBuilder = new StringBuilder(iamSession.getSessionId()).append(filterUrl).append(requestMethod); + Boolean isAllowed = accessCache.getIfPresent(sessionPermissionKeyBuilder.toString()); + if(isAllowed == null) { + isAllowed = accessManager.checkAccess(iamSession, new Permission(filterUrl, requestMethod)); + accessCache.put(sessionPermissionKeyBuilder.toString(), isAllowed); + } + LOG.debug("{} is {} to access {} with method {}", + new Object[] {securityContextAuthentication.getName(), + isAllowed ? "granted" : "denied", filterUrl, requestMethod}); + if (isAllowed) { + return super.ACCESS_GRANTED; + } + } catch (Exception e) { + LOG.error("catch exception when communicate with iam server", e); + } + } + return super.ACCESS_DENIED; + } + } + +Yep, i calling fortress to check if the user is allowed to access fortress permission or not. + + UserDetail Populator + Spring use the implementation of AbstractCasAssertionUserDetailsService to populate the user detail after the authentication success, you can see the example at IamUserDetails code, here is the snipet of that class + +@Override + protected UserDetails loadUserDetails(final Assertion assertion) { + List grantedAuthorities = new ArrayList<>(); + LOG.debug("user asssertion : {}", ToStringBuilder.reflectionToString(assertion)); + boolean accountNonExpired = true; + boolean credentialsNonExpired = true; + boolean accountNonLocked = true; + boolean enabled = true; + for (String attribute : this.attributes) { + String value = (String) assertion.getPrincipal().getAttributes().get(attribute); + LOG.debug("value = {}", value); + if (value != null) { + LOG.debug("adding default authorization to user"); + grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_USER)); + + Unmarshaller unmarshaller = null; + Session iamSession = null; + try { + unmarshaller = context.createUnmarshaller(); + iamSession = (Session) unmarshaller.unmarshal(new StringReader(value)); + for (UserRole role : iamSession.getRoles()) { + LOG.debug("adding {} authorization to user", role.getName().toUpperCase()); + grantedAuthorities.add(new SimpleGrantedAuthority(role.getName().toUpperCase())); + } + } catch (Exception ex) { + LOG.error("cannot generate user details", ex); + } + } + } + LOG.debug( + "accountNonExpired : {}, credentialsNonExpired : {}, accountNonLocked : {}, enabled : {}", + new Object[] {accountNonExpired, credentialsNonExpired, accountNonLocked, enabled}); + return new User(assertion.getPrincipal().getName().toLowerCase().trim(), NON_EXISTENT_PASSWORD_VALUE, enabled, + accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities); + } + +you can change the implementation later for your needs. + + Network Might Give Problem + Since it is the production environment, we need to consider that sometimes it might be a trouble in our network. That is why it is important to give some delay time in our application. + Here is the example how i delay some time in order the network is not as my expectation. + +/* + * Copyright 2017 to PT. Global Digital Niaga(Blibli.com) + * + * Licensed under the Apache License, Version 2.0; 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.gdn.iam.spring.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.cas.authentication.CasAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +public class GdnCasAuthenticationProvider extends CasAuthenticationProvider { + + private static transient Logger LOG = LoggerFactory.getLogger(GdnCasAuthenticationProvider.class); + private long sleepForDistributeTicketTime = 300; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + try { + LOG.trace( + "will try to sleep for waiting ticket to be distributed to other node, sleep time : {}", + getSleepForDistributeTicketTime()); + Thread.sleep(getSleepForDistributeTicketTime()); + } catch (InterruptedException e) { + LOG.error("something wrong when sleeping", e); + } + return super.authenticate(authentication); + } + + public long getSleepForDistributeTicketTime() { + return sleepForDistributeTicketTime; + } + + public void setSleepForDistributeTicketTime(long sleepForDistributeTicketTime) { + this.sleepForDistributeTicketTime = sleepForDistributeTicketTime; + } + +} + +Descriptions of authentication flow + +The CAS authentication flow will be the same, there none of changes made in term of the authentication flow. Further, you can see the flow at Apereo CAS 4.2.x documentation page. +The main different is we not put the ticket registry inside memory database, we put it on Apache Ignite cache so when other node is there it can replicate the ticket to that node. +Descriptions of authorization flow + +If you ever use Spring Security, usually you will put the authorization role configuration inside your xml or using annotation. This is the only difference between plain spring security with my extended framework solution, we put the configuration inside Fortress. Everytime user changing the URL, then it will check whenever the user has access to that specific URL or not through the extended voter class. If the user is authorized then the app will give the correct page, otherwise it will give 40X http status and page. +Instructions to test + +For testing this example, you need to understand that Apache Fortress configuration is necessary to find fortress.properties at the classpath so it might be good if you put that configuration file at the same classpath, for instance, if you are using tomcat remove all the fortress.properties inside the classes directory and put it on $TOMCAT_HOME/lib/ folder. Make sure you are make Apache Fortress running at the first step. Here are the detail instruction for testing this example : +Server Section + + Read and find the instruction at : + – https://github.com/apache/directory-fortress-core + – https://github.com/apache/directory-fortress-enmasse + – https://github.com/apache/directory-fortress-commander + and configure your Apache Fortress properly. + Clone the project from link at Where to download section below, change the configuration properly inside cas-fortress-servers/src/main/resources folder and package it using + mvn clean package. + Copy the war file from cas-fortress-server/target into the web-container deploy directory. + Start your web-container and you get cas fortress integrated. + +Client Section + + Simply put the war file inside the web-container deploy directory. + Open and login to your commander(fortress-web) + Create a user with role ROLE_USER (you can change to what ever role). The role need to align with spring-security.xml for this statement . This is the mandatory role, with this role we are seperate between the anonymous role and authenticate one. + Create a permission object containing your restricted url, for instance http://localhost:8080/cas-fortress-client/profile and http://localhost:8080/cas-fortress-client/catalog. + Map the permission object and role at permission tab at your commander. Currently we only support get for both of the url. + Start your web-container and play with your cas-fortress-client later on. + +Where to download + +https://github.com/bliblidotcom/cas-fortress-example +Next Steps + +The next step should be implementing ARBAC solution. Since i did not allowed people to create a conditional statement inside their code to check the roles, button or page element that should be not accessible for specific user will appear, even they can not go or do the action, that causing some confusion in term or usability for my user. With ARBAC i believe i can do a whitelist for the page attribute and increase the usability.