/******************************************************************************* * This file is part of OpenNMS(R). * * Copyright (C) 2010-2011 The OpenNMS Group, Inc. * OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc. * * OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc. * * OpenNMS(R) is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * OpenNMS(R) is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenNMS(R). If not, see: * http://www.gnu.org/licenses/ * * For more information contact: * OpenNMS(R) Licensing <license@opennms.org> * http://www.opennms.org/ * http://www.opennms.com/ *******************************************************************************/ package org.opennms.protocols.radius.springsecurity; import java.io.IOException; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import net.jradius.client.RadiusClient; import net.jradius.client.auth.PAPAuthenticator; import net.jradius.client.auth.RadiusAuthenticator; import net.jradius.dictionary.Attr_UserName; import net.jradius.dictionary.Attr_UserPassword; import net.jradius.exception.RadiusException; import net.jradius.packet.AccessAccept; import net.jradius.packet.AccessRequest; import net.jradius.packet.RadiusPacket; import net.jradius.packet.attribute.AttributeFactory; import net.jradius.packet.attribute.AttributeList; import net.jradius.packet.attribute.RadiusAttribute; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.opennms.core.utils.InetAddressUtils; import org.opennms.web.springframework.security.Authentication; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * An org.springframework.security.providers.AuthenticationProvider implementation that provides integration with a Radius server. * * @author Paul Donohue */ public class RadiusAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private static final Log logger = LogFactory.getLog(RadiusAuthenticationProvider.class); private String server, secret; private int port = 1812, timeout = 5, retries = 3; /** * There is a bug in {@link net.jradius.client.auth.PAPAuthenticator#processRequest(RadiusPacket)} that prevents * instances of the class from being reused. Set the authenticator class to null to work * around this problem (while still using the {@link net.jradius.client.auth.PAPAuthenticator} * class). * * @see net.jradius.client.RadiusClient#authenticate(AccessRequest, RadiusAuthenticator, int) */ private RadiusAuthenticator authTypeClass = null; private String defaultRoles = Authentication.ROLE_USER, rolesAttribute; /** * Create an instance using the supplied server and shared secret. * * @param server a {@link java.lang.String} object. * @param sharedSecret a {@link java.lang.String} object. */ public RadiusAuthenticationProvider(String server, String sharedSecret) { Assert.hasLength(server, "A server must be specified"); this.server = server; Assert.hasLength(sharedSecret, "A shared secret must be specified"); this.secret = sharedSecret; } /** * <p>doAfterPropertiesSet</p> * * @throws java.lang.Exception if any. */ @Override protected void doAfterPropertiesSet() throws Exception { Assert.notNull(this.port, "A port number must be specified"); Assert.notNull(this.timeout, "A timeout must be specified"); Assert.notNull(this.retries, "A retry count must be specified"); //Assert.notNull(this.authTypeClass, "A RadiusAuthenticator object must be supplied in authTypeClass"); Assert.notNull(this.defaultRoles, "Default Roles must be supplied in defaultRoles"); } /** * Sets the port number the radius server is listening on * * @param port (defaults to 1812) */ public void setPort(int port) { this.port = port; } /** * Sets the authentication timeout (in seconds) * * @param timeout (defaults to 5) */ public void setTimeout(int timeout) { this.timeout = timeout; } /** * Sets the number of times to retry a timed-out authentication request * * @param retries (defaults to 3) */ public void setRetries(int retries) { this.retries = retries; } /** * Sets the authenticator, which determines the authentication type (PAP, CHAP, etc) * * @param authTypeClass An instance of net.jradius.client.auth.RadiusAuthenticator (defaults to PAPAuthenticator) */ public void setAuthTypeClass(RadiusAuthenticator authTypeClass) { if (authTypeClass instanceof PAPAuthenticator) { // There is a bug in PAPAuthenticator() that prevents instances of the class // from being reused. Set the authenticator class to null to work around this // problem. this.authTypeClass = null; } else { this.authTypeClass = authTypeClass; } } /** * Sets the default authorities (roles) that should be assigned to authenticated users * * @param defaultRoles comma-separated list of roles (defaults to "ROLE_USER") */ public void setDefaultRoles(String defaultRoles) { this.defaultRoles = defaultRoles; } /** * Sets the name of a radius attribute to be returned by the radius server * with a comma-separated list of authorities (roles) to be assigned to the user * * If this is not set, or if the specified attribute is not found in the reply * from the radius server, defaultRoles will be used to assign roles * * If JRadius's built-in attribute dictionary does not contain the desired * attribute name, use "Unknown-VSAttribute(<Vendor ID>:<Attribute Number>)" * * @param rolesAttribute a {@link java.lang.String} object. */ public void setRolesAttribute(String rolesAttribute) { this.rolesAttribute = rolesAttribute; } /* (non-Javadoc) * @see org.springframework.security.providers.dao.AbstractUserDetailsAuthenticationProvider#additionalAuthenticationChecks(org.springframework.security.userdetails.UserDetails, org.springframework.security.providers.UsernamePasswordAuthenticationToken) */ /** {@inheritDoc} */ @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken token) throws AuthenticationException { if (!userDetails.getPassword().equals(token.getCredentials().toString())) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"), userDetails); } } /* (non-Javadoc) * @see org.springframework.security.providers.dao.AbstractUserDetailsAuthenticationProvider#retrieveUser(java.lang.String, org.springframework.security.providers.UsernamePasswordAuthenticationToken) */ /** {@inheritDoc} */ @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken token) throws AuthenticationException { if (!StringUtils.hasLength(username)) { logger.info("Authentication attempted with empty username"); throw new BadCredentialsException(messages.getMessage("RadiusAuthenticationProvider.emptyUsername", "Username cannot be empty")); } String password = (String) token.getCredentials(); if (!StringUtils.hasLength(password)) { logger.info("Authentication attempted with empty password"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } InetAddress serverIP = null; serverIP = InetAddressUtils.addr(server); if (serverIP == null) { logger.error("Could not resolve radius server address "+server); throw new AuthenticationServiceException(messages.getMessage("RadiusAuthenticationProvider.unknownServer", "Could not resolve radius server address")); } AttributeFactory.loadAttributeDictionary("net.jradius.dictionary.AttributeDictionaryImpl"); AttributeList attributeList = new AttributeList(); attributeList.add(new Attr_UserName(username)); attributeList.add(new Attr_UserPassword(password)); RadiusPacket reply; try { RadiusClient radiusClient = new RadiusClient(serverIP, secret, port, port+1, timeout); AccessRequest request = new AccessRequest(radiusClient, attributeList); logger.debug("Sending AccessRequest message to "+InetAddressUtils.str(serverIP)+":"+port+" using "+(authTypeClass == null ? "PAP" : authTypeClass.getAuthName())+" protocol with timeout = "+timeout+", retries = "+retries+", attributes:\n"+attributeList.toString()); reply = radiusClient.authenticate(request, authTypeClass, retries); } catch (RadiusException e) { logger.error("Error connecting to radius server "+server+" : "+e); throw new AuthenticationServiceException(messages.getMessage("RadiusAuthenticationProvider.radiusError", new Object[] {e}, "Error connecting to radius server: "+e)); } catch (IOException e) { logger.error("Error connecting to radius server "+server+" : "+e); throw new AuthenticationServiceException(messages.getMessage("RadiusAuthenticationProvider.radiusError", new Object[] {e}, "Error connecting to radius server: "+e)); } if (reply == null) { logger.error("Timed out connecting to radius server "+server); throw new AuthenticationServiceException(messages.getMessage("RadiusAuthenticationProvider.radiusTimeout", "Timed out connecting to radius server")); } if (!(reply instanceof AccessAccept)) { logger.info("Received a reply other than AccessAccept from radius server "+server+" for user "+username+" :\n"+reply.toString()); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } logger.debug("Received AccessAccept message from "+InetAddressUtils.str(serverIP)+":"+port+" for user "+username+" with attributes:\n"+reply.getAttributes().toString()); String roles = null; if (!StringUtils.hasLength(rolesAttribute)) { logger.debug("rolesAttribute not set, using default roles ("+defaultRoles+") for user "+username); roles = new String(defaultRoles); } else { Iterator<RadiusAttribute> attributes = reply.getAttributes().getAttributeList().iterator(); while (attributes.hasNext()) { RadiusAttribute attribute = attributes.next(); if (rolesAttribute.equals(attribute.getAttributeName())) { roles = new String(attribute.getValue().getBytes()); break; } } if (roles == null) { logger.info("Radius attribute "+rolesAttribute+" not found, using default roles ("+defaultRoles+") for user "+username); roles = new String(defaultRoles); } } String[] rolesArray = roles.replaceAll("\\s*","").split(","); Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(rolesArray.length); for (String role : rolesArray) { authorities.add(new SimpleGrantedAuthority(role)); } if(logger.isDebugEnabled()) { StringBuffer readRoles = new StringBuffer(); for (GrantedAuthority authority : authorities) { readRoles.append(authority.toString()+", "); } if (readRoles.length() > 0) { readRoles.delete(readRoles.length()-2, readRoles.length()); } logger.debug("Parsed roles "+readRoles+" for user "+username); } return new User(username, password, true, true, true, true, authorities); } }