Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ApiKey/Token authentication #1498

Merged
merged 11 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package org.deegree.services.config;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.util.Base64;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Random;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
import org.deegree.commons.config.DeegreeWorkspace;
import org.deegree.commons.utils.TunableParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Handle access to an API key file containing a token
*
* @author <a href="mailto:reichhelm@grit.de">Stephan Reichhelm</a>
*/
public class ApiKey {

private static final Logger LOG = LoggerFactory.getLogger( ApiKey.class );

private static final String API_TOKEN_FILE = "config.apikey";

/**
* Token to be checked
*
* Every value matches this token if the value is "*". No value matches this token if the value is null or an empty
* string.
*/
class Token {

final boolean allowAll;

private final String key;

public Token( String value ) {
this.allowAll = value != null && "*".equals( value.trim() );
this.key = value != null && value.trim().length() > 0 ? value.trim() : value;
}

public Token() {
this.allowAll = false;
this.key = null;
}

public boolean matches( String value ) {
if ( allowAll )
return true;

if ( key == null )
return false;

return key.matches( value != null ? value.trim() : value );
}

public boolean isAnyAllowed() {
return allowAll;
}
}

private Path getPasswordFile() {
String workspace = DeegreeWorkspace.getWorkspaceRoot();
return Paths.get( workspace, API_TOKEN_FILE );
}

private String generateRandomApiKey() {
try {
MessageDigest md = DigestUtils.getSha1Digest();
// add some random data
Random rnd = new Random();
byte[] data = new byte[128];
rnd.nextBytes( data );
// add random data
md.update( data );
md.update( new Date().toString().getBytes() );
byte[] digest = md.digest();

return Hex.encodeHexString( digest );
} catch ( Exception ex ) {
LOG.warn( "Could not generate random key with SHA-1: {}", ex.getMessage() );
LOG.trace( "Exception", ex );
}
return null;
}

public Token getCurrentToken()
throws SecurityException {
Path file = getPasswordFile();
Token token = null;
final String ls = System.lineSeparator();
final String marker = "*************************************************************" + ls;

try {
if ( Files.isReadable( file ) ) {
List<String> lines = Files.readAllLines( file );
if ( lines.size() != 1 ) {
LOG.warn( "{}API Key file '{}' has an incorrect format (multiple lines). {} " + //
"The REST API will not be accessible. {}", //
ls + ls + marker + marker + marker + ls, //
file, ls, //
ls + marker + marker + marker );
} else {
token = new Token( lines.get( 0 ) );
}
} else if ( !Files.exists( file ) ) {
// create new one, if no file exists
String apikey = generateRandomApiKey();
Files.write( file, Collections.singleton( apikey ) );
token = new Token( apikey );
LOG.warn( "{}An API Key file with an random key was generated at '{}'.{}", //
ls + ls + marker + marker + marker + ls, //
file, ls, //
ls + marker + marker + marker );
} else {
LOG.warn( "{}API Key file '{}' is not a regular file or not readable. {} " + //
"The REST API will not be accessible.{}", //
ls + ls + marker + marker + marker + ls, //
file, ls, //
ls + marker + marker + marker );
}
} catch ( IOException ioe ) {
LOG.warn( "{}API Key file '{}' could not be accessed. {} " + //
"The REST API will not be accessible.{}", //
ls + ls + marker + marker + marker + ls, //
file, ls, //
ls + marker + marker + marker );
LOG.debug("API key file could not be accessed", ioe);
}

if ( token == null ) {
token = new Token();
} else if ( token.isAnyAllowed() ) {
if ( TunableParameter.get( "deegree.config.apikey.warn-when-disabled", true ) ) {
LOG.warn( "{}The REST API is currently configured insecure. We strongly recommend to use a key value instead at '{}'.{}",
ls + ls + marker + marker + marker + ls, //
file, //
ls + marker + marker + marker );
}
} else {
LOG.info( "***" );
LOG.info( "*** NOTE: The REST API is secured, so that the key set in file '{}' is required to access it." );
LOG.info( "***" );
}

return token;
}

public void validate( HttpServletRequest req )
throws SecurityException {
String tmp, value = null;
// check for headers
if ( value == null ) {
value = req.getHeader( "X-API-Key" );
}
if ( value == null ) {
tmp = req.getHeader( "Authorization" );
if ( tmp != null && tmp.toLowerCase().startsWith( "bearer " ) ) {
value = tmp.substring( 7 );
} else if ( tmp != null && tmp.toLowerCase().startsWith( "basic " ) ) {
tmp = tmp.substring( 6 );
final byte[] decoded = Base64.getDecoder().decode( tmp );
final String credentials = new String( decoded, StandardCharsets.UTF_8 );
// credentials = username:password
final String[] values = credentials.split( ":", 2 );
if ( values.length == 2 && values[1] != null ) {
value = values[1];
}
}
}

// check for parameter
if ( value == null ) {
Enumeration<?> keys = req.getParameterNames();
while ( keys.hasMoreElements() ) {
String key = (String) keys.nextElement();
if ( "token".equalsIgnoreCase( key ) || "api_key".equalsIgnoreCase( key ) ) {
value = req.getParameter( key );
break;
}
}
}

// initialize early to allow creation of apikey/token
Token token = getCurrentToken();

if ( token.isAnyAllowed() ) {
// no API Key required
return;
}

if ( value == null || value.trim().length() == 0 ) {
throw new SecurityException( "Please specify API Key" );
}

if ( !token.matches( value ) ) {
throw new SecurityException( "Invalid API Key specified" );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.deegree.services.config.ApiKey;
import org.slf4j.Logger;

/**
Expand All @@ -69,6 +70,8 @@ public class ConfigServlet extends HttpServlet {
private static final long serialVersionUID = -4412872621677620591L;

private static final Logger LOG = getLogger( ConfigServlet.class );

private static ApiKey token = new ApiKey();

@Override
public void init()
Expand Down Expand Up @@ -128,6 +131,8 @@ protected void doGet( HttpServletRequest req, HttpServletResponse resp )

private void dispatch( String path, HttpServletRequest req, HttpServletResponse resp )
throws IOException, ServletException {
token.validate( req );

if ( path.toLowerCase().startsWith( "/download" ) ) {
download( path.substring( 9 ), resp );
}
Expand Down Expand Up @@ -200,5 +205,4 @@ protected void doPut( HttpServletRequest req, HttpServletResponse resp )
dispatch( path, req, resp );
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ f

|deegree.gml.property.simple.trim |java.lang.Boolean |true |When deegree reads GML data, by default (`true`) simple property values get their leading and trailing whitespace characters removed.

|deegree.config.apikey.warn-when-disabled |java.lang.Boolean |true |Log warning if security on REST api is disabled by specifying `*` in _config.apikey_.

|===
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ files exist:
|console.pw |Password for services console
|proxy.xml |Proxy settings
|webapps.properties |Selects the active workspace
|config.apikey |Contains the key to protect the REST API
|===

Note that only a single workspace can be active at a time. The
Expand All @@ -156,6 +157,11 @@ every instance can use a different workspace. The file
_webapps.properties_ stores the active workspace for every deegree
webapp separately.

TIP: If there is no _config.apikey_ file, one will be generated on startup
with an random value. Alternatively, a value of `*` in config.apikey will
turn off security for the REST API. We strongly advise against doing this
in productive environments.

=== Structure of the deegree workspace directory

The workspace directory is a container for resource files with a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,20 @@ workspaces or resources and start a different workspace.

The servlet that handles the REST interface is already running if you
use the standard _web.xml_ deployment descriptor. For security reasons
the REST API is only accessible after successful authentication against
the servlet container. When using Apache Tomcat you'll need to add a
user with the role _deegree_ to your Tomcat configuration
_conf/tomcat-users.xml_ file.
the REST API is secured by default with an API key read from the
_config.apikey_ file in deegree workspace directory.

The API key can be provide in multiple different ways.
* As header value of key `X-API-Key`
* As authorization header of bearer type
* As basic authorization password where username will be ignored
* As parameter `token`
* As parameter `api_key`

TIP: If there is no _config.apikey_ file, one will be generated on startup
with an random value. Alternatively, a value of `*` in config.apikey will
turn off security for the REST API. We strongly advise against doing this
in productive environments.

Once you did that, you can get an overview of available 'commands' by
requesting _http://localhost:8080/deegree-webservices/config_. You'll
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>

<!-- REST API is protected by API key by default. -->
<!--
<security-constraint>
<web-resource-collection>
<web-resource-name>Configuration</web-resource-name>
Expand All @@ -110,6 +112,7 @@
<description>deegree administrator role</description>
<role-name>deegree</role-name>
</security-role>
-->

<!-- http basic auth enable -->
<!-- Define a security constraint on this application -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
<welcome-file>index.xhtml</welcome-file>
</welcome-file-list>

<!-- REST API is protected by API key by default. -->
<!--
<security-constraint>
<web-resource-collection>
<web-resource-name>Configuration</web-resource-name>
Expand All @@ -75,5 +77,6 @@
<description>deegree administrator role</description>
<role-name>deegree</role-name>
</security-role>
-->

</web-app>
1 change: 0 additions & 1 deletion deegree-tests/deegree-workspace-tests/src/main/.gitignore

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*