All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mongodb.jdbc.oidc.RFC8252HttpServer Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
/*
 * Copyright 2024-present MongoDB, 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 com.mongodb.jdbc.oidc;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

/**
 * The RFC8252HttpServer class implements an OIDC (OpenID Connect) server based on RFC 8252. It
 * handles the OIDC authorization code flow by providing endpoints for the callback and redirection.
 * The server listens on a specified port (default is 27017) and processes incoming HTTP requests.
 */
public class RFC8252HttpServer {
    public static final int DEFAULT_REDIRECT_PORT = 27097;

    // SQL-2008: make sure this page exists and possibly update the link if the
    // docs team has a preference
    private static final String LOGIN_ERROR_URI =
            "https://www.mongodb.com/docs/atlas/security-oidc";
    private static final String PRODUCT_DOCS_LINK =
            "https://www.mongodb.com/docs/atlas/data-federation/query/sql/drivers/odbc/connect";
    private static final String PRODUCT_DOCS_NAME = "Atlas SQL ODBC Driver";

    // OIDC response parameters
    private static final String CODE = "code";
    private static final String LOCATION = "Location";
    private static final String STATE = "state";

    // template variables
    private static final String PRODUCT_DOCS_LINK_KEY = "product_docs_link";
    private static final String PRODUCT_DOCS_NAME_KEY = "product_docs_name";
    private static final String ERROR_URI_KEY = "error_uri";
    private static final String ERROR_KEY = "error";
    private static final String ERROR_DESCRIPTION_KEY = "error_description";

    // server endpoints
    private static final String ACCEPTED_ENDPOINT = "/accepted";
    private static final String CALLBACK_ENDPOINT = "/callback";
    private static final String REDIRECT_ENDPOINT = "/redirect";

    private HttpServer server;
    private final TemplateEngine templateEngine;
    private final BlockingQueue oidcResponseQueue;

    public RFC8252HttpServer() {
        templateEngine = createTemplateEngine();
        oidcResponseQueue = new LinkedBlockingQueue<>();
    }

    /**
     * Starts the HTTP server and sets up the necessary contexts and handlers.
     *
     * @throws IOException if an I/O error occurs while creating or starting the server
     */
    public void start() throws IOException {
        server = HttpServer.create(new InetSocketAddress(DEFAULT_REDIRECT_PORT), 0);

        server.createContext(CALLBACK_ENDPOINT, new CallbackHandler());
        server.createContext(REDIRECT_ENDPOINT, new CallbackHandler());
        server.createContext(ACCEPTED_ENDPOINT, new AcceptedHandler());
        server.setExecutor(null);
        server.start();
    }

    /**
     * Attempts to retrieve an OIDC response from the queue, waiting up to a default timeout of 300
     * seconds.
     *
     * @return the OIDC response, if available within the default timeout period
     * @throws InterruptedException if no response is available within the default timeout period
     */
    public OidcResponse getOidcResponse() throws InterruptedException, OidcTimeoutException {
        return getOidcResponse(Duration.ofSeconds(300));
    }

    /**
     * Attempts to retrieve an OIDC response from the queue, waiting up to the specified timeout. If
     * no response is available within the timeout period, an InterruptedException is thrown.
     *
     * @param timeout the maximum time to wait for an OIDC response, in seconds
     * @return the OIDC response, if available within the timeout period
     * @throws InterruptedException if no response is available within the timeout period or if the
     *     current thread is interrupted while waiting
     */
    public OidcResponse getOidcResponse(Duration timeout)
            throws OidcTimeoutException, InterruptedException {
        if (timeout == null) {
            return getOidcResponse();
        }
        OidcResponse response = oidcResponseQueue.poll(timeout.getSeconds(), TimeUnit.SECONDS);
        if (response == null) {
            throw new OidcTimeoutException("Timeout waiting for OIDC response");
        }
        return response;
    }

    public void stop() {
        if (server != null) {
            server.stop(0);
        }
    }

