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-2015 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.el.Bindings.bindings; 024import static org.forgerock.openig.http.Responses.blockingCall; 025import static org.forgerock.openig.http.Responses.newInternalServerError; 026import static org.forgerock.openig.util.JsonValues.asExpression; 027import static org.forgerock.util.Utils.closeSilently; 028import static org.forgerock.util.promise.Promises.newResultPromise; 029 030import java.nio.charset.Charset; 031import java.util.Arrays; 032 033import org.forgerock.http.Filter; 034import org.forgerock.http.Handler; 035import org.forgerock.http.protocol.Request; 036import org.forgerock.http.protocol.Response; 037import org.forgerock.http.protocol.Status; 038import org.forgerock.http.session.Session; 039import org.forgerock.http.session.SessionContext; 040import org.forgerock.http.util.CaseInsensitiveSet; 041import org.forgerock.openig.el.Bindings; 042import org.forgerock.openig.el.Expression; 043import org.forgerock.openig.heap.GenericHeapObject; 044import org.forgerock.openig.heap.GenericHeaplet; 045import org.forgerock.openig.heap.HeapException; 046import org.forgerock.services.context.Context; 047import org.forgerock.util.encode.Base64; 048import org.forgerock.util.promise.NeverThrowsException; 049import org.forgerock.util.promise.Promise; 050 051/** 052 * Performs authentication through the HTTP Basic authentication scheme. For more information, 053 * see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>. 054 * <p> 055 * If challenged for authentication via a {@code 401 Unauthorized} status code by the server, 056 * this filter will retry the request with credentials attached. Therefore, the request entity 057 * will be branched and stored for the duration of the processing. 058 * <p> 059 * Once an HTTP authentication challenge (status code 401) is issued from the remote server, 060 * all subsequent requests to that remote server that pass through the filter will include the 061 * user credentials. 062 * <p> 063 * Credentials are cached in the session to allow subsequent requests to automatically include 064 * authentication credentials. If authentication fails (including the case of no credentials 065 * yielded from the {@code username} or {@code password} expressions, then the processing is diverted 066 * to the authentication failure handler. 067 */ 068public class HttpBasicAuthFilter extends GenericHeapObject implements Filter { 069 070 /** Headers that are suppressed from incoming request. */ 071 private static final CaseInsensitiveSet SUPPRESS_REQUEST_HEADERS = 072 new CaseInsensitiveSet(Arrays.asList("Authorization")); 073 074 /** Headers that are suppressed for outgoing response. */ 075 private static final CaseInsensitiveSet SUPPRESS_RESPONSE_HEADERS = 076 new CaseInsensitiveSet(Arrays.asList("WWW-Authenticate")); 077 078 /** Expression that yields the username to supply during authentication. */ 079 private final Expression<String> username; 080 081 /** Expression that yields the password to supply during authentication. */ 082 private final Expression<String> password; 083 084 /** Handler dispatch to if authentication fails. */ 085 private final Handler failureHandler; 086 087 /** Decide if we cache the password header result. */ 088 private boolean cacheHeader = true; 089 090 /** 091 * Builds a {@code HttpBasicAuthFilter} with required expressions and error handler. 092 * @param username the expression that yields the username to supply during authentication. 093 * @param password the expression that yields the password to supply during authentication. 094 * @param failureHandler the Handler to dispatch to if authentication fails. 095 */ 096 public HttpBasicAuthFilter(final Expression<String> username, 097 final Expression<String> password, 098 final Handler failureHandler) { 099 this.username = username; 100 this.password = password; 101 this.failureHandler = failureHandler; 102 } 103 104 /** 105 * Decide if we cache the password header result (defaults to {@literal true}). 106 * @param cacheHeader cache (or not) the {@literal Authorization} header 107 */ 108 public void setCacheHeader(final boolean cacheHeader) { 109 this.cacheHeader = cacheHeader; 110 } 111 112 /** 113 * Resolves a session attribute name for the remote server specified in the specified 114 * request. 115 * 116 * @param request the request of the attribute to resolve. 117 * @return the session attribute name, fully qualified the request remote server. 118 */ 119 private String attributeName(Request request) { 120 return getClass().getName() + ':' + request.getUri().getScheme() + ':' 121 + request.getUri().getHost() + ':' + request.getUri().getPort() + ':' + "userpass"; 122 } 123 124 @Override 125 public Promise<Response, NeverThrowsException> filter(final Context context, 126 final Request request, 127 final Handler next) { 128 129 Session session = context.asContext(SessionContext.class).getSession(); 130 131 // Remove existing headers from incoming message 132 for (String header : SUPPRESS_REQUEST_HEADERS) { 133 request.getHeaders().remove(header); 134 } 135 136 String userpass = null; 137 138 // loop to retry for initially retrieved (or refreshed) credentials 139 try { 140 for (int n = 0; n < 2; n++) { 141 // put a branch of the trunk in the entity to allow retries 142 request.getEntity().push(); 143 Response response; 144 try { 145 // because credentials are sent in every request, this class caches them in the session 146 if (cacheHeader) { 147 userpass = (String) session.get(attributeName(request)); 148 } 149 if (userpass != null) { 150 request.getHeaders().add("Authorization", "Basic " + userpass); 151 } 152 response = blockingCall(next, context, request); 153 } finally { 154 request.getEntity().pop(); 155 } 156 // successful response from this filter's standpoint 157 if (!Status.UNAUTHORIZED.equals(response.getStatus())) { 158 // Remove headers from outgoing message 159 for (String header : SUPPRESS_RESPONSE_HEADERS) { 160 response.getHeaders().remove(header); 161 } 162 return newResultPromise(response); 163 } 164 // close the incoming response because it's about to be dereferenced 165 closeSilently(response); 166 167 // credentials might be stale, so fetch them 168 Bindings bindings = bindings(context, request); 169 String user = username.eval(bindings); 170 String pass = password.eval(bindings); 171 // no credentials is equivalent to invalid credentials 172 if (user == null || pass == null) { 173 break; 174 } 175 // ensure conformance with specification 176 if (user.indexOf(':') >= 0) { 177 return newResultPromise( 178 newInternalServerError("username must not contain a colon ':' character")); 179 } 180 if (cacheHeader) { 181 // set in session for fetch in next iteration of this loop 182 session.put(attributeName(request), 183 Base64.encode((user + ":" + pass).getBytes(Charset.defaultCharset()))); 184 } else { 185 userpass = Base64.encode((user + ":" + pass).getBytes(Charset.defaultCharset())); 186 } 187 } 188 } catch (Exception e) { 189 Response response = new Response().setStatus(Status.FORBIDDEN) 190 .setEntity("Can't authenticate user with Basic Http Authorization"); 191 return newResultPromise(response); 192 } 193 194 195 // credentials were missing or invalid; let failure handler deal with it 196 return failureHandler.handle(context, request); 197 } 198 199 /** Creates and initializes an HTTP basic authentication filter in a heap environment. */ 200 public static class Heaplet extends GenericHeaplet { 201 @Override 202 public Object create() throws HeapException { 203 Handler failureHandler = 204 heap.resolve(config.get("failureHandler"), Handler.class); 205 206 Expression<String> usernameExpr = asExpression(config.get("username").required(), String.class); 207 Expression<String> passwordExpr = asExpression(config.get("password").required(), String.class); 208 HttpBasicAuthFilter filter = new HttpBasicAuthFilter(usernameExpr, passwordExpr, failureHandler); 209 210 filter.cacheHeader = config.get("cacheHeader").defaultTo(filter.cacheHeader).asBoolean(); 211 212 logger.debug("HttpBasicAuthFilter: cacheHeader set to " + filter.cacheHeader); 213 214 return filter; 215 } 216 } 217}