08 abril 2011

[Single Sign-On] Configurando SSO no Spring Security com o JaSig CAS

Nas "horas vagas" que eu disponho aqui na empresa, eu tento estudar coisas interessantes, úteis para uma eventual migração tecnológica que um dia pode vir a acontecer por aqui. Comecei com JPA 2, integrando com Spring, depois juntando isso com JSF. A missão desta semana foi implementar uma solução de Single Sign-On, ou seja, exatamente o que ocorre quando você se loga no google, e não precisa mais logar quando usar outros serviços o do google (entre eles, os costumeiros gmail, google reader, orkut, youtube, etc).

Acabei de testar, com tudo ainda meio default, mas aparentemente está funcionando. Por isso, eu vou documentar as configurações que eu tive que fazer aqui, para que eu mesmo não esqueça posteriormente.

Entendendo o funcionamento


Achei essa figura e resolvi pegar emprestado. Esta estava no contexto do RubyCas, mas no nosso caso, o funcionamento é exatamente o mesmo.


Tá em inglês, porque eu não queria mexer na imagem, mas vamos lá.


  1. O usuário se autentica ou tenta acessar uma página de acesso restrito em uma de suas aplicações web
  2. O cliente CAS (no caso, a aplicação web) redireciona o pedido de login para a página de login do seu servidor CAS.
  3. Seu servidor CAS valida usuário e senha em suas bases de dados (diretório LDAP, banco de dados relacional, arquivo texto, etc).
  4. O usuário está agora autenticado em todas as suas aplicações web, e é redirecionado para a página inicialmente requisitada, possuindo agora um service ticket.
  5. O cliente CAS (a aplicação web) valida o service ticket recebido juntamente ao servidor CAS (via protocolo HTTP CAS)
  6. Se a validação do service ticket falhar, o usuário é redirecionado para a página de login, caso contrário, é autorizado a continuar.

O processo de validar o service ticket com o servidor CAS utiliza protocolo SSL, logo, você precisará configurar o SSL no seu servidor (eu estou usando o tomcat 7). Além disso, também é importante importar o certificado no JDK, pois a validação do service ticket também valida a cadeia de certificados confiáveis.


Configurando o Tomcat 7 com SSL


Crie uma keystore usando o keytool da JRE.
keytool -genkey -alias tomcat -keypass changeit -keyalg RSA -keystore <caminho_do_keystore>

Informe a senha de armazenamento das chaves (deixei como "changeit" mesmo). A única coisa importante é deixar o Common Name (CN) como o hostname da máquina onde o tomcat está instalado. Se o tomcat está instalado na sua própria máquina, coloque "localhost".

Qual é o seu nome e o seu sobrenome?
  [Unknown]:  localhost

Em seguida, exporte o certificado da keystore recém criada

keytool -export -alias tomcat -keypass changeit -keystore <caminho_do_keystore> -file <caminho_do_certificado>

Depois, importe o certificado no JDK

keytool -import -alias tomcat -file <caminho_do_certificado> -keypass changeit -keystore $JAVA_HOME/jre/lib/security/cacerts

O último passo para a configuração do SSL, é alterar o arquivo servers.xml, que fica dentro da pasta conf do tomcat.

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"
       maxThreads="150" scheme="https" secure="true"
       clientAuth="false" sslProtocol="TLS"
       keystoreFile="<caminho_do_keystore>"
       keystorePass="changeit" 
       />


Instalando o CAS Server


Antes de mais nada, CAS significa Central Authorization Service. Você precisará instalar um servidor CAS em algum local na sua empresa, de modo que as aplicações (os clientes) acessem tal servidor para autenticar os usuários. Você poderá baixar o servidor CAS da JASIG neste link. Utilizei aqui a versão 3.4.7. Como talvez você possa ter visto em outras referências, não há mistérios para instalar o servidor, pois ele é apenas um arquivo .war, que você pode descompactar diretamente no Apache Tomcat (no diretório apropriado, é claro). Para fins de documentação, estou utilizando a versão 7.0.11 do Apache Tomcat.

Por default, o servidor CAS não tem configuração para SSL, de modo que ele pode ser acessado diretamente a partir da url

http://<ip_do_servidor>:<porta_http_do_servidor>/cas-cas-server-webapp-<versao_do_cas>/

