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

com.networknt.basicauth.BasicAuthHandler Maven / Gradle / Ivy

There is a newer version: 2.1.38
Show newest version
/*
 * Copyright (c) 2016 Network New Technologies 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.networknt.basicauth;

import com.networknt.config.Config;
import com.networknt.handler.Handler;
import com.networknt.handler.MiddlewareHandler;
import com.networknt.ldap.LdapUtil;
import com.networknt.utility.ModuleRegistry;
import com.networknt.utility.StringUtils;
import io.undertow.Handlers;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * This is a middleware handler that handles basic authentication in restful APIs. It is not used
 * in most situations as OAuth 2.0 is the standard. In certain cases for example, the server is
 * deployed to IoT devices, basic authentication can be used to replace OAuth 2.0 handlers.
 * 

* There are multiple users that can be defined in basic.yml config file. Password can be stored in plain or * encrypted format in basic.yml. In case of password encryption, please remember to add corresponding * com.networknt.utility.Decryptor in service.yml. And access is logged into audit.log if audit middleware is used. * * @author Steve Hu */ public class BasicAuthHandler implements MiddlewareHandler { static final Logger logger = LoggerFactory.getLogger(BasicAuthHandler.class); static final String BEARER_PREFIX = "BEARER"; static final String BASIC_PREFIX = "BASIC"; static BasicAuthConfig config; static final String MISSING_AUTH_TOKEN = "ERR10002"; static final String INVALID_BASIC_HEADER = "ERR10046"; static final String INVALID_USERNAME_OR_PASSWORD = "ERR10047"; static final String NOT_AUTHORIZED_REQUEST_PATH = "ERR10071"; static final String INVALID_AUTHORIZATION_HEADER = "ERR12003"; static final String BEARER_USER_NOT_FOUND = "ERR10072"; private volatile HttpHandler next; public BasicAuthHandler() { config = BasicAuthConfig.load(); if (logger.isInfoEnabled()) logger.info("BasicAuthHandler is loaded."); } /** * This is a constructor for test cases only. Please don't use it. * * @param cfg BasicAuthConfig */ @Deprecated public BasicAuthHandler(BasicAuthConfig cfg) { config = cfg; if (logger.isInfoEnabled()) logger.info("BasicAuthHandler is loaded."); } @Override public void handleRequest(final HttpServerExchange exchange) throws Exception { if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest starts."); String auth = exchange.getRequestHeaders().getFirst(Headers.AUTHORIZATION); String requestPath = exchange.getRequestPath(); /* no auth header */ if (auth == null || auth.trim().length() == 0) { boolean b = this.handleAnonymousAuth(exchange, requestPath); // if the return value is false, we need to stop the handler and return immediately. if(!b) return; /* contains auth header */ } else { // verify the header with the config file. assuming it is basic authentication first. if (BASIC_PREFIX.equalsIgnoreCase(auth.substring(0, 5))) { // check if the length is greater than 6 for issue1513 if(auth.trim().length() == 5) { logger.error("Invalid/Unsupported authorization header {}", auth); setExchangeStatus(exchange, INVALID_AUTHORIZATION_HEADER, auth); exchange.endExchange(); return; } else { boolean b = this.handleBasicAuth(exchange, requestPath, auth); // if the return value is false, we need to stop the handler and return immediately. if(!b) return; } } else if (BEARER_PREFIX.equalsIgnoreCase(auth.substring(0, 6))) { boolean b = this.handleBearerToken(exchange, requestPath, auth); if(!b) return; } else { logger.error("Invalid/Unsupported authorization header {}", auth.substring(0, 10)); setExchangeStatus(exchange, INVALID_AUTHORIZATION_HEADER, auth.substring(0, 10)); exchange.endExchange(); return; } } if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends."); Handler.next(exchange, next); } /** * Handle anonymous authentication. * If requests are anonymous and do not have a path entry, we block the request. * * @param exchange - current exchange. * @param requestPath - path for current request. * @return true if there is no error. Otherwise, there is an error and need to return the handler instead of calling the next. */ private boolean handleAnonymousAuth(HttpServerExchange exchange, String requestPath) { if (config.isAllowAnonymous() && config.getUsers().containsKey(BasicAuthConfig.ANONYMOUS)) { List paths = config.getUsers().get(BasicAuthConfig.ANONYMOUS).getPaths(); boolean match = false; for (String path : paths) { if (requestPath.startsWith(path)) { match = true; break; } } if (!match) { logger.error("Request path '{}' is not authorized for user '{}'", requestPath, BasicAuthConfig.ANONYMOUS); // this is to handler the client with pre-emptive authentication with response code 401 exchange.getResponseHeaders().put(Headers.WWW_AUTHENTICATE, "Basic realm=\"Default Realm\""); setExchangeStatus(exchange, NOT_AUTHORIZED_REQUEST_PATH, requestPath, BasicAuthConfig.ANONYMOUS); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } } else { logger.error("Anonymous is not allowed and authorization header is missing."); // this is to handler the client with pre-emptive authentication with response code 401 exchange.getResponseHeaders().put(Headers.WWW_AUTHENTICATE, "Basic realm=\"Basic Auth\""); setExchangeStatus(exchange, MISSING_AUTH_TOKEN); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } return true; } /** * Handle basic authentication header. * If the request coming in has an incorrect format for basic auth, we block the request. * We also block the request if the path is not configured to have basic authentication. * * @param exchange - current exchange. * @param requestPath - path found within current request. * @param auth - auth string * @return boolean to indicate if an error or success. */ public boolean handleBasicAuth(HttpServerExchange exchange, String requestPath, String auth) { String credentials = auth.substring(6); int pos = credentials.indexOf(':'); if (pos == -1) { credentials = new String(org.apache.commons.codec.binary.Base64.decodeBase64(credentials), UTF_8); } pos = credentials.indexOf(':'); if (pos != -1) { String username = credentials.substring(0, pos); String password = credentials.substring(pos + 1); if(logger.isTraceEnabled()) logger.trace("input username = {}, password = {}", username, StringUtils.maskHalfString(password)); UserAuth user = config.getUsers().get(username); // if user cannot be found in the config, return immediately. if (user == null) { logger.error("User '{}' is not found in the configuration file.", username); setExchangeStatus(exchange, INVALID_USERNAME_OR_PASSWORD); exchange.endExchange(); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); return false; } // At this point, we know the user is found in the config file. if (username.equals(user.getUsername()) && StringUtils.isEmpty(user.getPassword()) && config.enableAD) { // Call LdapUtil with LDAP authentication and authorization given user is matched, password is empty, and AD is enabled. if(logger.isTraceEnabled()) logger.trace("Call LdapUtil with LDAP authentication and authorization for user = {}", username); if (!handleLdapAuth(user, password)) { setExchangeStatus(exchange, INVALID_USERNAME_OR_PASSWORD); exchange.endExchange(); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); return false; } } else { if(logger.isTraceEnabled()) logger.trace("Validate basic auth based on config username {} and password {}", user.getUsername(), StringUtils.maskHalfString(user.getPassword())); // if username matches config, password matches config, and path matches config, pass if (!(user.getUsername().equals(username) && password.equals(user.getPassword()))) { logger.error("Invalid username or password with authorization header = {}", StringUtils.maskHalfString(auth)); setExchangeStatus(exchange, INVALID_USERNAME_OR_PASSWORD); exchange.endExchange(); if (logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); return false; } } // Here we have passed the authentication. Let's do the authorization with the paths. if(logger.isTraceEnabled()) logger.trace("Username and password validation is done for user = {}", username); boolean match = false; for (String path : user.getPaths()) { if (requestPath.startsWith(path)) { match = true; break; } } if (!match) { logger.error("Request path '{}' is not authorized for user '{}", requestPath, user.getUsername()); setExchangeStatus(exchange, NOT_AUTHORIZED_REQUEST_PATH, requestPath, user.getUsername()); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } } else { logger.error("Invalid basic authentication header. It must be username:password base64 encode."); setExchangeStatus(exchange, INVALID_BASIC_HEADER, auth.substring(0, 10)); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } return true; } /** * Handle LDAP authentication and authorization * @param user * @return true if Ldap auth success, false if Ldap auth failure */ private static boolean handleLdapAuth(UserAuth user, String password) { boolean isAuthenticated = LdapUtil.authenticate(user.getUsername(), password); if (!isAuthenticated) { logger.error("user '" + user.getUsername() + "' Ldap authentication failed"); return false; } return true; } /** * Handle Bearer token authentication. * We block requests that are not configured to have bearer tokens. * We also block requests that are configured to have a bearer token * * @param exchange - current exchange. * @param requestPath - path for request * @param auth - auth string * @return boolean to indicate if an error or success. */ private boolean handleBearerToken(HttpServerExchange exchange, String requestPath, String auth) { // not basic token. check if the OAuth 2.0 bearer token is allowed. if (!config.allowBearerToken) { logger.error("Not a basic authentication header, and bearer token is not allowed."); setExchangeStatus(exchange, INVALID_BASIC_HEADER, auth.substring(0, 10)); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } else { // bearer token is allowed, we need to validate it and check the allowed paths. UserAuth user = config.getUsers().get(BasicAuthConfig.BEARER); if (user != null) { // check the path for authorization List paths = user.getPaths(); boolean match = false; for (String path : paths) { if (requestPath.startsWith(path)) { match = true; break; } } if (!match) { logger.error("Request path '{}' is not authorized for user '{}' ", requestPath, BasicAuthConfig.BEARER); setExchangeStatus(exchange, NOT_AUTHORIZED_REQUEST_PATH, requestPath, BasicAuthConfig.BEARER); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } } else { logger.error("Bearer token is allowed but missing the bearer user path definitions for authorization"); setExchangeStatus(exchange, BEARER_USER_NOT_FOUND); if(logger.isDebugEnabled()) logger.debug("BasicAuthHandler.handleRequest ends with an error."); exchange.endExchange(); return false; } } return true; } @Override public HttpHandler getNext() { return next; } @Override public MiddlewareHandler setNext(final HttpHandler next) { Handlers.handlerNotNull(next); this.next = next; return this; } @Override public boolean isEnabled() { return config.isEnabled(); } @Override public void register() { // As passwords are in the config file, we need to mask them. List masks = new ArrayList<>(); masks.add("password"); ModuleRegistry.registerModule(BasicAuthConfig.CONFIG_NAME, BasicAuthHandler.class.getName(), Config.getNoneDecryptedInstance().getJsonMapConfigNoCache(BasicAuthConfig.CONFIG_NAME), masks); } @Override public void reload() { config.reload(); List masks = new ArrayList<>(); masks.add("password"); ModuleRegistry.registerModule(BasicAuthConfig.CONFIG_NAME, BasicAuthHandler.class.getName(), Config.getNoneDecryptedInstance().getJsonMapConfigNoCache(BasicAuthConfig.CONFIG_NAME), masks); if(logger.isInfoEnabled()) logger.info("BasicAuthHandler is reloaded."); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy