com.networknt.basicauth.BasicAuthHandler Maven / Gradle / Ivy
Show all versions of basic-auth Show documentation
/*
* 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.");
}
}