001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2009 Sun Microsystems Inc. 015 * Portions Copyright 2010–2011 ApexIdentity Inc. 016 * Portions Copyright 2011-2014 ForgeRock AS. 017 */ 018 019// TODO: distinguish between basic and other schemes that use 401 (Digest, OAuth, ...) 020 021package org.forgerock.openig.filter; 022 023import static org.forgerock.openig.util.Json.*; 024import static org.forgerock.util.Utils.*; 025 026import java.io.IOException; 027import java.nio.charset.Charset; 028import java.util.Arrays; 029 030import org.forgerock.openig.el.Expression; 031import org.forgerock.openig.handler.Handler; 032import org.forgerock.openig.handler.HandlerException; 033import org.forgerock.openig.heap.GenericHeaplet; 034import org.forgerock.openig.heap.HeapException; 035import org.forgerock.openig.http.Exchange; 036import org.forgerock.openig.http.Request; 037import org.forgerock.openig.http.Response; 038import org.forgerock.openig.util.CaseInsensitiveSet; 039import org.forgerock.util.encode.Base64; 040 041/** 042 * Performs authentication through the HTTP Basic authentication scheme. For more information, 043 * see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>. 044 * <p> 045 * If challenged for authentication via a {@code 401 Unauthorized} status code by the server, 046 * this filter will retry the request with credentials attached. Therefore, the request entity 047 * will be branched and stored for the duration of the exchange. 048 * <p> 049 * Once an HTTP authentication challenge (status code 401) is issued from the remote server, 050 * all subsequent requests to that remote server that pass through the filter will include the 051 * user credentials. 052 * <p> 053 * Credentials are cached in the session to allow subsequent requests to automatically include 054 * authentication credentials. If authentication fails (including the case of no credentials 055 * yielded from the {@code username} or {@code password} expressions, then the exchange is diverted 056 * to the authentication failure handler. 057 */ 058public class HttpBasicAuthFilter extends GenericFilter { 059 060 /** Headers that are suppressed from incoming request. */ 061 private static final CaseInsensitiveSet SUPPRESS_REQUEST_HEADERS = 062 new CaseInsensitiveSet(Arrays.asList("Authorization")); 063 064 /** Headers that are suppressed for outgoing response. */ 065 private static final CaseInsensitiveSet SUPPRESS_RESPONSE_HEADERS = 066 new CaseInsensitiveSet(Arrays.asList("WWW-Authenticate")); 067 068 /** Expression that yields the username to supply during authentication. */ 069 private final Expression username; 070 071 /** Expression that yields the password to supply during authentication. */ 072 private final Expression password; 073 074 /** Handler dispatch to if authentication fails. */ 075 private final Handler failureHandler; 076 077 /** Decide if we cache the password header result. */ 078 private boolean cacheHeader = true; 079 080 /** 081 * Builds a {@code HttpBasicAuthFilter} with required expressions and error handler. 082 * @param username the expression that yields the username to supply during authentication. 083 * @param password the expression that yields the password to supply during authentication. 084 * @param failureHandler the Handler to dispatch to if authentication fails. 085 */ 086 public HttpBasicAuthFilter(final Expression username, final Expression password, final Handler failureHandler) { 087 this.username = username; 088 this.password = password; 089 this.failureHandler = failureHandler; 090 } 091 092 /** 093 * Decide if we cache the password header result (defaults to {@literal true}). 094 * @param cacheHeader cache (or not) the {@literal Authorization} header 095 */ 096 public void setCacheHeader(final boolean cacheHeader) { 097 this.cacheHeader = cacheHeader; 098 } 099 100 /** 101 * Resolves a session attribute name for the remote server specified in the specified 102 * request. 103 * 104 * @param request the request of the attribute to resolve. 105 * @return the session attribute name, fully qualified the request remote server. 106 */ 107 private String attributeName(Request request) { 108 return this.getClass().getName() + ':' + request.getUri().getScheme() + ':' 109 + request.getUri().getHost() + ':' + request.getUri().getPort() + ':' + "userpass"; 110 } 111 112 @Override 113 public void filter(Exchange exchange, Handler next) throws HandlerException, IOException { 114 115 // Remove existing headers from incoming message 116 for (String header : SUPPRESS_REQUEST_HEADERS) { 117 exchange.request.getHeaders().remove(header); 118 } 119 120 String userpass = null; 121 122 // loop to retry for initially retrieved (or refreshed) credentials 123 for (int n = 0; n < 2; n++) { 124 // put a branch of the trunk in the entity to allow retries 125 exchange.request.getEntity().push(); 126 try { 127 // because credentials are sent in every request, this class caches them in the session 128 if (cacheHeader) { 129 userpass = (String) exchange.session.get(attributeName(exchange.request)); 130 } 131 if (userpass != null) { 132 exchange.request.getHeaders().add("Authorization", "Basic " + userpass); 133 } 134 next.handle(exchange); 135 } finally { 136 exchange.request.getEntity().pop(); 137 } 138 // successful exchange from this filter's standpoint 139 if (exchange.response.getStatus() != 401) { 140 // Remove headers from outgoing message 141 for (String header : SUPPRESS_RESPONSE_HEADERS) { 142 exchange.response.getHeaders().remove(header); 143 } 144 return; 145 } 146 // credentials might be stale, so fetch them 147 String user = username.eval(exchange, String.class); 148 String pass = password.eval(exchange, String.class); 149 // no credentials is equivalent to invalid credentials 150 if (user == null || pass == null) { 151 break; 152 } 153 // ensure conformance with specification 154 if (user.indexOf(':') >= 0) { 155 throw new HandlerException("username must not contain a colon ':' character"); 156 } 157 if (cacheHeader) { 158 // set in session for fetch in next iteration of this loop 159 exchange.session.put(attributeName(exchange.request), 160 Base64.encode((user + ":" + pass).getBytes(Charset.defaultCharset()))); 161 } else { 162 userpass = Base64.encode((user + ":" + pass).getBytes(Charset.defaultCharset())); 163 } 164 } 165 // close the incoming response because it's about to be dereferenced 166 closeSilently(exchange.response); 167 168 // credentials were missing or invalid; let failure handler deal with it 169 exchange.response = new Response(); 170 failureHandler.handle(exchange); 171 } 172 173 /** Creates and initializes an HTTP basic authentication filter in a heap environment. */ 174 public static class Heaplet extends GenericHeaplet { 175 @Override 176 public Object create() throws HeapException { 177 Handler failureHandler = 178 heap.resolve(config.get("failureHandler"), Handler.class); 179 180 HttpBasicAuthFilter filter = new HttpBasicAuthFilter(asExpression(config.get("username").required()), 181 asExpression(config.get("password").required()), 182 failureHandler); 183 184 filter.cacheHeader = config.get("cacheHeader").defaultTo(filter.cacheHeader).asBoolean(); 185 186 logger.debug("HttpBasicAuthFilter: cacheHeader set to " + filter.cacheHeader); 187 188 return filter; 189 } 190 } 191}