    /**
     * Creates and configures the template engine.
     *
     * @return the configured template engine
     */
    private TemplateEngine createTemplateEngine() {
        TemplateEngine templateEngine = new TemplateEngine();
        ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
        templateResolver.setPrefix("/templates/");
        templateResolver.setSuffix(".html");
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    /** HTTP handler for handling the callback and redirect endpoints. */
    private class CallbackHandler implements HttpHandler {

        @Override
        public void handle(HttpExchange exchange) throws IOException {
            Map queryParams = parseQueryParams(exchange);
            OidcResponse oidcResponse = new OidcResponse();

            if (queryParams.containsKey(CODE)) {
                oidcResponse.setCode(queryParams.get(CODE));
                oidcResponse.setState(queryParams.getOrDefault(STATE, ""));
                if (!putOidcResponse(exchange, oidcResponse)) {
                    return;
                }
                // This will hide the code and state from the URL bar by doing a redirect
                // to the /accepted page rather than rendering the accepted page directly
                exchange.getResponseHeaders().set(LOCATION, ACCEPTED_ENDPOINT);
                sendResponse(exchange, "", HttpURLConnection.HTTP_MOVED_TEMP);
            } else if (queryParams.containsKey(ERROR_KEY)) {
                oidcResponse.setError(queryParams.get(ERROR_KEY));
                oidcResponse.setErrorDescription(
                        queryParams.getOrDefault(ERROR_DESCRIPTION_KEY, "Unknown error"));
                if (!putOidcResponse(exchange, oidcResponse)) {
                    return;
                }
                Context context = new Context();
                context.setVariable(ERROR_URI_KEY, LOGIN_ERROR_URI);
                context.setVariable(PRODUCT_DOCS_LINK_KEY, PRODUCT_DOCS_LINK);
                context.setVariable(PRODUCT_DOCS_NAME_KEY, PRODUCT_DOCS_NAME);
                context.setVariable(ERROR_KEY, queryParams.get(ERROR_KEY));
                context.setVariable(
                        ERROR_DESCRIPTION_KEY,
                        queryParams.getOrDefault(ERROR_DESCRIPTION_KEY, "Unknown error"));
                String errorHtml = templateEngine.process("OIDCErrorTemplate", context);
                sendResponse(exchange, errorHtml, HttpURLConnection.HTTP_BAD_REQUEST);

            } else {
                oidcResponse.setError("Not found");
                String allParams =
                        queryParams
                                .entrySet()
                                .stream()
                                .map(entry -> entry.getKey() + "=" + entry.getValue())
                                .reduce((param1, param2) -> param1 + ", " + param2)
                                .orElse("No parameters");
                oidcResponse.setErrorDescription("Not found. Parameters: " + allParams);
                if (!putOidcResponse(exchange, oidcResponse)) {
                    return;
                }
                Context context = new Context();
                context.setVariable(PRODUCT_DOCS_LINK_KEY, PRODUCT_DOCS_LINK);
                context.setVariable(PRODUCT_DOCS_NAME_KEY, PRODUCT_DOCS_NAME);
                String notFoundHtml = templateEngine.process("OIDCNotFoundTemplate", context);
                sendResponse(exchange, notFoundHtml, HttpURLConnection.HTTP_NOT_FOUND);
            }
        }
    }

    /** HTTP handler for handling the accepted endpoint. */
    private class AcceptedHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange exchange) throws IOException {
            Context context = new Context();
            context.setVariable(PRODUCT_DOCS_LINK_KEY, PRODUCT_DOCS_LINK);
            context.setVariable(PRODUCT_DOCS_NAME_KEY, PRODUCT_DOCS_NAME);
            String acceptedHtml = templateEngine.process("OIDCAcceptedTemplate", context);
            sendResponse(exchange, acceptedHtml, HttpURLConnection.HTTP_OK);
        }
    }

    /**
     * Parses the query parameters from the HTTP exchange.
     *
     * @param exchange the HTTP exchange
     * @return a map containing the parsed query parameters
     * @throws UnsupportedEncodingException if the encoding is not supported
     */
    private Map parseQueryParams(HttpExchange exchange)
            throws UnsupportedEncodingException {
        Map queryParams = new HashMap<>();
        String rawQuery = exchange.getRequestURI().getRawQuery();

        if (rawQuery != null) {
            String[] params = rawQuery.split("&");
            for (String param : params) {
                int equalsIndex = param.indexOf('=');
                if (equalsIndex > 0) {
                    String key = param.substring(0, equalsIndex);
                    String encodedValue = param.substring(equalsIndex + 1);
                    String value = URLDecoder.decode(encodedValue, "UTF-8");
                    queryParams.put(key, value);
                } else {
                    queryParams.put(param, "");
                }
            }
        }
        return queryParams;
    }

    /**
     * Puts the OIDC response into the blocking queue. If the queue is full, an error response is
     * sent to the client and the HttpExchange is closed.
     *
     * @param exchange the HTTP exchange
     * @param oidcResponse the OIDC response to put into the queue
     * @return true if the response was successfully put into the queue, false otherwise
     * @throws IOException if an I/O error occurs while sending a response
     */
    private boolean putOidcResponse(HttpExchange exchange, OidcResponse oidcResponse)
            throws IOException {
        try {
            oidcResponseQueue.put(oidcResponse);
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            // sendResponse will close the exchange
            sendResponse(exchange, "

Internal Server Error

", 500); return false; } } /** * Sends an HTTP response with the specified content and status code. * * @param exchange the HTTP exchange * @param response the response content * @param statusCode the HTTP status code * @throws IOException if an I/O error occurs while sending the response */ private void sendResponse(HttpExchange exchange, String response, int statusCode) throws IOException { exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); try { exchange.sendResponseHeaders( statusCode, response.getBytes(StandardCharsets.UTF_8).length); try (OutputStream os = exchange.getResponseBody()) { os.write(response.getBytes(StandardCharsets.UTF_8)); } } catch (IOException e) { Logger logger = Logger.getLogger(RFC8252HttpServer.class.getName()); logger.log(Level.SEVERE, "Error sending response", e); throw e; } finally { exchange.close(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy