mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-01-25 19:55:48 +01:00
Add cache for Basic Authentication (#2101)
Also-by: Sebastian Gerber <github@sgerber.de> Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
parent
6568dc1478
commit
4964b51160
@ -13,8 +13,13 @@
|
|||||||
package org.openhab.core.io.rest.auth.internal;
|
package org.openhab.core.io.rest.auth.internal;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
import javax.annotation.Priority;
|
import javax.annotation.Priority;
|
||||||
import javax.ws.rs.Priorities;
|
import javax.ws.rs.Priorities;
|
||||||
@ -26,6 +31,7 @@ import javax.ws.rs.core.Response.Status;
|
|||||||
import javax.ws.rs.core.SecurityContext;
|
import javax.ws.rs.core.SecurityContext;
|
||||||
import javax.ws.rs.ext.Provider;
|
import javax.ws.rs.ext.Provider;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.core.auth.Authentication;
|
import org.openhab.core.auth.Authentication;
|
||||||
import org.openhab.core.auth.AuthenticationException;
|
import org.openhab.core.auth.AuthenticationException;
|
||||||
@ -33,12 +39,14 @@ import org.openhab.core.auth.User;
|
|||||||
import org.openhab.core.auth.UserApiTokenCredentials;
|
import org.openhab.core.auth.UserApiTokenCredentials;
|
||||||
import org.openhab.core.auth.UserRegistry;
|
import org.openhab.core.auth.UserRegistry;
|
||||||
import org.openhab.core.auth.UsernamePasswordCredentials;
|
import org.openhab.core.auth.UsernamePasswordCredentials;
|
||||||
|
import org.openhab.core.common.registry.RegistryChangeListener;
|
||||||
import org.openhab.core.config.core.ConfigurableService;
|
import org.openhab.core.config.core.ConfigurableService;
|
||||||
import org.openhab.core.io.rest.JSONResponse;
|
import org.openhab.core.io.rest.JSONResponse;
|
||||||
import org.openhab.core.io.rest.RESTConstants;
|
import org.openhab.core.io.rest.RESTConstants;
|
||||||
import org.osgi.framework.Constants;
|
import org.osgi.framework.Constants;
|
||||||
import org.osgi.service.component.annotations.Activate;
|
import org.osgi.service.component.annotations.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
import org.osgi.service.component.annotations.Component;
|
||||||
|
import org.osgi.service.component.annotations.Deactivate;
|
||||||
import org.osgi.service.component.annotations.Modified;
|
import org.osgi.service.component.annotations.Modified;
|
||||||
import org.osgi.service.component.annotations.Reference;
|
import org.osgi.service.component.annotations.Reference;
|
||||||
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
|
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
|
||||||
@ -54,6 +62,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
* @author Yannick Schaus - initial contribution
|
* @author Yannick Schaus - initial contribution
|
||||||
* @author Yannick Schaus - Allow basic authentication
|
* @author Yannick Schaus - Allow basic authentication
|
||||||
* @author Yannick Schaus - Add support for API tokens
|
* @author Yannick Schaus - Add support for API tokens
|
||||||
|
* @author Sebastian Gerber - Add basic auth caching
|
||||||
|
* @author Kai Kreuzer - Add null annotations, constructor initialization
|
||||||
*/
|
*/
|
||||||
@PreMatching
|
@PreMatching
|
||||||
@Component(configurationPid = "org.openhab.restauth", property = Constants.SERVICE_PID + "=org.openhab.restauth")
|
@Component(configurationPid = "org.openhab.restauth", property = Constants.SERVICE_PID + "=org.openhab.restauth")
|
||||||
@ -62,6 +72,7 @@ import org.slf4j.LoggerFactory;
|
|||||||
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
|
||||||
@Priority(Priorities.AUTHENTICATION)
|
@Priority(Priorities.AUTHENTICATION)
|
||||||
@Provider
|
@Provider
|
||||||
|
@NonNullByDefault
|
||||||
public class AuthFilter implements ContainerRequestFilter {
|
public class AuthFilter implements ContainerRequestFilter {
|
||||||
private final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
|
private final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
|
||||||
|
|
||||||
@ -71,19 +82,49 @@ public class AuthFilter implements ContainerRequestFilter {
|
|||||||
protected static final String CONFIG_URI = "system:restauth";
|
protected static final String CONFIG_URI = "system:restauth";
|
||||||
private static final String CONFIG_ALLOW_BASIC_AUTH = "allowBasicAuth";
|
private static final String CONFIG_ALLOW_BASIC_AUTH = "allowBasicAuth";
|
||||||
private static final String CONFIG_IMPLICIT_USER_ROLE = "implicitUserRole";
|
private static final String CONFIG_IMPLICIT_USER_ROLE = "implicitUserRole";
|
||||||
|
private static final String CONFIG_CACHE_EXPIRATION = "cacheExpiration";
|
||||||
|
|
||||||
private boolean allowBasicAuth = false;
|
private boolean allowBasicAuth = false;
|
||||||
private boolean implicitUserRole = true;
|
private boolean implicitUserRole = true;
|
||||||
|
private Long cacheExpiration = 6L;
|
||||||
|
|
||||||
@Reference
|
private ExpiringUserSecurityContextCache authCache = new ExpiringUserSecurityContextCache(
|
||||||
private JwtHelper jwtHelper;
|
Duration.ofHours(cacheExpiration).toMillis());
|
||||||
|
|
||||||
@Reference
|
private final byte[] RANDOM_BYTES = new byte[32];
|
||||||
private UserRegistry userRegistry;
|
|
||||||
|
private final JwtHelper jwtHelper;
|
||||||
|
private final UserRegistry userRegistry;
|
||||||
|
|
||||||
|
private RegistryChangeListener<User> userRegistryListener = new RegistryChangeListener<User>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void added(User element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removed(User element) {
|
||||||
|
authCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updated(User oldElement, User element) {
|
||||||
|
authCache.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@Activate
|
||||||
|
public AuthFilter(@Reference JwtHelper jwtHelper, @Reference UserRegistry userRegistry) {
|
||||||
|
this.jwtHelper = jwtHelper;
|
||||||
|
this.userRegistry = userRegistry;
|
||||||
|
new Random().nextBytes(RANDOM_BYTES);
|
||||||
|
}
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
protected void activate(Map<String, Object> config) {
|
protected void activate(Map<String, Object> config) {
|
||||||
modified(config);
|
modified(config);
|
||||||
|
userRegistry.addRegistryChangeListener(userRegistryListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Modified
|
@Modified
|
||||||
@ -93,6 +134,37 @@ public class AuthFilter implements ContainerRequestFilter {
|
|||||||
allowBasicAuth = value != null && "true".equals(value.toString());
|
allowBasicAuth = value != null && "true".equals(value.toString());
|
||||||
value = properties.get(CONFIG_IMPLICIT_USER_ROLE);
|
value = properties.get(CONFIG_IMPLICIT_USER_ROLE);
|
||||||
implicitUserRole = value == null || !"false".equals(value.toString());
|
implicitUserRole = value == null || !"false".equals(value.toString());
|
||||||
|
value = properties.get(CONFIG_CACHE_EXPIRATION);
|
||||||
|
if (value != null) {
|
||||||
|
try {
|
||||||
|
cacheExpiration = Long.valueOf(value.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.warn("Ignoring invalid configuration value '{}' for cacheExpiration parameter.", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authCache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deactivate
|
||||||
|
protected void deactivate() {
|
||||||
|
userRegistry.removeRegistryChangeListener(userRegistryListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable String getCacheKey(String credentials) {
|
||||||
|
if (cacheExpiration == 0) {
|
||||||
|
// caching is disabled
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
md.update(RANDOM_BYTES);
|
||||||
|
return new String(md.digest(credentials.getBytes()));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// SHA-256 is available for all java distributions so this code will actually never run
|
||||||
|
// If it does we'll just flood the cache with random values
|
||||||
|
logger.warn("SHA-256 is not available. Cache for basic auth disabled!");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,36 +183,59 @@ public class AuthFilter implements ContainerRequestFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SecurityContext authenticateUsernamePassword(String username, String password)
|
private SecurityContext authenticateBasicAuth(String credentialString) throws AuthenticationException {
|
||||||
throws AuthenticationException {
|
final String cacheKey = getCacheKey(credentialString);
|
||||||
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
|
if (cacheKey != null) {
|
||||||
|
final UserSecurityContext cachedValue = authCache.get(cacheKey);
|
||||||
|
if (cachedValue != null) {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] decodedCredentials = new String(Base64.getDecoder().decode(credentialString), StandardCharsets.UTF_8)
|
||||||
|
.split(":");
|
||||||
|
if (decodedCredentials.length != 2) {
|
||||||
|
throw new AuthenticationException("Invalid Basic authentication credential format");
|
||||||
|
}
|
||||||
|
|
||||||
|
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(decodedCredentials[0],
|
||||||
|
decodedCredentials[1]);
|
||||||
Authentication auth = userRegistry.authenticate(credentials);
|
Authentication auth = userRegistry.authenticate(credentials);
|
||||||
User user = userRegistry.get(auth.getUsername());
|
User user = userRegistry.get(auth.getUsername());
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
throw new AuthenticationException("User not found in registry");
|
throw new AuthenticationException("User not found in registry");
|
||||||
}
|
}
|
||||||
return new UserSecurityContext(user, auth, "Basic");
|
|
||||||
|
UserSecurityContext context = new UserSecurityContext(user, auth, "Basic");
|
||||||
|
|
||||||
|
if (cacheKey != null) {
|
||||||
|
authCache.put(cacheKey, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void filter(ContainerRequestContext requestContext) throws IOException {
|
public void filter(@Nullable ContainerRequestContext requestContext) throws IOException {
|
||||||
try {
|
if (requestContext != null) {
|
||||||
String altTokenHeader = requestContext.getHeaderString(ALT_AUTH_HEADER);
|
try {
|
||||||
if (altTokenHeader != null) {
|
String altTokenHeader = requestContext.getHeaderString(ALT_AUTH_HEADER);
|
||||||
requestContext.setSecurityContext(authenticateBearerToken(altTokenHeader));
|
if (altTokenHeader != null) {
|
||||||
return;
|
requestContext.setSecurityContext(authenticateBearerToken(altTokenHeader));
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
String authHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
|
||||||
if (authHeader != null) {
|
if (authHeader != null) {
|
||||||
String[] authParts = authHeader.split(" ");
|
String[] authParts = authHeader.split(" ");
|
||||||
if (authParts.length == 2) {
|
if (authParts.length == 2) {
|
||||||
if ("Bearer".equalsIgnoreCase(authParts[0])) {
|
String authType = authParts[0];
|
||||||
requestContext.setSecurityContext(authenticateBearerToken(authParts[1]));
|
String authValue = authParts[1];
|
||||||
return;
|
if ("Bearer".equalsIgnoreCase(authType)) {
|
||||||
} else if ("Basic".equalsIgnoreCase(authParts[0])) {
|
requestContext.setSecurityContext(authenticateBearerToken(authValue));
|
||||||
try {
|
return;
|
||||||
String[] decodedCredentials = new String(Base64.getDecoder().decode(authParts[1]), "UTF-8")
|
} else if ("Basic".equalsIgnoreCase(authType)) {
|
||||||
|
String[] decodedCredentials = new String(Base64.getDecoder().decode(authValue), "UTF-8")
|
||||||
.split(":");
|
.split(":");
|
||||||
if (decodedCredentials.length > 2) {
|
if (decodedCredentials.length > 2) {
|
||||||
throw new AuthenticationException("Invalid Basic authentication credential format");
|
throw new AuthenticationException("Invalid Basic authentication credential format");
|
||||||
@ -154,25 +249,17 @@ public class AuthFilter implements ContainerRequestFilter {
|
|||||||
throw new AuthenticationException(
|
throw new AuthenticationException(
|
||||||
"Basic authentication with username/password is not allowed");
|
"Basic authentication with username/password is not allowed");
|
||||||
}
|
}
|
||||||
requestContext.setSecurityContext(
|
requestContext.setSecurityContext(authenticateBasicAuth(authValue));
|
||||||
authenticateUsernamePassword(decodedCredentials[0], decodedCredentials[1]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
throw new AuthenticationException("Invalid Basic authentication credentials", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (implicitUserRole) {
|
||||||
|
requestContext.setSecurityContext(new AnonymousUserSecurityContext());
|
||||||
}
|
}
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
logger.warn("Unauthorized API request: {}", e.getMessage());
|
||||||
|
requestContext.abortWith(JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Invalid credentials"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (implicitUserRole) {
|
|
||||||
requestContext.setSecurityContext(new AnonymousUserSecurityContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
logger.warn("Unauthorized API request: {}", e.getMessage());
|
|
||||||
requestContext.abortWith(JSONResponse.createErrorResponse(Status.UNAUTHORIZED, "Invalid credentials"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2010-2021 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.core.io.rest.auth.internal;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides a cache for up to 10 UserSecurityContexts.
|
||||||
|
* Entries have a lifetime and are removed from the cache upon the next
|
||||||
|
* get call.
|
||||||
|
*
|
||||||
|
* @author Kai Kreuzer - Initial contribution
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ExpiringUserSecurityContextCache {
|
||||||
|
final static private int MAX_SIZE = 10;
|
||||||
|
final static private int CLEANUP_FREQUENCY = 10;
|
||||||
|
|
||||||
|
final private long keepPeriod;
|
||||||
|
final private Map<String, Entry> entryMap;
|
||||||
|
|
||||||
|
private int calls = 0;
|
||||||
|
|
||||||
|
ExpiringUserSecurityContextCache(long expirationTime) {
|
||||||
|
this.keepPeriod = expirationTime;
|
||||||
|
entryMap = new LinkedHashMap<>() {
|
||||||
|
private static final long serialVersionUID = -1220310861591070462L;
|
||||||
|
|
||||||
|
protected boolean removeEldestEntry(Map.@Nullable Entry<String, Entry> eldest) {
|
||||||
|
return size() > MAX_SIZE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized @Nullable UserSecurityContext get(String key) {
|
||||||
|
calls++;
|
||||||
|
if (calls >= CLEANUP_FREQUENCY) {
|
||||||
|
entryMap.keySet().forEach(k -> getEntry(k));
|
||||||
|
calls = 0;
|
||||||
|
}
|
||||||
|
Entry entry = getEntry(key);
|
||||||
|
if (entry != null) {
|
||||||
|
return entry.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void put(String key, UserSecurityContext value) {
|
||||||
|
entryMap.put(key, new Entry(System.currentTimeMillis(), value));
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized void clear() {
|
||||||
|
entryMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable Entry getEntry(String key) {
|
||||||
|
Entry entry = entryMap.get(key);
|
||||||
|
if (entry != null) {
|
||||||
|
final long curTimeMillis = System.currentTimeMillis();
|
||||||
|
long entryAge = curTimeMillis - entry.timestamp;
|
||||||
|
if (entryAge < 0 || entryAge >= keepPeriod) {
|
||||||
|
entryMap.remove(key);
|
||||||
|
entry = null;
|
||||||
|
} else {
|
||||||
|
entry.timestamp = curTimeMillis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
static class Entry {
|
||||||
|
public long timestamp;
|
||||||
|
final public UserSecurityContext value;
|
||||||
|
|
||||||
|
Entry(long timestamp, UserSecurityContext value) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,18 @@
|
|||||||
<description>Allow the use of Basic authentication to access protected API resources, in addition to access tokens
|
<description>Allow the use of Basic authentication to access protected API resources, in addition to access tokens
|
||||||
and API tokens.</description>
|
and API tokens.</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
|
<parameter name="cacheExpiration" type="integer" min="0">
|
||||||
|
<advanced>true</advanced>
|
||||||
|
<label>Cache Expiration Time</label>
|
||||||
|
<default>6</default>
|
||||||
|
<unitLabel>h</unitLabel>
|
||||||
|
<description>When basic authentication is activated, credentials are put in a cache in order to speed up request
|
||||||
|
authorization.
|
||||||
|
The entries in the cache expire after a while in order to not keep credentials in memory indefinitely.
|
||||||
|
This value defines the expiration time in hours.
|
||||||
|
Set it to 0 for disabling the cache.
|
||||||
|
</description>
|
||||||
|
</parameter>
|
||||||
<parameter name="implicitUserRole" type="boolean" required="false">
|
<parameter name="implicitUserRole" type="boolean" required="false">
|
||||||
<advanced>true</advanced>
|
<advanced>true</advanced>
|
||||||
<label>Implicit user role for unauthenticated requests</label>
|
<label>Implicit user role for unauthenticated requests</label>
|
||||||
|
Loading…
Reference in New Issue
Block a user