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

Add optional support to security providers #3039

Merged
merged 44 commits into from
Jun 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c1c3a5f
helidon/2486 - Fix broken links
arjav-desai Nov 9, 2020
ed3b67c
Add Dev-guidelines as reference
arjav-desai Nov 9, 2020
6a6887e
Merge remote-tracking branch 'upstream/master'
arjav-desai Nov 9, 2020
6dde736
Merge remote-tracking branch 'upstream/master'
arjav-desai Nov 16, 2020
4810d62
Merge remote-tracking branch 'upstream/master'
arjav-desai Nov 16, 2020
09afb7b
Merge remote-tracking branch 'upstream/master'
arjav-desai Nov 30, 2020
a326fa7
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 3, 2020
e7c93ba
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 4, 2020
7b40302
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 7, 2020
3b2a08e
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 8, 2020
04c6846
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 8, 2020
26cebc2
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 11, 2020
f9ee0fc
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 14, 2020
59b757e
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 17, 2020
a9f3630
Merge remote-tracking branch 'upstream/master'
arjav-desai Dec 21, 2020
b24923f
Merge remote-tracking branch 'upstream/master'
arjav-desai Jan 5, 2021
c03ad57
Merge remote-tracking branch 'upstream/master'
arjav-desai Jan 13, 2021
e3f065d
Merge remote-tracking branch 'upstream/master'
arjav-desai Mar 8, 2021
b11511c
Merge remote-tracking branch 'upstream/master'
arjav-desai Mar 15, 2021
f3d2298
Merge remote-tracking branch 'upstream/master'
arjav-desai Mar 16, 2021
12e0fe3
Merge remote-tracking branch 'upstream/master'
arjav-desai Mar 26, 2021
ff9c3f8
Merge remote-tracking branch 'upstream/master'
arjav-desai Mar 29, 2021
651733d
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 5, 2021
d283eae
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 9, 2021
465ea75
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 12, 2021
36f3383
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 21, 2021
8d0eaf9
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 23, 2021
b5d540e
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 26, 2021
1985483
Merge remote-tracking branch 'upstream/master'
arjav-desai Apr 28, 2021
705c0f7
Merge remote-tracking branch 'upstream/master'
arjav-desai May 7, 2021
86a6270
Merge remote-tracking branch 'upstream/master'
arjav-desai May 10, 2021
d37ba9d
Merge remote-tracking branch 'upstream/master'
arjav-desai May 13, 2021
a8021cb
Merge remote-tracking branch 'upstream/master'
arjav-desai May 18, 2021
d9e9acd
Merge remote-tracking branch 'upstream/master'
arjav-desai May 20, 2021
b137f91
Add optional support
arjav-desai May 21, 2021
dcbacbb
Merge remote-tracking branch 'upstream/master'
arjav-desai May 21, 2021
2d5a51b
Merge branch 'master' into helidon-2772
arjav-desai May 21, 2021
e729981
Fix copyright and checkstyle
arjav-desai May 21, 2021
143a71a
Merge remote-tracking branch 'upstream/master'
arjav-desai May 24, 2021
9635376
Merge remote-tracking branch 'upstream/master'
arjav-desai Jun 1, 2021
a995f57
Merge remote-tracking branch 'upstream/master'
arjav-desai Jun 7, 2021
b7baf9b
Merge branch 'master' into helidon-2772
arjav-desai Jun 7, 2021
06e8b7b
Address review comments
arjav-desai Jun 7, 2021
72c0a0b
Add braces for if-else
arjav-desai Jun 7, 2021
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
6 changes: 6 additions & 0 deletions security/providers/http-auth/docs/full.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ security:
# Http basic and digest authentication
# https://tools.ietf.org/html/rfc2617
- http-basic-auth:
# If set to true, provider will abstain rather than fail if authentication fails
# defaults to false
optional: true
realm: "helidon"
# Can be USER or SERVICE
# defaults to USER
Expand All @@ -32,6 +35,9 @@ security:
password: "${AES=3XQ8A1RszE9JbXl+lUnnsX0gakuqjnTyp8YJWIAU1D3SiM2TaSnxd6U0/LjrdJYv}"
roles: ["user"]
- http-digest-auth:
# If set to true, provider will abstain rather than fail if authentication fails
# defaults to false
optional: true
realm: "helidon"
# Can be USER or SERVICE
# defaults to USER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import io.helidon.security.Role;
import io.helidon.security.SecurityContext;
import io.helidon.security.SecurityEnvironment;
import io.helidon.security.SecurityResponse;
import io.helidon.security.Subject;
import io.helidon.security.SubjectType;
import io.helidon.security.providers.common.OutboundConfig;
Expand Down Expand Up @@ -71,13 +72,15 @@ public class HttpBasicAuthProvider extends SynchronousProvider implements Authen
static final Pattern CREDENTIAL_PATTERN = Pattern.compile("(.*?):(.*)");