Se você já instalou o servidor CAS no Tomcat e já o levantou, verá que uma página semelhante à mostrada a seguir será exibida:

Tela de login do servidor CAS

Apenas entre com o usuário e senha iguais, e uma página de sucesso semelhante a mostrada a seguir será exibida:


Configurando a aplicação web (CAS Client)


Dentre todas as demais dependências que você já configurou na sua aplicação web, adicione as que seguem:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${springframework.version}</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${springframework.version}</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas-client</artifactId>
    <version>${springframework.version}</version>
    <type>jar</type>
    <scope>compile</scope>
</dependency>


<properties>
    ...
    <springframework.version>3.0.5.RELEASE</springframework.version>
    ...
</properties>

Feito isso, altere o seu web.xml, para incluir os filtros e listeners necessários:

<!-- CAS Single Sign Out Listener -->
<listener>
    <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>

...

<!-- CAS Single Sign Out Filter -->
<filter>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- Spring Security Filter -->
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Single Sign Out, como o nome sugere, é um mecanismo para efetuar o logout único em todas as aplicações.

Agora, partiremos para a configuração das opções de segurança. Eu escolhi deixar cada tipo de configuração num arquivo xml do spring separadamente, ou seja, um arquivo para as opções de banco de dados, outro para as opções de segurança, outro para o tratamento de exceções, e assim por diante. Além disso, resolvi criar um .properties para mudar (conforme necessidade) os parâmetros de configuração de forma mais organizada. No arquivo com as opções de segurança, descreverei passo a passo o que eu precisei configurar.

Para usar o tal arquivo .properties, de modo semelhante ao que normalmente fazemos para as configurações de acesso a banco de dados, incluí o tal .properties no PropertyPlaceholderConfigurer, deste modo:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
        <array>
            <value>classpath:/jdbc.properties</value>
            <value>classpath:/singleSignOn.properties</value>
        </array>
    </property>
</bean>

O conteúdo do meu arquivo singleSignOn.properties é descrito como abaixo:
webapp.context=aplicacao
webapp.location=http://localhost:8080/${webapp.context}
webapp.location.ssl=https://localhost:8443/${webapp.context}

cas.server.location=http://localhost:8080/cas-server-webapp-3.4.7
cas.server.location.ssl=https://localhost:8443/cas-server-webapp-3.4.7

service.url=${webapp.location.ssl}/j_spring_cas_security_check
logout.success.url=${cas.server.location.ssl}/logout
login.url=${cas.server.location.ssl}/login
proxy.callback.url=${webapp.location.ssl}/secure/receptor
cas.server.url.prefix=${cas.server.location}

Você precisará de um bean ServiceProperties para o contexto da sua aplicação. Isso representará o seu serviço CAS.

<beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
    <beans:property name="service" value="${service.url}" />
    <beans:property name="sendRenew" value="false" />
</beans:bean>

A propriedade service precisa ser igual à url que será monitorada pelo bean CasAuthenticationFilter. Aqui ela é setada com a propriedade service.url do arquivo singleSignOn.properties definido anteriormente. Caso deseje alterar esta url, você precisará mudar a url monitorada pelo CasAuthenticationFilter através da propriedade filterProcessesUrl (preferi não alterar isso).

Os beans a seguir devem ser configurados para que se dê início ao processo de autenticação via CAS.

<http entry-point-ref="casAuthenticationEntryPoint" use-expressions="true" >
    <intercept-url pattern="/javax.faces.resource/**" filters="none" />
    <intercept-url pattern="/resources/**" filters="none" />
    <intercept-url pattern="/**" access="hasRole('ROLE_USER')" />
    <intercept-url pattern="/secure/**" access="hasRole('ROLE_ADMIN')" requires-channel="https" />
    <logout logout-success-url="${logout.success.url}" />
    <custom-filter ref="casAuthenticationFilter" after="CAS_FILTER"/>
</http>

Algumas considerações importantes devem ser feitas aqui. Configuramos o acesso aos recursos de layout (como os css's, imagens e javascripts do jsf) utilizando um <security:intercept-url/>, de modo que não seja necessário permissão de acesso para eles. Todas as demais telas do sistema são acessíveis através da permissão "ROLE_USER". Entretanto, alteramos o fluxo do formulário de login (comumente configurado utilizando a tag <security:form-login/>) para utilizarmos um filtro customizável (<security:custom-filter/>) de modo que a autenticação seja feita redirecionando-se o processamento para o cliente CAS.

<beans:bean id="casAuthenticationFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
    <beans:property name="authenticationManager" ref="authenticationManager" />
    <beans:property name="authenticationFailureHandler">
        <beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
            <beans:property name="defaultFailureUrl" value="/casFailed.jsf" />
        </beans:bean>
    </beans:property>
    <beans:property name="authenticationSuccessHandler">
        <beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler">
            <beans:property name="defaultTargetUrl" value="/" />
        </beans:bean>
    </beans:property>
    <beans:property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
    <beans:property name="proxyReceptorUrl" value="/secure/receptor" />
</beans:bean>

<beans:bean id="casAuthenticationEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
    <beans:property name="loginUrl" value="${login.url}" />
    <beans:property name="serviceProperties" ref="serviceProperties" />
</beans:bean>

O CasAuthenticationEntryPoint deve ser selecionado (através do atributo entry-point-ref para conduzir o processo de autenticação. Ele também precisa referenciar o bean serviceProperties, que provê a URL para o servidor de login CAS da empresa. É para este link que o browser do usuário será redirecionado durante o processo de login.

O CasAuthenticationFilter tem propriedades muito similares ao UsernamePasswordAuthenticationFilter, que é utilizado para autenticação baseada em formulários. Note que há duas propriedades relacionadas à validação do ticket recebido durante o processo de login junto ao servidor CAS: o proxyGrantingTicketStorage e o proxyReceptorUrl. O proxyGrantingTicketStorage se refere ao local onde armazenar o ticket depois de recebido, e o proxyReceptorUrl é o local pra onde o servidor CAS manda o service ticket depois de autenticar o usuário.

O próximo passo agora é adicionar um bean CasAuthenticationProvider e seus colaboradores:

<authentication-manager alias="authenticationManager">
    <authentication-provider ref="casAuthenticationProvider" />
</authentication-manager>

<beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
    <beans:property name="userDetailsService" ref="userService" />
    <beans:property name="serviceProperties" ref="serviceProperties" />
    <beans:property name="ticketValidator">
        <beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
            <beans:constructor-arg index="0" value="${cas.server.url.prefix}" />
            <beans:property name="proxyGrantingTicketStorage" ref="proxyGrantingTicketStorage" />
            <beans:property name="proxyCallbackUrl" value="${proxy.callback.url}" />
        </beans:bean>
    </beans:property>
    <beans:property name="key" value="an_id_for_this_auth_provider_only"/>
</beans:bean>

<beans:bean id="proxyGrantingTicketStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl" />

O CasAuthenticationProvider utiliza uma instância de UserDetailsService para carregar os perfis de acesso para os usuários, após terem sido autenticados pelo CAS. A configuração feita para o UserDetailsService aqui representa um conjunto de usuários armazenados em memória.

Notem o bean ticketValidator. É ele quem é chamado para validar o service ticket depois de recebido. Novamente aqui temos o proxyGrantingTicketStorage, lugar onde ficarão armazenados os service ticket recebidos. O proxyCallbackUrl é a url utilizada pela aplicação web para validar o service ticket no servidor CAS.

Testando


Criei uma segunda aplicação com exatamente a mesma configuração, alterando apenas o arquivo singleSignOn.properties, fiz o deploy das duas no mesmo servidor tomcat, devidamente configurado com SSL, conforme vimos aqui. Ao logar em uma, fui redirecionado para a página de login do servidor CAS, como esperado. Fiz o login, e fui redirecionado para a página inicial da minha aplicação. Ao tentar acessar a segunda aplicação, não fui redirecionado para a tal página de login do servidor CAS.



Em alguns exemplos, o parametro logout.sucess.url, ao invés da url de logout do servidor CAS, é configurado para uma página dentro do próprio sistema, com a opção de logout de todas as aplicações, ou apenas da aplicação em questão. Tal configuração é feita passando-se um parametro chamado service à url de logout do servidor CAS, informando a aplicação que se deseja deslogar. Funciona de modo análogo ao primeiro login efetuado (note que há o mesmo parametro service na url de login do servidor CAS).

Outra menção a se fazer é: porque o google tem diferentes páginas de login pra os diferentes serviços? Podemos imitar essa funcionalidade se alterarmos a página de login do servidor CAS com base nos parâmetros que lhe são passados (no caso, o parâmetro service).


Fonte:
Spring Security 3.0.5-RELEASE Documentation
Spring Security 3.1.x Documentation
JASIG CAS Server Deployment

Creative Commons License
Esta obra está licenciada sob uma Licença Creative Commons.
Comentários
11 Comentários

11 comments:

Weles disse...

Olá Meu amigo!

Excelente artigo!
Estou tentando implementá-lo, mas o java está reclamando o do bean adicionado na tag casAuthenticationProvider:


Geralmente neste bean estão contidos os usuários e senhas,
e em outros casos utilizamos o jdbcUserService para buscar
do banco.

Qdo tento acrescentar:




Ele diz que não é permitido.

Será que faltou vc acrescentar este detalhe, ou o que posso fazer para corrigir o problema?

Obrigado

OrionPax disse...

excelente articulo muy bien explicado, sin embatgo tengo una duda, todo funciona muy bien cuando trabajo con localhost, pero no asi cuendo esta en un servidor remoto es decir cuendo accedo desde otra maquina mediante el ip del equipo no hace logout de manera correcta, tiene algun ejemplo de una configuracion en un entorno de produccion? si es asi seria muy amable de decir como seria esta. gracias

Leandro Soriano Ferreira disse...

@Weles,

Cara, eu nunca fiz esse teste... Como nunca mais eu mexi nessas coisas eu nem tenho como refazer os testes e lhe dar uma resposta nesse momento. Mas eu lembro que tinha como pegar o xml de configuracao do spring e colocar nele próprio a SQL que traz os dados dos usuários

Leandro Soriano Ferreira disse...

@OrionPax,

Tente alterar o arquivo .properties onde há o webapp.location, e ao invés de por localhost, ponha o IP do servidor onde sua aplicação está implantada. Creio que assim há de funcionar

OrionPax disse...

muchas gracias por lo que me dice, hice eso mismo sin embargo todo ok hsta que hago logout es como si no su hibiese destruido la sesion del usuario si bien parece que el ticket si lo fue un problema gracias y a seguir probando

Jossemar Ávila de Morais disse...

Parabéns, ótimo post!!

Me ajudou muito!

Alan Lanzoni disse...

Opa, blza? Estou seguindo o tuto.... mto bom valeu.... mas tenho algumas dúvidas...
Qndo acesso o cas-webapp recebo a seguinte msg:
Non-secure Connection
You are currently accessing CAS over a non-secure connection. Single Sign On WILL NOT WORK. In order to have single sign on work, you MUST log in over HTTPS.

Será q errei algum passo?

É possível fazer autorização na aplicação?

Estou tentando fazer um login centralizado, para acessar todas as aplicações.... terei que fazer estas configs do spring security em todas elas?

Leandro Soriano Ferreira disse...

Olá, Alan... Já faz um bom tempo que eu nao mexo nisso... Não me lembro se é uma obrigatoriedade do CAS usar conexão HTTPS ou não...

Alan Lanzoni disse...

Ok...
Descobri.... Vou postar a solução caso alguem precise....
Por padrão o CAS usa HTTP pra demos
Para utilizar HTTPS tem que alterar o nome do usuário e senhado do deployerConfigContext.xml na webapp

Bruno Oliveira de Alcântara disse...

Muito bom o artigo Leandro. Estou tentando implementar a minha própria gerencia em um cenário de N aplicações em um Tomcat que compartilham sessão, e seu post vai ajudar muito.

Abraço,

Nalomy Souza disse...

teria como compartilhar o codigo do exemplo? queria entender como fez para pegar usuario e senha com o provedor de autenticação

Postar um comentário

Regras são chatas, mas...

- Seu comentário precisa ter relação com o assunto do post;
- Em hipótese alguma faça propaganda de outros blogs ou sites;
- Não inclua links desnecessários no conteúdo do seu comentário;
- Se quiser deixar sua URL, comente usando a opção OpenID;
- CAIXA ALTA, miguxês ou erros de ortografia não serão tolerados;
- Ofensas pessoais, ameaças e xingamentos não são permitidos;