001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.server.config.rest; 018 019import java.io.*; 020import java.net.*; 021import java.nio.charset.*; 022import java.security.*; 023 024import org.apache.juneau.config.*; 025import org.apache.juneau.http.response.*; 026import org.apache.juneau.rest.server.*; 027import org.apache.juneau.rest.server.guard.*; 028 029/** 030 * Guard that protects {@link LoadConfigResource} with a shared API key. 031 * 032 * <p> 033 * The expected key is read from the <c>[Security]</c> section of <c>juneau-server-config.cfg</c>: 034 * <p class='bini'> 035 * <cs>[Security]</cs> 036 * <ck>apiKey</ck> = <cv>some-long-random-secret</cv> 037 * </p> 038 * 039 * <p> 040 * Clients supply the key in either an <c>Authorization: Bearer <key></c> header or an <c>X-Api-Key</c> header. 041 * The comparison is constant-time ({@link MessageDigest#isEqual(byte[], byte[])} over UTF-8 bytes). 042 * 043 * <p> 044 * The key is read once, when the guard is created, and the {@link Config} used to read it is closed straight away. 045 * Changing the key requires a restart. 046 * 047 * <p> 048 * Requests that did not arrive over a secure channel are refused unless they come from a loopback address or 049 * <c>[Security]</c> <ck>allowInsecure</ck> is <cv>true</cv>. 050 * 051 * <p> 052 * The guard <b>fails closed</b>: if no key is configured (missing, empty or unreadable setting), every request is 053 * rejected with a 403. The key is never logged or echoed. Always use TLS. 054 */ 055public class ConfigApiKeyGuard extends RestGuard { 056 057 private static final String CONFIG_NAME = "juneau-server-config.cfg"; 058 private static final String CONFIG_KEY = "Security/apiKey"; 059 private static final String ALLOW_INSECURE_KEY = "Security/allowInsecure"; 060 private static final String BEARER = "Bearer "; 061 062 private final String key; 063 private final boolean allowInsecure; 064 065 /** Constructor. Reads the settings once and releases the configuration. */ 066 public ConfigApiKeyGuard() { 067 String k = null; 068 var insecure = false; 069 try { 070 var c = Config.create().name(CONFIG_NAME).build(); 071 try { 072 k = c.get(CONFIG_KEY).orElse(null); 073 insecure = c.get(ALLOW_INSECURE_KEY).asBoolean().orElse(false); 074 } finally { 075 c.close(); 076 } 077 } catch (@SuppressWarnings("unused") Exception e) { 078 // Fail closed; deliberately do not log (avoid leaking config content). 079 } 080 key = k; 081 allowInsecure = insecure; 082 } 083 084 @Override /* Overridden from RestGuard */ 085 public boolean guard(RestRequest req, RestResponse res) throws BasicHttpException { 086 if (! isRequestAllowed(req)) 087 throw new Forbidden("Access denied."); 088 return true; 089 } 090 091 @Override /* Overridden from RestGuard */ 092 public boolean isRequestAllowed(RestRequest req) { 093 var r = req.getHttpServletRequest(); 094 if (! isChannelAcceptable(r.isSecure(), r.getRemoteAddr(), allowInsecure)) 095 return false; 096 return isAuthorized(configuredKey(), req.getHeader("Authorization"), req.getHeader("X-Api-Key")); 097 } 098 099 static boolean isChannelAcceptable(boolean secure, String remoteAddr, boolean allowInsecure) { 100 if (secure || allowInsecure) 101 return true; 102 try { 103 return remoteAddr != null && InetAddress.getByName(remoteAddr).isLoopbackAddress(); 104 } catch (@SuppressWarnings("unused") IOException e) { 105 return false; 106 } 107 } 108 109 /** 110 * Returns the key read at startup, or <jk>null</jk> if none is configured or the configuration could not be read. 111 * 112 * @return The configured key, or <jk>null</jk>. 113 */ 114 protected String configuredKey() { 115 return key; 116 } 117 118 static boolean isAuthorized(String configured, String authorization, String xApiKey) { 119 if (configured == null || configured.isEmpty()) 120 return false; 121 var expected = configured.getBytes(StandardCharsets.UTF_8); 122 var ok = false; 123 if (authorization != null && authorization.regionMatches(true, 0, BEARER, 0, BEARER.length())) 124 ok = matches(expected, authorization.substring(BEARER.length()).trim()); 125 if (xApiKey != null) 126 ok |= matches(expected, xApiKey.trim()); 127 return ok; 128 } 129 130 private static boolean matches(byte[] expected, String supplied) { 131 return MessageDigest.isEqual(expected, supplied.getBytes(StandardCharsets.UTF_8)); 132 } 133}