private final List<SecureUserStore> userStores;
private final boolean optional;
private final String realm;
private final SubjectType subjectType;
private final OutboundConfig outboundConfig;
private final boolean outboundTargetsExist;

HttpBasicAuthProvider(Builder builder) {
this.userStores = new LinkedList<>(builder.userStores);
this.optional = builder.optional;
this.realm = builder.realm;
this.subjectType = builder.subjectType;
this.outboundConfig = builder.outboundBuilder.build();
Expand Down Expand Up @@ -208,14 +211,15 @@ protected AuthenticationResponse syncAuthenticate(ProviderRequest providerReques
List<String> authorizationHeader = headers.get(HEADER_AUTHENTICATION);

if (null == authorizationHeader) {
return fail("No " + HEADER_AUTHENTICATION + " header");
return failOrAbstain("No " + HEADER_AUTHENTICATION + " header");
}

return authorizationHeader.stream()
.filter(header -> header.toLowerCase().startsWith(BASIC_PREFIX))
.findFirst()
.map(this::validateBasicAuth)
.orElseGet(() -> fail("Authorization header does not contain basic authentication: " + authorizationHeader));
.orElseGet(() ->
failOrAbstain("Authorization header does not contain basic authentication: " + authorizationHeader));
}

private AuthenticationResponse validateBasicAuth(String basicAuthHeader) {
Expand All @@ -226,13 +230,13 @@ private AuthenticationResponse validateBasicAuth(String basicAuthHeader) {
usernameAndPassword = new String(Base64.getDecoder().decode(b64), StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
// not a base64 encoded string
return fail("Basic authentication header with invalid content - not base64 encoded");
return failOrAbstain("Basic authentication header with invalid content - not base64 encoded");
}

Matcher matcher = CREDENTIAL_PATTERN.matcher(usernameAndPassword);
if (!matcher.matches()) {
LOGGER.finest(() -> "Basic authentication header with invalid content: " + usernameAndPassword);
return fail("Basic authentication header with invalid content");
return failOrAbstain("Basic authentication header with invalid content");
}

final String username = matcher.group(1);
Expand Down Expand Up @@ -263,16 +267,23 @@ private AuthenticationResponse invalidUser() {
// extracted to method to make sure we return the same message for invalid user and password
// DO NOT change this - it is a security problem if the message differs, as it gives too much information
// to potential attacker
return fail("Invalid username or password");
return failOrAbstain("Invalid username or password");
}

private AuthenticationResponse fail(String message) {
return AuthenticationResponse.builder()
.statusCode(401)
.responseHeader(HEADER_AUTHENTICATION_REQUIRED, buildChallenge())
.status(AuthenticationResponse.SecurityStatus.FAILURE)
.description(message)
.build();
private AuthenticationResponse failOrAbstain(String message) {
if (optional) {
return AuthenticationResponse.builder()
.status(SecurityResponse.SecurityStatus.ABSTAIN)
.description(message)
.build();
} else {
return AuthenticationResponse.builder()
.statusCode(401)
.responseHeader(HEADER_AUTHENTICATION_REQUIRED, buildChallenge())
.status(AuthenticationResponse.SecurityStatus.FAILURE)
.description(message)
.build();
}
}

private String buildChallenge() {
Expand All @@ -299,6 +310,7 @@ public static final class Builder implements io.helidon.common.Builder<HttpBasic
private final List<SecureUserStore> userStores = new LinkedList<>();
private final OutboundConfig.Builder outboundBuilder = OutboundConfig.builder();

private boolean optional = false;
private String realm = "helidon";
private SubjectType subjectType = SubjectType.USER;

Expand All @@ -311,6 +323,7 @@ private Builder() {
* @return updated builder instance
*/
public Builder config(Config config) {
config.get("optional").asBoolean().ifPresent(this::optional);
config.get("realm").asString().ifPresent(this::realm);
config.get("principal-type").asString().as(SubjectType::valueOf).ifPresent(this::subjectType);

Expand Down Expand Up @@ -409,6 +422,19 @@ public Builder realm(String realm) {
return this;
}

/**
* Whether authentication is required.
* By default, request will fail if the authentication cannot be verified.
* If set to false, request will process and this provider will abstain.
*
* @param optional whether authentication is optional (true) or required (false)
* @return updated builder instance
*/
public Builder optional(boolean optional) {
this.optional = optional;
return this;
}

/**
* Add a new outbound target to configure identity propagation or explicit username/password.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2020 Oracle and/or its affiliates.
* Copyright (c) 2018, 2021 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -39,6 +39,7 @@
import io.helidon.security.ProviderRequest;
import io.helidon.security.Role;
import io.helidon.security.SecurityEnvironment;
import io.helidon.security.SecurityResponse;
import io.helidon.security.Subject;
import io.helidon.security.SubjectType;
import io.helidon.security.spi.AuthenticationProvider;
Expand All @@ -59,6 +60,7 @@ public final class HttpDigestAuthProvider extends SynchronousProvider implements

private final List<HttpDigest.Qop> digestQopOptions = new LinkedList<>();
private final SecureUserStore userStore;
private final boolean optional;
private final String realm;
private final SubjectType subjectType;
private final HttpDigest.Algorithm digestAlgorithm;
Expand All @@ -70,6 +72,7 @@ public final class HttpDigestAuthProvider extends SynchronousProvider implements

private HttpDigestAuthProvider(Builder builder) {
this.userStore = builder.userStore;
this.optional = builder.optional;
this.realm = builder.realm;
this.subjectType = builder.subjectType;
this.digestAlgorithm = builder.digestAlgorithm;
Expand Down Expand Up @@ -132,14 +135,15 @@ protected AuthenticationResponse syncAuthenticate(ProviderRequest providerReques
List<String> authorizationHeader = headers.get(HEADER_AUTHENTICATION);

if (null == authorizationHeader) {
return fail("No " + HEADER_AUTHENTICATION + " header");
return failOrAbstain("No " + HEADER_AUTHENTICATION + " header");
}

return authorizationHeader.stream()
.filter(header -> header.toLowerCase().startsWith(DIGEST_PREFIX))
.findFirst()
.map(value -> validateDigestAuth(value, providerRequest.env()))
.orElseGet(() -> fail("Authorization header does not contain digest authentication: " + authorizationHeader));
.orElseGet(() ->
failOrAbstain("Authorization header does not contain digest authentication: " + authorizationHeader));

}

Expand All @@ -150,7 +154,7 @@ private AuthenticationResponse validateDigestAuth(String headerValue, SecurityEn
env.method().toLowerCase());
} catch (HttpAuthException e) {
LOGGER.log(Level.FINEST, "Failed to process digest token", e);
return fail(e.getMessage());
return failOrAbstain(e.getMessage());
}
// decrypt
byte[] bytes;
Expand All @@ -159,10 +163,10 @@ private AuthenticationResponse validateDigestAuth(String headerValue, SecurityEn
} catch (IllegalArgumentException e) {
LOGGER.log(Level.FINEST, "Failed to base64 decode nonce", e);
// not base 64
return fail("Nonce must be base64 encoded");
return failOrAbstain("Nonce must be base64 encoded");
}
if (bytes.length < 17) {
return fail("Invalid nonce length");
return failOrAbstain("Invalid nonce length");
}
byte[] salt = new byte[SALT_LENGTH];
byte[] aesNonce = new byte[AES_NONCE_LENGTH];
Expand All @@ -178,16 +182,16 @@ private AuthenticationResponse validateDigestAuth(String headerValue, SecurityEn
long nonceTimestamp = HttpAuthUtil.toLong(timestampBytes, 0, timestampBytes.length);
//validate nonce
if ((System.currentTimeMillis() - nonceTimestamp) > digestNonceTimeoutMillis) {
return fail("Nonce timeout");
return failOrAbstain("Nonce timeout");
}
} catch (Exception e) {
LOGGER.log(Level.FINEST, "Failed to validate nonce", e);
return fail("Invalid nonce value");
return failOrAbstain("Invalid nonce value");
}

// validate realm
if (!realm.equals(token.getRealm())) {
return fail("Invalid realm");
return failOrAbstain("Invalid realm");
}

return userStore.user(token.getUsername())
Expand All @@ -200,19 +204,26 @@ private AuthenticationResponse validateDigestAuth(String headerValue, SecurityEn
return AuthenticationResponse.successService(buildSubject(user));
}
} else {
return fail("Invalid username or password");
return failOrAbstain("Invalid username or password");
}
})
.orElse(fail("Invalid username or password"));
.orElse(failOrAbstain("Invalid username or password"));
}

private AuthenticationResponse fail(String message) {
return AuthenticationResponse.builder()
.statusCode(UNAUTHORIZED_STATUS_CODE)
.responseHeader(HEADER_AUTHENTICATION_REQUIRED, buildChallenge())
.status(AuthenticationResponse.SecurityStatus.FAILURE)
.description(message)
.build();
private AuthenticationResponse failOrAbstain(String message) {
if (optional) {
return AuthenticationResponse.builder()
.status(SecurityResponse.SecurityStatus.ABSTAIN)
.description(message)
.build();
} else {
return AuthenticationResponse.builder()
.statusCode(UNAUTHORIZED_STATUS_CODE)
.responseHeader(HEADER_AUTHENTICATION_REQUIRED, buildChallenge())
.status(AuthenticationResponse.SecurityStatus.FAILURE)
.description(message)
.build();
}
}

private String buildChallenge() {
Expand Down Expand Up @@ -266,6 +277,7 @@ public static final class Builder implements io.helidon.common.Builder<HttpDiges
public static final long DEFAULT_DIGEST_NONCE_TIMEOUT = 24 * 60 * 60 * 1000;
private final List<HttpDigest.Qop> digestQopOptions = new LinkedList<>();
private SecureUserStore userStore = EMPTY_STORE;
private boolean optional = false;
private String realm = "Helidon";
private SubjectType subjectType = SubjectType.USER;
private HttpDigest.Algorithm digestAlgorithm = HttpDigest.Algorithm.MD5;
Expand All @@ -282,6 +294,7 @@ private Builder() {
* @return updated builder instance
*/
public Builder config(Config config) {
config.get("optional").asBoolean().ifPresent(this::optional);
config.get("realm").asString().ifPresent(this::realm);
config.get("users").as(ConfigUserStore::create).ifPresent(this::userStore);
config.get("algorithm").asString().as(HttpDigest.Algorithm::valueOf).ifPresent(this::digestAlgorithm);
Expand Down Expand Up @@ -354,6 +367,19 @@ public Builder userStore(SecureUserStore store) {
return this;
}

/**
* Whether authentication is required.
* By default, request will fail if the authentication cannot be verified.
* If set to false, request will process and this provider will abstain.
*
* @param optional whether authentication is optional (true) or required (false)
* @return updated builder instance
*/
public Builder optional(boolean optional) {
this.optional = optional;
return this;
}

/**
* Set the realm to use when challenging users.
*
Expand Down
Loading