Skip to content

Commit

Permalink
Setup token authentication (#148)
Browse files Browse the repository at this point in the history
Co-authored-by: Antonio Murgia <[email protected]>
  • Loading branch information
duhizjame and agilelab-tmnd1991 authored Dec 30, 2023
1 parent 7ccfa42 commit ba9394e
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 1 deletion.
24 changes: 24 additions & 0 deletions docsite/docs/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Authentication

By default, Whitefox runs with authentication disabled.

## Token authentication

We provide a simple way to secure your application using a single static token.

To enable token authentication you need to:

- set `whitefox.server.authentication.enabled` to `true`
- set `whitefox.server.authentication.bearerToken` to a secret string

After doing so, you need to provide a http header such as `Authentication: Bearer $token` in every request otherwise
you will receive a 401 error authentication error on the client.

| property name | default value | description |
|--------------------------------------------|-----------------|-------------------------------------------------|
| whitefox.server.authentication.enabled | false | either true or false to enable authentication |
| whitefox.server.authentication.bearerToken | | the token used for authentication |



In order to set configurations refer to [Quarkus documentation](https://quarkus.io/guides/config-reference).
1 change: 1 addition & 0 deletions server/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies {
// QUARKUS
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-security")
implementation("io.quarkus:quarkus-container-image-docker")
implementation("io.quarkus:quarkus-resteasy-reactive")
implementation("io.quarkus:quarkus-resteasy-reactive-jackson")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.whitefox.api.server.auth;

import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AnonymousAuthenticationRequest;
import io.quarkus.security.identity.request.AuthenticationRequest;
import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
import jakarta.inject.Inject;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class SimpleTokenAuthenticationMechanism implements HttpAuthenticationMechanism {

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final QuarkusPrincipal principal = new QuarkusPrincipal("Mr. WhiteFox");

String token;

@Inject
IdentityProviderManager identityProvider;

public SimpleTokenAuthenticationMechanism(String token) {
this.token = token;
}

private Uni<SecurityIdentity> anonymous() {
return identityProvider.authenticate(AnonymousAuthenticationRequest.INSTANCE);
}

@Override
public Uni<SecurityIdentity> authenticate(
RoutingContext context, IdentityProviderManager identityProviderManager) {
if (context.normalizedPath().startsWith("/q/")) return anonymous();
var optionalHeader = Optional.ofNullable(context.request().headers().get(AUTHORIZATION_HEADER));
QuarkusSecurityIdentity identity =
new QuarkusSecurityIdentity.Builder().setPrincipal(principal).build();
AuthenticationFailedException missingOrUnrecognizedCredentials =
new AuthenticationFailedException("Missing or unrecognized credentials");
AuthenticationFailedException missingToken = new AuthenticationFailedException(
"Simple authentication enabled, but token is missing in the request");
if (optionalHeader.isEmpty()) throw missingToken;
else {
if (Objects.equals("Bearer " + token, optionalHeader.get())) {
return Uni.createFrom().item(identity);
} else {
throw missingOrUnrecognizedCredentials;
}
}
}

// Not really needed for this mechanism, does not get called;
// The response to the user is handled through a custom attemptAuthentication from
// WhitefoxHttpAuthenticator
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
var challenge = "token -> " + token;
ChallengeData result = new ChallengeData(
HttpResponseStatus.UNAUTHORIZED.code(), HttpHeaderNames.WWW_AUTHENTICATE, challenge);
return Uni.createFrom().item(result);
}

// no need for this mechanism, for others
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.whitefox.api.server.auth;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;
import java.util.Optional;

/** Configuration for Whitefox authentication settings. */
@ConfigMapping(prefix = "whitefox.server.authentication")
public interface WhitefoxAuthenticationConfig {

/** Returns {@code true} if Whitefox authentication is enabled. */
@WithName("enabled")
@WithDefault("false")
boolean enabled();

/** Bearer token that should be used in requests to grant authorization. */
@WithName("bearerToken")
Optional<String> bearerToken();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.whitefox.api.server.auth;

import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.AnonymousAuthenticationRequest;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticator;
import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
import jakarta.annotation.Priority;
import jakarta.enterprise.inject.Alternative;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

/**
* A custom {@link HttpAuthenticator}. This authenticator that performs the following main duties:
*
* <ul>
* <li>Authenticates requests using a token provided in the application.properties when authentication is enabled.
* <li>Completely disallows unauthenticated requests when authentication is enabled.
* </ul>
*/
@Alternative // @Alternative + @Priority ensure the original HttpAuthenticator bean is not used
@Priority(1)
@Singleton
public class WhitefoxHttpAuthenticator extends HttpAuthenticator {

private final IdentityProviderManager identityProvider;
private final boolean authEnabled;
private final WhitefoxAuthenticationConfig config;

@Inject
public WhitefoxHttpAuthenticator(
WhitefoxAuthenticationConfig config,
IdentityProviderManager identityProviderManager,
Instance<PathMatchingHttpSecurityPolicy> pathMatchingPolicy,
Instance<HttpAuthenticationMechanism> httpAuthenticationMechanism,
Instance<IdentityProvider<?>> providers) {
super(identityProviderManager, pathMatchingPolicy, httpAuthenticationMechanism, providers);
this.identityProvider = identityProviderManager;
this.config = config;
authEnabled = config.enabled();
}

private HttpAuthenticationMechanism selectAuthenticationMechanism(
WhitefoxAuthenticationConfig config, RoutingContext context) {
if (config.bearerToken().isPresent()) {
return new SimpleTokenAuthenticationMechanism(config.bearerToken().get());
} else {
throw new AuthenticationFailedException(
"Other auth mechanisms not supported right now! Please add your token to application.properties");
}
}

@Override
public Uni<SecurityIdentity> attemptAuthentication(RoutingContext context) {
if (!authEnabled) {
return anonymous();
}
// quarkus dev paths
else if (context.normalizedPath().startsWith("/q/")) {
return anonymous();
} else {
return selectAuthenticationMechanism(config, context).authenticate(context, identityProvider);
}
}

private Uni<SecurityIdentity> anonymous() {
return identityProvider.authenticate(AnonymousAuthenticationRequest.INSTANCE);
}
}
4 changes: 3 additions & 1 deletion server/app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
io.delta.sharing.api.server.defaultMaxResults=10
quarkus.banner.path=banner.txt
quarkus.banner.path=banner.txt

whitefox.server.authentication.enabled=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package io.whitefox.api.server.auth;

import static io.restassured.RestAssured.given;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.restassured.http.Header;
import io.whitefox.api.model.v1.generated.CreateMetastore;
import io.whitefox.api.model.v1.generated.MetastoreProperties;
import io.whitefox.api.model.v1.generated.SimpleAwsCredentials;
import java.util.Map;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

@QuarkusTest
@TestProfile(WhitefoxHttpAuthenticatorTest.AuthenticationProfile.class)
public class WhitefoxHttpAuthenticatorTest {

private final CreateMetastore createMetastore = new CreateMetastore()
.name("glue_metastore_prod")
.skipValidation(false)
.type(CreateMetastore.TypeEnum.GLUE)
.properties(new MetastoreProperties()
.catalogId("123")
.credentials(new SimpleAwsCredentials()
.awsAccessKeyId("access")
.awsSecretAccessKey("secret")
.region("eu-west1")));

public static class AuthenticationProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of(
"whitefox.server.authentication.enabled",
"true",
"whitefox.server.authentication.bearerToken",
"myToken");
}
}

public static class NoAuthenticationProfile implements QuarkusTestProfile {
@Override
public Map<String, String> getConfigOverrides() {
return Map.of("whitefox.server.authentication.enabled", "false");
}
}

// Nesting two test-profiles leads to an OOM: https://github.com/quarkusio/quarkus/issues/12498
@Test
void expectDenied() {
given()
.when()
.get("/whitefox-api/v1/metastores/{name}", createMetastore.getName())
.then()
.statusCode(401);
}

@Test
void expectAcceptedWithAuth() {
given()
.when()
.header(new Header("Authorization", "Bearer myToken"))
.get("/whitefox-api/v1/metastores/{name}", createMetastore.getName())
.then()
.statusCode(404);
}

@Nested
@TestProfile(WhitefoxHttpAuthenticatorTest.NoAuthenticationProfile.class)
class TestNotAuthorized {

@Test
void expectAccepted() {
given()
.when()
.get("/whitefox-api/v1/metastores/{name}", createMetastore.getName())
.then()
.statusCode(404);
}
}
}

0 comments on commit ba9394e

Please sign in to comment.