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}