/* * Copyright 2014 JBoss Inc * * Licensed 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 io.apiman.common.servlet; import io.apiman.common.auth.AuthPrincipal; import io.apiman.common.auth.AuthToken; import io.apiman.common.auth.AuthTokenUtil; import java.io.IOException; import java.security.Principal; import java.util.Collections; import java.util.Set; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.StringUtils; /** * A simple implementation of an authentication filter - uses the APIMan * {@link AuthToken} concept to implement the equivalent of bearer token * authentication. This filter supports both {@link AuthToken}'s as well * as standard BASIC authentication. The latter is implemented by * delegating to the container. * * @author eric.wittmann@redhat.com */ public class AuthenticationFilter implements Filter { private String realm; @SuppressWarnings("unused") private boolean signatureRequired; @SuppressWarnings("unused") private String keystorePath; @SuppressWarnings("unused") private String keystorePassword; @SuppressWarnings("unused") private String keyAlias; @SuppressWarnings("unused") private String keyPassword; /** * Constructor. */ public AuthenticationFilter() { } /** * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) */ @Override public void init(FilterConfig config) throws ServletException { // Realm String parameter = config.getInitParameter("realm"); //$NON-NLS-1$ if (parameter != null && parameter.trim().length() > 0) { realm = parameter; } else { realm = defaultRealm(); } // Signature Required parameter = config.getInitParameter("signatureRequired"); //$NON-NLS-1$ if (parameter != null && parameter.trim().length() > 0) { signatureRequired = Boolean.parseBoolean(parameter); } else { signatureRequired = defaultSignatureRequired(); } // Keystore Path parameter = config.getInitParameter("keystorePath"); //$NON-NLS-1$ if (parameter != null && parameter.trim().length() > 0) { keystorePath = parameter; } else { keystorePath = defaultKeystorePath(); } // Keystore Password parameter = config.getInitParameter("keystorePassword"); //$NON-NLS-1$ if (parameter != null && parameter.trim().length() > 0) { keystorePassword = parameter; } else { keystorePassword = defaultKeystorePassword(); } // Key alias parameter = config.getInitParameter("keyAlias"); //$NON-NLS-1$ if (parameter != null && parameter.trim().length() > 0) { keyAlias = parameter; } else { keyAlias = defaultKeyAlias(); } // Key Password parameter = config.getInitParameter("keyPassword"); //$NON-NLS-1$ if (parameter != null && parameter.trim().length() > 0) { keyPassword = parameter; } else { keyPassword = defaultKeyPassword(); } } /** * @return the default keystore password */ protected String defaultKeystorePassword() { return null; } /** * @return the default key alias */ protected String defaultKeyAlias() { return null; } /** * @return the default key password */ protected String defaultKeyPassword() { return null; } /** * @return the default value of keystorePath */ protected String defaultKeystorePath() { return null; } /** * @return the default value of signatureRequired */ protected boolean defaultSignatureRequired() { return false; } /** * @return the default value of wrapRequest */ protected boolean defaultWrapRequest() { return false; } /** * @return the default set of allowed issuers */ protected Set<String> defaultAllowedIssuers() { return Collections.<String>emptySet(); } /** * @return the default realm */ protected String defaultRealm() { return "apiman"; //$NON-NLS-1$ } /** * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) */ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String authHeader = req.getHeader("Authorization"); //$NON-NLS-1$ if (authHeader == null) { sendAuthResponse((HttpServletResponse) response); } else if (authHeader.toUpperCase().startsWith("BASIC")) { //$NON-NLS-1$ Creds credentials = parseAuthorizationBasic(authHeader); if (credentials == null) { sendAuthResponse((HttpServletResponse) response); } else { doBasicAuth(credentials, req, (HttpServletResponse) response, chain); } } else if (authHeader.toUpperCase().startsWith("AUTH-TOKEN")) { //$NON-NLS-1$ AuthToken token = parseAuthorizationToken(authHeader); if (token == null) { sendAuthResponse((HttpServletResponse) response); } else { doTokenAuth(token, req, (HttpServletResponse) response, chain); } } else { sendAuthResponse((HttpServletResponse) response); } } /** * Handle BASIC authentication. Delegates this to the container by invoking 'login' * on the inbound http servlet request object. * @param credentials the credentials * @param request the http servlet request * @param response the http servlet respose * @param chain the filter chain * @throws IOException when I/O failure occurs in filter chain * @throws ServletException when servlet exception occurs during auth */ protected void doBasicAuth(Creds credentials, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { if (credentials.username.equals(request.getRemoteUser())) { // Already logged in as this user - do nothing. This can happen // in some app servers if the app server processes the BASIC auth // credentials before this filter gets a crack at them. WildFly 8 // works this way, for example (despite the web.xml not specifying // any login config!). } else if (request.getRemoteUser() != null) { // switch user request.logout(); request.login(credentials.username, credentials.password); } else { request.login(credentials.username, credentials.password); } } catch (Exception e) { // TODO log this error? e.printStackTrace(); sendAuthResponse(response); return; } doFilterChain(request, response, chain, null); } /** * Implements token based authentication. This simply creates a principal from the {@link AuthToken} * and then calls doFilterChain. * @param token the token * @param request the request * @param response the response * @param chain the filterchain * @throws IOException when I/O failure occurs in filter chain * @throws ServletException when servlet exception occurs during auth */ protected void doTokenAuth(AuthToken token, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { AuthPrincipal principal = new AuthPrincipal(token.getPrincipal()); principal.addRoles(token.getRoles()); doFilterChain(request, response, chain, principal); } /** * Further process the filter chain. * @param request the request * @param response the response * @param chain the filter chain * @param principal the auth principal * @throws IOException when I/O failure occurs in filter chain * @throws ServletException when servlet exception occurs during auth */ protected void doFilterChain(ServletRequest request, ServletResponse response, FilterChain chain, AuthPrincipal principal) throws IOException, ServletException { if (principal == null) { chain.doFilter(request, response); } else { HttpServletRequest hsr; hsr = wrapTheRequest(request, principal); chain.doFilter(hsr, response); } } /** * Wrap the request to provide the principal. * @param request the request * @param principal the principal */ private HttpServletRequest wrapTheRequest(final ServletRequest request, final AuthPrincipal principal) { HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper((HttpServletRequest) request) { @Override public Principal getUserPrincipal() { return principal; } @Override public boolean isUserInRole(String role) { return principal.getRoles().contains(role); } @Override public String getRemoteUser() { return principal.getName(); } }; return wrapper; } /** * Parses the Authorization request header into a username and password. * @param authHeader the auth header */ private Creds parseAuthorizationBasic(String authHeader) { String userpassEncoded = authHeader.substring(6); String data = StringUtils.newStringUtf8(Base64.decodeBase64(userpassEncoded)); int sepIdx = data.indexOf(':'); if (sepIdx > 0) { String username = data.substring(0, sepIdx); String password = data.substring(sepIdx + 1); return new Creds(username, password); } else { return new Creds(data, null); } } /** * Parses the Authorization request to retrieve the Base64 encoded auth token. * @param authHeader the auth header */ private AuthToken parseAuthorizationToken(String authHeader) { try { String tokenEncoded = authHeader.substring(11); return AuthTokenUtil.consumeToken(tokenEncoded); } catch (IllegalArgumentException e) { // TODO log this error return null; } } /** * Sends a response that tells the client that authentication is required. * @param response the response * @throws IOException when an error cannot be sent */ private void sendAuthResponse(HttpServletResponse response) throws IOException { response.setHeader("WWW-Authenticate", String.format("Basic realm=\"%1$s\"", realm)); //$NON-NLS-1$ //$NON-NLS-2$ response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } /** * @see javax.servlet.Filter#destroy() */ @Override public void destroy() { } /** * Models inbound basic auth credentials (user/password). * @author eric.wittmann@redhat.com */ protected static class Creds { public String username; public String password; /** * Constructor. * * @param username the username * @param password the password */ public Creds(String username, String password) { this.username = username; this.password = password; } } }