diff --git a/alpine-server/src/main/java/alpine/server/auth/AllowApiKeyInQueryParameter.java b/alpine-server/src/main/java/alpine/server/auth/AllowApiKeyInQueryParameter.java new file mode 100644 index 00000000..1262bfc8 --- /dev/null +++ b/alpine-server/src/main/java/alpine/server/auth/AllowApiKeyInQueryParameter.java @@ -0,0 +1,40 @@ +/* + * This file is part of Alpine. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) Steve Springett. All Rights Reserved. + */ +package alpine.server.auth; + +import jakarta.ws.rs.NameBinding; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation that indicates that a method of a JAX-RX resource is authenticated using + * the URI query parameter 'apiKey' instead of the header 'X-API-Key'. + * + * @author Kirill Sybin + * @since 3.0.2 + */ +@NameBinding +@Retention(value = RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface AllowApiKeyInQueryParameter { +} \ No newline at end of file diff --git a/alpine-server/src/main/java/alpine/server/auth/ApiKeyAuthenticationService.java b/alpine-server/src/main/java/alpine/server/auth/ApiKeyAuthenticationService.java index 36af86e6..cfc5d837 100644 --- a/alpine-server/src/main/java/alpine/server/auth/ApiKeyAuthenticationService.java +++ b/alpine-server/src/main/java/alpine/server/auth/ApiKeyAuthenticationService.java @@ -37,12 +37,21 @@ public class ApiKeyAuthenticationService implements AuthenticationService { /** * Given the specified ContainerRequest, the constructor retrieves a header - * named 'X-Api-Key', if it exists. + * named 'X-Api-Key' or, if allowed, a URI query parameter named 'apiKey', if + * they exist. * @param request the ContainerRequest object + * @param allowByQuery allow looking for the API key in the query when + * it is not passed via header * @since 1.0.0 */ - public ApiKeyAuthenticationService(final ContainerRequest request) { - this.assertedApiKey = request.getHeaderString("X-Api-Key"); + public ApiKeyAuthenticationService(final ContainerRequest request, boolean allowByQuery) { + if (request.getHeaderString("X-Api-Key") != null) { + this.assertedApiKey = request.getHeaderString("X-Api-Key"); + } else if (allowByQuery) { + this.assertedApiKey = request.getUriInfo().getQueryParameters().getFirst("apiKey"); + } else { + this.assertedApiKey = null; + } } /** @@ -55,9 +64,9 @@ public boolean isSpecified() { } /** - * Authenticates the API key (if it was specified in the X-Api-Key header) - * and returns a Principal if authentication is successful. Otherwise, - * returns an AuthenticationException. + * Authenticates the API key (if it was specified in the X-Api-Key header + * or apiKey query param and returns a Principal if authentication is + * successful. Otherwise, returns an AuthenticationException. * @return a Principal of which ApiKey is an instance of * @throws AuthenticationException upon an authentication failure * @since 1.0.0 diff --git a/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java b/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java index 13ec980c..7838304f 100644 --- a/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java +++ b/alpine-server/src/main/java/alpine/server/filters/AuthenticationFilter.java @@ -21,6 +21,7 @@ import alpine.common.logging.Logger; import alpine.model.ApiKey; import alpine.server.auth.ApiKeyAuthenticationService; +import alpine.server.auth.AllowApiKeyInQueryParameter; import alpine.server.auth.JwtAuthenticationService; import org.glassfish.jersey.server.ContainerRequest; import org.owasp.security.logging.SecurityMarkers; @@ -31,6 +32,8 @@ import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Context; import javax.naming.AuthenticationException; import java.security.Principal; @@ -48,6 +51,9 @@ public class AuthenticationFilter implements ContainerRequestFilter { // Setup logging private static final Logger LOGGER = Logger.getLogger(AuthenticationFilter.class); + @Context + private ResourceInfo resourceInfo; + @Override public void filter(ContainerRequestContext requestContext) { if (requestContext instanceof ContainerRequest) { @@ -59,7 +65,8 @@ public void filter(ContainerRequestContext requestContext) { Principal principal = null; - final ApiKeyAuthenticationService apiKeyAuthService = new ApiKeyAuthenticationService(request); + final boolean allowsApiKeyInQueryParameter = resourceInfo.getResourceMethod().isAnnotationPresent(AllowApiKeyInQueryParameter.class); + final ApiKeyAuthenticationService apiKeyAuthService = new ApiKeyAuthenticationService(request, allowsApiKeyInQueryParameter); if (apiKeyAuthService.isSpecified()) { try { principal = apiKeyAuthService.authenticate();