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 &lt;key&gt;</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}