|
10 | 10 | import org.elasticsearch.rest.RestRequest;
|
11 | 11 | import org.elasticsearch.rest.RestStatus;
|
12 | 12 | import org.elasticsearch.transport.TransportMessage;
|
| 13 | +import org.elasticsearch.xpack.core.XPackField; |
| 14 | + |
| 15 | +import java.util.Collections; |
| 16 | +import java.util.List; |
| 17 | +import java.util.Map; |
13 | 18 |
|
14 | 19 | import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError;
|
15 | 20 |
|
16 | 21 | /**
|
17 |
| - * The default implementation of a {@link AuthenticationFailureHandler}. This handler will return an exception with a |
18 |
| - * RestStatus of 401 and the WWW-Authenticate header with a Basic challenge. |
| 22 | + * The default implementation of a {@link AuthenticationFailureHandler}. This |
| 23 | + * handler will return an exception with a RestStatus of 401 and default failure |
| 24 | + * response headers like 'WWW-Authenticate' |
19 | 25 | */
|
20 | 26 | public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
| 27 | + private final Map<String, List<String>> defaultFailureResponseHeaders; |
| 28 | + |
| 29 | + /** |
| 30 | + * Constructs default authentication failure handler |
| 31 | + * |
| 32 | + * @deprecated replaced by {@link #DefaultAuthenticationFailureHandler(Map)} |
| 33 | + */ |
| 34 | + @Deprecated |
| 35 | + public DefaultAuthenticationFailureHandler() { |
| 36 | + this(null); |
| 37 | + } |
| 38 | + |
| 39 | + /** |
| 40 | + * Constructs default authentication failure handler with provided default |
| 41 | + * response headers. |
| 42 | + * |
| 43 | + * @param failureResponseHeaders Map of header key and list of header values to |
| 44 | + * be sent as failure response. |
| 45 | + * @see Realm#getAuthenticationFailureHeaders() |
| 46 | + */ |
| 47 | + public DefaultAuthenticationFailureHandler(Map<String, List<String>> failureResponseHeaders) { |
| 48 | + if (failureResponseHeaders == null || failureResponseHeaders.isEmpty()) { |
| 49 | + failureResponseHeaders = Collections.singletonMap("WWW-Authenticate", |
| 50 | + Collections.singletonList("Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\"")); |
| 51 | + } |
| 52 | + this.defaultFailureResponseHeaders = Collections.unmodifiableMap(failureResponseHeaders); |
| 53 | + } |
21 | 54 |
|
22 | 55 | @Override
|
23 |
| - public ElasticsearchSecurityException failedAuthentication(RestRequest request, AuthenticationToken token, |
24 |
| - ThreadContext context) { |
25 |
| - return authenticationError("unable to authenticate user [{}] for REST request [{}]", token.principal(), request.uri()); |
| 56 | + public ElasticsearchSecurityException failedAuthentication(RestRequest request, AuthenticationToken token, ThreadContext context) { |
| 57 | + return createAuthenticationError("unable to authenticate user [{}] for REST request [{}]", null, token.principal(), request.uri()); |
26 | 58 | }
|
27 | 59 |
|
28 | 60 | @Override
|
29 | 61 | public ElasticsearchSecurityException failedAuthentication(TransportMessage message, AuthenticationToken token, String action,
|
30 |
| - ThreadContext context) { |
31 |
| - return authenticationError("unable to authenticate user [{}] for action [{}]", token.principal(), action); |
| 62 | + ThreadContext context) { |
| 63 | + return createAuthenticationError("unable to authenticate user [{}] for action [{}]", null, token.principal(), action); |
32 | 64 | }
|
33 | 65 |
|
34 | 66 | @Override
|
35 | 67 | public ElasticsearchSecurityException exceptionProcessingRequest(RestRequest request, Exception e, ThreadContext context) {
|
36 |
| - if (e instanceof ElasticsearchSecurityException) { |
37 |
| - assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED; |
38 |
| - assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1; |
39 |
| - return (ElasticsearchSecurityException) e; |
40 |
| - } |
41 |
| - return authenticationError("error attempting to authenticate request", e); |
| 68 | + return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); |
42 | 69 | }
|
43 | 70 |
|
44 | 71 | @Override
|
45 | 72 | public ElasticsearchSecurityException exceptionProcessingRequest(TransportMessage message, String action, Exception e,
|
46 |
| - ThreadContext context) { |
47 |
| - if (e instanceof ElasticsearchSecurityException) { |
48 |
| - assert ((ElasticsearchSecurityException) e).status() == RestStatus.UNAUTHORIZED; |
49 |
| - assert ((ElasticsearchSecurityException) e).getHeader("WWW-Authenticate").size() == 1; |
50 |
| - return (ElasticsearchSecurityException) e; |
51 |
| - } |
52 |
| - return authenticationError("error attempting to authenticate request", e); |
| 73 | + ThreadContext context) { |
| 74 | + return createAuthenticationError("error attempting to authenticate request", e, (Object[]) null); |
53 | 75 | }
|
54 | 76 |
|
55 | 77 | @Override
|
56 | 78 | public ElasticsearchSecurityException missingToken(RestRequest request, ThreadContext context) {
|
57 |
| - return authenticationError("missing authentication token for REST request [{}]", request.uri()); |
| 79 | + return createAuthenticationError("missing authentication token for REST request [{}]", null, request.uri()); |
58 | 80 | }
|
59 | 81 |
|
60 | 82 | @Override
|
61 | 83 | public ElasticsearchSecurityException missingToken(TransportMessage message, String action, ThreadContext context) {
|
62 |
| - return authenticationError("missing authentication token for action [{}]", action); |
| 84 | + return createAuthenticationError("missing authentication token for action [{}]", null, action); |
63 | 85 | }
|
64 | 86 |
|
65 | 87 | @Override
|
66 | 88 | public ElasticsearchSecurityException authenticationRequired(String action, ThreadContext context) {
|
67 |
| - return authenticationError("action [{}] requires authentication", action); |
| 89 | + return createAuthenticationError("action [{}] requires authentication", null, action); |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Creates an instance of {@link ElasticsearchSecurityException} with |
| 94 | + * {@link RestStatus#UNAUTHORIZED} status. |
| 95 | + * <p> |
| 96 | + * Also adds default failure response headers as configured for this |
| 97 | + * {@link DefaultAuthenticationFailureHandler} |
| 98 | + * <p> |
| 99 | + * It may replace existing response headers if the cause is an instance of |
| 100 | + * {@link ElasticsearchSecurityException} |
| 101 | + * |
| 102 | + * @param message error message |
| 103 | + * @param t cause, if it is an instance of |
| 104 | + * {@link ElasticsearchSecurityException} asserts status is |
| 105 | + * RestStatus.UNAUTHORIZED and adds headers to it, else it will |
| 106 | + * create a new instance of {@link ElasticsearchSecurityException} |
| 107 | + * @param args error message args |
| 108 | + * @return instance of {@link ElasticsearchSecurityException} |
| 109 | + */ |
| 110 | + private ElasticsearchSecurityException createAuthenticationError(final String message, final Throwable t, final Object... args) { |
| 111 | + final ElasticsearchSecurityException ese; |
| 112 | + final boolean containsNegotiateWithToken; |
| 113 | + if (t instanceof ElasticsearchSecurityException) { |
| 114 | + assert ((ElasticsearchSecurityException) t).status() == RestStatus.UNAUTHORIZED; |
| 115 | + ese = (ElasticsearchSecurityException) t; |
| 116 | + if (ese.getHeader("WWW-Authenticate") != null && ese.getHeader("WWW-Authenticate").isEmpty() == false) { |
| 117 | + /** |
| 118 | + * If 'WWW-Authenticate' header is present with 'Negotiate ' then do not |
| 119 | + * replace. In case of kerberos spnego mechanism, we use |
| 120 | + * 'WWW-Authenticate' header value to communicate outToken to peer. |
| 121 | + */ |
| 122 | + containsNegotiateWithToken = |
| 123 | + ese.getHeader("WWW-Authenticate").stream() |
| 124 | + .anyMatch(s -> s != null && s.regionMatches(true, 0, "Negotiate ", 0, "Negotiate ".length())); |
| 125 | + } else { |
| 126 | + containsNegotiateWithToken = false; |
| 127 | + } |
| 128 | + } else { |
| 129 | + ese = authenticationError(message, t, args); |
| 130 | + containsNegotiateWithToken = false; |
| 131 | + } |
| 132 | + defaultFailureResponseHeaders.entrySet().stream().forEach((e) -> { |
| 133 | + if (containsNegotiateWithToken && e.getKey().equalsIgnoreCase("WWW-Authenticate")) { |
| 134 | + return; |
| 135 | + } |
| 136 | + // If it is already present then it will replace the existing header. |
| 137 | + ese.addHeader(e.getKey(), e.getValue()); |
| 138 | + }); |
| 139 | + return ese; |
68 | 140 | }
|
69 | 141 | }
|
0 commit comments