Add support for authentication events.

LifecycleAwareSessionManager and ReactiveLifecycleAwareSessionManager now emit AuthenticationEvents for token renewal.

Listeners can be registered through the common base class AuthenticationEventPublisher.

Closes gh-431.
This commit is contained in:
Mark Paluch
2019-06-29 23:06:15 +02:00
parent 22ca10c441
commit 8e6ea04530
21 changed files with 926 additions and 38 deletions

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.util.Assert;
import org.springframework.vault.authentication.event.AuthenticationErrorEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationEvent;
import org.springframework.vault.authentication.event.AuthenticationListener;
/**
* Publisher for {@link AuthenticationEvent}s.
* <p>
* This publisher dispatches events to {@link AuthenticationListener} and
* {@link AuthenticationErrorListener}.
*
* @author Mark Paluch
* @since 2.2
* @see AuthenticationEvent
* @see AuthenticationErrorEvent
* @see AuthenticationListener
* @see AuthenticationErrorListener
*/
public abstract class AuthenticationEventPublisher {
private final Set<AuthenticationListener> listeners = new CopyOnWriteArraySet<>();
private final Set<AuthenticationErrorListener> errorListeners = new CopyOnWriteArraySet<>();
/**
* Add a {@link AuthenticationListener}. The listener starts receiving events as soon
* as possible.
*
* @param listener lease listener, must not be {@literal null}.
*/
public void addAuthenticationListener(AuthenticationListener listener) {
Assert.notNull(listener, "AuthenticationEventListener must not be null");
this.listeners.add(listener);
}
/**
* Remove a {@link AuthenticationListener}.
*
* @param listener must not be {@literal null}.
*/
public void removeAuthenticationListener(AuthenticationListener listener) {
this.listeners.remove(listener);
}
/**
* Add a {@link AuthenticationErrorListener}. The listener starts receiving events as
* soon as possible.
*
* @param listener lease listener, must not be {@literal null}.
*/
public void addErrorListener(AuthenticationErrorListener listener) {
Assert.notNull(listener, "AuthenticationEventErrorListener must not be null");
this.errorListeners.add(listener);
}
/**
* Remove a {@link AuthenticationErrorListener}.
*
* @param listener must not be {@literal null}.
*/
public void removeErrorListener(AuthenticationErrorListener listener) {
this.errorListeners.remove(listener);
}
/**
* Dispatch the event to all {@link AuthenticationListener}s.
*
* @param authenticationEvent the event to dispatch.
*/
void dispatch(AuthenticationEvent authenticationEvent) {
for (AuthenticationListener listener : listeners) {
listener.onAuthenticationEvent(authenticationEvent);
}
}
/**
* Dispatch the event to all {@link AuthenticationErrorListener}s.
*
* @param authenticationEvent the event to dispatch.
*/
void dispatch(AuthenticationErrorEvent authenticationEvent) {
for (AuthenticationErrorListener listener : errorListeners) {
listener.onAuthenticationError(authenticationEvent);
}
}
}

View File

@@ -27,6 +27,19 @@ import org.springframework.http.HttpEntity;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.event.AfterLoginEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationListener;
import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.BeforeLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.LoginFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenExpiredEvent;
import org.springframework.vault.authentication.event.LoginTokenRenewalFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenRevocationFailedEvent;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
@@ -54,6 +67,10 @@ import org.springframework.web.client.RestOperations;
* By default, {@link VaultToken} are looked up in Vault to determine renewability and the
* remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}.
* <p>
* The session manager dispatches authentication events to {@link AuthenticationListener}
* and {@link AuthenticationErrorListener}. Event notifications are dispatched either on
* the calling {@link Thread} or worker threads used for background renewal.
* <p>
* This class is thread-safe.
*
* @author Mark Paluch
@@ -61,6 +78,7 @@ import org.springframework.web.client.RestOperations;
* @see LoginToken
* @see SessionManager
* @see TaskScheduler
* @see AuthenticationEventPublisher
*/
public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSupport
implements SessionManager, DisposableBean {
@@ -161,14 +179,14 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
protected void revoke(VaultToken token) {
try {
dispatch(new BeforeLoginTokenRevocationEvent(token));
restOperations.postForObject("auth/token/revoke-self", new HttpEntity<>(
VaultHttpHeaders.from(token)), Map.class);
}
catch (HttpStatusCodeException e) {
logger.warn(format("Cannot revoke VaultToken", e));
dispatch(new AfterLoginTokenRevocationEvent(token));
}
catch (RuntimeException e) {
logger.warn("Cannot revoke VaultToken: %s", e);
dispatch(new LoginTokenRevocationFailedEvent(token, e));
}
}
@@ -191,8 +209,9 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
return false;
}
TokenWrapper tokenWrapper = token.get();
try {
return doRenew(token.get());
return doRenew(tokenWrapper);
}
catch (HttpStatusCodeException e) {
@@ -201,7 +220,9 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
String message = "Cannot renew token, resetting token and performing re-login";
if (e.getStatusCode().is4xxClientError()) {
logger.warn(format(message, e));
dispatch(new LoginTokenRenewalFailedEvent(tokenWrapper.getToken(), e));
return false;
}
@@ -222,6 +243,7 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
private boolean doRenew(TokenWrapper wrapper) {
dispatch(new BeforeLoginTokenRenewedEvent(wrapper.getToken()));
VaultResponse vaultResponse = restOperations.postForObject(
"auth/token/renew-self",
new HttpEntity<>(VaultHttpHeaders.from(wrapper.token)),
@@ -229,10 +251,11 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
LoginToken renewed = LoginTokenUtil.from(vaultResponse.getRequiredAuth());
Duration validTtlThreshold = getRefreshTrigger().getValidTtlThreshold(renewed);
if (renewed.getLeaseDuration().compareTo(validTtlThreshold) <= 0) {
if (isExpired(renewed)) {
if (logger.isDebugEnabled()) {
Duration validTtlThreshold = getRefreshTrigger().getValidTtlThreshold(
renewed);
logger.info(String
.format("Token TTL (%s) exceeded validity TTL threshold (%s). Dropping token.",
renewed.getLeaseDuration(), validTtlThreshold));
@@ -242,10 +265,12 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
}
setToken(Optional.empty());
dispatch(new LoginTokenExpiredEvent(renewed));
return false;
}
setToken(Optional.of(new TokenWrapper(renewed, wrapper.revocable)));
dispatch(new AfterLoginTokenRenewedEvent(renewed));
return true;
}
@@ -269,7 +294,15 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
private void doGetSessionToken() {
VaultToken token = clientAuthentication.login();
VaultToken token;
try {
token = clientAuthentication.login();
}
catch (VaultException e) {
dispatch(new LoginFailedEvent(clientAuthentication, e));
throw e;
}
TokenWrapper wrapper = new TokenWrapper(token, token instanceof LoginToken);
@@ -283,10 +316,12 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
catch (VaultTokenLookupException e) {
logger.warn(String.format(
"Cannot enhance VaultToken to a LoginToken: %s", e.getMessage()));
dispatch(new AuthenticationErrorEvent(token, e));
}
}
setToken(Optional.of(wrapper));
dispatch(new AfterLoginEvent(token));
if (isTokenRenewable()) {
scheduleRenewal();
@@ -318,8 +353,16 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
logger.info("Scheduling Token renewal");
Runnable task = () -> {
Optional<TokenWrapper> tokenWrapper = getToken();
if (!tokenWrapper.isPresent()) {
return;
}
VaultToken token = tokenWrapper.get().getToken();
try {
if (getToken().isPresent() && isTokenRenewable()) {
if (isTokenRenewable()) {
if (renewToken()) {
scheduleRenewal();
}
@@ -327,6 +370,7 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu
}
catch (Exception e) {
logger.error("Cannot renew VaultToken", e);
dispatch(new LoginTokenRenewalFailedEvent(token, e));
}
};

View File

@@ -41,7 +41,8 @@ import org.springframework.vault.support.VaultToken;
* @author Mark Paluch
* @since 2.0
*/
public abstract class LifecycleAwareSessionManagerSupport {
public abstract class LifecycleAwareSessionManagerSupport extends
AuthenticationEventPublisher {
/**
* Refresh 5 seconds before the token expires.

View File

@@ -29,6 +29,18 @@ import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.authentication.event.AfterLoginEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationListener;
import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.BeforeLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.LoginFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenExpiredEvent;
import org.springframework.vault.authentication.event.LoginTokenRenewalFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenRevocationFailedEvent;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
@@ -58,6 +70,9 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
* By default, {@link VaultToken} are looked up in Vault to determine renewability and the
* remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}.
* <p>
* The session manager dispatches authentication events to {@link AuthenticationListener}
* and {@link AuthenticationErrorListener}.
* <p>
* This class is thread-safe and uses lock-free synchronization.
*
* @author Mark Paluch
@@ -65,6 +80,7 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
* @see LoginToken
* @see ReactiveSessionManager
* @see TaskScheduler
* @see AuthenticationEventPublisher
*/
public class ReactiveLifecycleAwareSessionManager extends
LifecycleAwareSessionManagerSupport implements ReactiveSessionManager,
@@ -168,17 +184,27 @@ public class ReactiveLifecycleAwareSessionManager extends
*/
protected Mono<Void> revoke(VaultToken token) {
return webClient.post().uri("auth/token/revoke-self").headers(httpHeaders -> {
httpHeaders.addAll(VaultHttpHeaders.from(token));
}).retrieve().bodyToMono(String.class).then()
return webClient
.post()
.uri("auth/token/revoke-self")
.headers(httpHeaders -> {
httpHeaders.addAll(VaultHttpHeaders.from(token));
})
.retrieve()
.bodyToMono(String.class)
.doOnSubscribe(
ignore -> dispatch(new BeforeLoginTokenRevocationEvent(token)))
.doOnNext(ignore -> dispatch(new AfterLoginTokenRevocationEvent(token)))
.onErrorResume(WebClientResponseException.class, e -> {
logger.warn(format("Could not revoke token", e));
dispatch(new LoginTokenRevocationFailedEvent(token, e));
return Mono.empty();
}).onErrorResume(Exception.class, e -> {
logger.warn("Could not revoke token", e);
dispatch(new LoginTokenRevocationFailedEvent(token, e));
return Mono.empty();
}).then();
@@ -198,8 +224,7 @@ public class ReactiveLifecycleAwareSessionManager extends
logger.info("Renewing token");
Mono<TokenWrapper> tokenWrapper = ReactiveLifecycleAwareSessionManager.this.token
.get();
Mono<TokenWrapper> tokenWrapper = this.token.get();
if (tokenWrapper == TERMINATED) {
return tokenWrapper.map(TokenWrapper::getToken);
@@ -209,8 +234,12 @@ public class ReactiveLifecycleAwareSessionManager extends
return getVaultToken();
}
return tokenWrapper
.flatMap(this::doRenew)
return tokenWrapper.flatMap(this::doRenewToken).map(TokenWrapper::getToken);
}
private Mono<TokenWrapper> doRenewToken(TokenWrapper wrapper) {
return doRenew(wrapper)
.onErrorResume(
WebClientResponseException.class,
e -> {
@@ -222,6 +251,8 @@ public class ReactiveLifecycleAwareSessionManager extends
if (e.getStatusCode().is4xxClientError()) {
logger.warn(format(message, e));
dispatch(new LoginTokenRenewalFailedEvent(wrapper
.getToken(), e));
return EMPTY;
}
@@ -231,7 +262,7 @@ public class ReactiveLifecycleAwareSessionManager extends
"Cannot renew token", e), e));
})
.onErrorMap(
it -> !VaultTokenRenewalException.class.isInstance(it),
it -> !(it instanceof VaultTokenRenewalException),
e -> {
dropCurrentToken();
@@ -240,7 +271,7 @@ public class ReactiveLifecycleAwareSessionManager extends
e.toString()));
return new VaultTokenRenewalException("Cannot renew token", e);
}).map(TokenWrapper::getToken);
});
}
private Mono<TokenWrapper> doRenew(TokenWrapper tokenWrapper) {
@@ -254,13 +285,17 @@ public class ReactiveLifecycleAwareSessionManager extends
.bodyToMono(VaultResponse.class);
return exchange
.flatMap(response -> {
.doOnSubscribe(
ignore -> dispatch(new BeforeLoginTokenRenewedEvent(tokenWrapper
.getToken())))
.handle((response, sink) -> {
LoginToken renewed = LoginTokenUtil.from(response.getRequiredAuth());
if (!isExpired(renewed)) {
return Mono
.just(new TokenWrapper(renewed, tokenWrapper.revocable));
sink.next(new TokenWrapper(renewed, tokenWrapper.revocable));
dispatch(new AfterLoginTokenRenewedEvent(renewed));
return;
}
if (logger.isDebugEnabled()) {
@@ -276,8 +311,7 @@ public class ReactiveLifecycleAwareSessionManager extends
}
dropCurrentToken();
return EMPTY;
dispatch(new LoginTokenExpiredEvent(renewed));
});
}
@@ -299,11 +333,16 @@ public class ReactiveLifecycleAwareSessionManager extends
Mono<TokenWrapper> obtainToken = clientAuthentication.getVaultToken()
.flatMap(this::doSelfLookup) //
.doOnNext(it -> {
.onErrorMap(it -> {
dispatch(new LoginFailedEvent(clientAuthentication, it));
return it;
}).doOnNext(it -> {
if (isTokenRenewable(it.getToken())) {
scheduleRenewal(it.getToken());
}
dispatch(new AfterLoginEvent(it.getToken()));
});
this.token.compareAndSet(tokenWrapper, obtainToken.cache());
@@ -327,7 +366,7 @@ public class ReactiveLifecycleAwareSessionManager extends
logger.warn(String.format(
"Cannot enhance VaultToken to a LoginToken: %s",
e.getMessage()));
dispatch(new AuthenticationErrorEvent(token, e));
return Mono.just(token);
}).map(it -> new TokenWrapper(it, false));
}
@@ -361,17 +400,21 @@ public class ReactiveLifecycleAwareSessionManager extends
Mono<TokenWrapper> tokenWrapper = ReactiveLifecycleAwareSessionManager.this.token
.get();
if (tokenWrapper == EMPTY || tokenWrapper == TERMINATED) {
if (tokenWrapper == Mono.<TokenWrapper> empty()
|| tokenWrapper == TERMINATED) {
return;
}
if (isTokenRenewable(token)) {
renewToken().subscribe(this::scheduleRenewal,
e -> logger.error("Cannot renew VaultToken", e));
renewToken().subscribe(this::scheduleRenewal, e -> {
logger.error("Cannot renew VaultToken", e);
dispatch(new LoginTokenRenewalFailedEvent(token, e));
});
}
}
catch (Exception e) {
logger.error("Cannot renew VaultToken", e);
dispatch(new LoginTokenRenewalFailedEvent(token, e));
}
};

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Event published after logging into Vault.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class AfterLoginEvent extends AuthenticationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link AfterLoginEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
public AfterLoginEvent(VaultToken source) {
super(source);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Event published after renewing a {@link VaultToken login token}.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class AfterLoginTokenRenewedEvent extends AuthenticationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link AfterLoginTokenRenewedEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
public AfterLoginTokenRenewedEvent(VaultToken source) {
super(source);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Event published after revoking a {@link VaultToken login token}.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class AfterLoginTokenRevocationEvent extends AuthenticationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link AfterLoginTokenRevocationEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
public AfterLoginTokenRevocationEvent(VaultToken source) {
super(source);
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
/**
* Generic event class for authentication error events. These can be generic failures or
* specific ones such as renewal or login errors.
*
* @author Mark Paluch
* @since 2.2
* @see LoginFailedEvent
* @see LoginTokenRenewalFailedEvent
* @see LoginTokenRevocationFailedEvent
* @see AuthenticationErrorListener
*/
public class AuthenticationErrorEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
private final Throwable exception;
/**
* Create a new {@link AuthenticationErrorEvent} given {@code source} and
* {@link Exception}.
*
* @param source must not be {@literal null}.
* @param exception must not be {@literal null}.
*/
public AuthenticationErrorEvent(Object source, Throwable exception) {
super(source);
this.exception = exception;
}
public Throwable getException() {
return exception;
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
/**
* Listener for Vault exceptional {@link AuthenticationEvent}s.
* <p>
* Error events can occur during login, login token renewal and login token revocation.
*
* @author Mark Paluch
* @since 2.2
*/
@FunctionalInterface
public interface AuthenticationErrorListener {
/**
* Callback for a {@link AuthenticationErrorEvent}.
*
* @param authenticationEvent the event object, must not be {@literal null}.
*/
void onAuthenticationError(AuthenticationErrorEvent authenticationEvent);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Abstract base class for authentication events.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public abstract class AuthenticationEvent extends ApplicationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link AuthenticationEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
protected AuthenticationEvent(VaultToken source) {
super(source);
}
@Override
public VaultToken getSource() {
return (VaultToken) super.getSource();
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
/**
* Listener for Vault {@link AuthenticationEvent}s.
*
* @author Mark Paluch
* @since 2.2
* @see AuthenticationEvent
*/
@FunctionalInterface
public interface AuthenticationListener {
/**
* Callback for a {@link AuthenticationEvent}
*
* @param leaseEvent the event object, must not be {@literal null}.
*/
void onAuthenticationEvent(AuthenticationEvent leaseEvent);
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Event published before renewing a {@link VaultToken login token}.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class BeforeLoginTokenRenewedEvent extends AuthenticationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link BeforeLoginTokenRenewedEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
public BeforeLoginTokenRenewedEvent(VaultToken source) {
super(source);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Event published before revoking a {@link VaultToken login token}.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class BeforeLoginTokenRevocationEvent extends AuthenticationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link BeforeLoginTokenRevocationEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
public BeforeLoginTokenRevocationEvent(VaultToken source) {
super(source);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.authentication.VaultTokenSupplier;
import org.springframework.vault.support.VaultToken;
/**
* Event published before renewing a {@link VaultToken login token}.
* <p>
* Provides {@link ClientAuthentication} or {@link VaultTokenSupplier} as
* {@link #getSource() source}.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class LoginFailedEvent extends AuthenticationErrorEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link LoginFailedEvent} given {@link Exception}.
*
* @param source the {@link ClientAuthentication} or {@link VaultTokenSupplier}
* associated with this event, must not be {@literal null}.
* @param exception must not be {@literal null}.
*/
public LoginFailedEvent(Object source, Throwable exception) {
super(source, exception);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Event published when dropping an expired {@link VaultToken login token}.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class LoginTokenExpiredEvent extends AuthenticationEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link LoginTokenExpiredEvent} given {@link VaultToken}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
*/
public LoginTokenExpiredEvent(VaultToken source) {
super(source);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Generic event class for authentication error events.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class LoginTokenRenewalFailedEvent extends AuthenticationErrorEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link LoginTokenRenewalFailedEvent} given {@link VaultToken} and
* {@link Exception}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
* @param exception must not be {@literal null}.
*/
public LoginTokenRenewalFailedEvent(VaultToken source, Throwable exception) {
super(source, exception);
}
public VaultToken getSource() {
return (VaultToken) super.getSource();
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019 the original author or authors.
*
* 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
*
* https://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.
*/
package org.springframework.vault.authentication.event;
import org.springframework.context.ApplicationEvent;
import org.springframework.vault.support.VaultToken;
/**
* Generic event class for authentication error events.
*
* @author Mark Paluch
* @since 2.2
* @see ApplicationEvent
*/
public class LoginTokenRevocationFailedEvent extends AuthenticationErrorEvent {
private static final long serialVersionUID = 1L;
/**
* Create a new {@link LoginTokenRevocationFailedEvent} given {@link VaultToken} and
* {@link Exception}.
*
* @param source the {@link VaultToken} associated with this event, must not be
* {@literal null}.
* @param exception must not be {@literal null}.
*/
public LoginTokenRevocationFailedEvent(VaultToken source, Throwable exception) {
super(source, exception);
}
public VaultToken getSource() {
return (VaultToken) super.getSource();
}
}

View File

@@ -0,0 +1,7 @@
/**
* Support classes for authentication application events.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.vault.authentication.event;

View File

@@ -25,6 +25,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@@ -34,6 +35,17 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.vault.authentication.event.AfterLoginEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationEvent;
import org.springframework.vault.authentication.event.AuthenticationListener;
import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.BeforeLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.LoginFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenExpiredEvent;
import org.springframework.vault.authentication.event.LoginTokenRevocationFailedEvent;
import org.springframework.vault.client.VaultHttpHeaders;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
@@ -49,6 +61,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
@@ -69,12 +82,23 @@ public class LifecycleAwareSessionManagerUnitTests {
@Mock
private RestOperations restOperations;
@Mock
private AuthenticationListener listener;
@Mock
private AuthenticationErrorListener errorListener;
@Captor
private ArgumentCaptor<AuthenticationEvent> captor;
private LifecycleAwareSessionManager sessionManager;
@Before
public void before() {
sessionManager = new LifecycleAwareSessionManager(clientAuthentication,
taskScheduler, restOperations);
sessionManager.addAuthenticationListener(listener);
sessionManager.addErrorListener(errorListener);
}
@Test
@@ -83,6 +107,18 @@ public class LifecycleAwareSessionManagerUnitTests {
when(clientAuthentication.login()).thenReturn(LoginToken.of("login"));
assertThat(sessionManager.getSessionToken()).isEqualTo(LoginToken.of("login"));
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
}
@Test
public void loginShouldFail() {
when(clientAuthentication.login()).thenThrow(new VaultLoginException("foo"));
assertThatThrownBy(() -> sessionManager.getSessionToken()).isInstanceOf(
VaultLoginException.class);
verifyZeroInteractions(listener);
verify(errorListener).onAuthenticationError(any(LoginFailedEvent.class));
}
@Test
@@ -105,6 +141,10 @@ public class LifecycleAwareSessionManagerUnitTests {
verify(restOperations).exchange(eq("auth/token/lookup-self"), eq(HttpMethod.GET),
eq(new HttpEntity<>(VaultHttpHeaders.from(LoginToken.of("login")))),
any(Class.class));
verify(listener).onAuthenticationEvent(captor.capture());
AfterLoginEvent event = (AfterLoginEvent) captor.getValue();
assertThat(event.getSource()).isSameAs(sessionToken);
}
@Test
@@ -123,6 +163,8 @@ public class LifecycleAwareSessionManagerUnitTests {
VaultToken sessionToken = sessionManager.getSessionToken();
assertThat(sessionToken).isExactlyInstanceOf(VaultToken.class);
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
verify(errorListener).onAuthenticationError(any());
}
@Test
@@ -150,11 +192,13 @@ public class LifecycleAwareSessionManagerUnitTests {
sessionManager.renewToken();
sessionManager.destroy();
verify(restOperations)
.postForObject(
eq("auth/token/revoke-self"),
eq(new HttpEntity<Object>(VaultHttpHeaders.from(LoginToken
.of("login")))), any(Class.class));
verify(restOperations).postForObject(eq("auth/token/revoke-self"),
eq(new HttpEntity<>(VaultHttpHeaders.from(LoginToken.of("login")))),
any(Class.class));
verify(listener)
.onAuthenticationEvent(any(BeforeLoginTokenRevocationEvent.class));
verify(listener).onAuthenticationEvent(any(AfterLoginTokenRevocationEvent.class));
}
@Test
@@ -167,6 +211,8 @@ public class LifecycleAwareSessionManagerUnitTests {
sessionManager.destroy();
verifyZeroInteractions(restOperations);
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
verifyNoMoreInteractions(listener);
}
@Test
@@ -182,11 +228,15 @@ public class LifecycleAwareSessionManagerUnitTests {
sessionManager.renewToken();
sessionManager.destroy();
verify(restOperations)
.postForObject(
eq("auth/token/revoke-self"),
eq(new HttpEntity<Object>(VaultHttpHeaders.from(LoginToken
.of("login")))), any(Class.class));
verify(restOperations).postForObject(eq("auth/token/revoke-self"),
eq(new HttpEntity<>(VaultHttpHeaders.from(LoginToken.of("login")))),
any(Class.class));
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
verify(listener)
.onAuthenticationEvent(any(BeforeLoginTokenRevocationEvent.class));
verifyNoMoreInteractions(listener);
verify(errorListener).onAuthenticationError(
any(LoginTokenRevocationFailedEvent.class));
}
@Test
@@ -205,6 +255,10 @@ public class LifecycleAwareSessionManagerUnitTests {
when(clientAuthentication.login()).thenReturn(
LoginToken.renewable("login".toCharArray(), Duration.ofSeconds(5)));
when(restOperations.postForObject(anyString(), any(), eq(VaultResponse.class)))
.thenReturn(
fromToken(LoginToken.of("foo".toCharArray(),
Duration.ofSeconds(10))));
ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
@@ -218,6 +272,8 @@ public class LifecycleAwareSessionManagerUnitTests {
eq(new HttpEntity<Object>(VaultHttpHeaders.from(LoginToken.renewable(
"login", 5)))), any(Class.class));
verify(clientAuthentication, times(1)).login();
verify(listener).onAuthenticationEvent(any(BeforeLoginTokenRenewedEvent.class));
verify(listener).onAuthenticationEvent(any(AfterLoginTokenRenewedEvent.class));
}
@Test
@@ -280,6 +336,8 @@ public class LifecycleAwareSessionManagerUnitTests {
LoginToken.renewable("bar".toCharArray(), Duration.ofSeconds(5)));
verify(clientAuthentication, times(2)).login();
verify(listener, times(2)).onAuthenticationEvent(any(AfterLoginEvent.class));
verify(listener).onAuthenticationEvent(any(LoginTokenExpiredEvent.class));
}
@Test

View File

@@ -24,6 +24,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import reactor.core.publisher.Mono;
@@ -31,6 +32,16 @@ import reactor.test.StepVerifier;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.Trigger;
import org.springframework.vault.authentication.event.AfterLoginEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.AfterLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.AuthenticationErrorListener;
import org.springframework.vault.authentication.event.AuthenticationEvent;
import org.springframework.vault.authentication.event.AuthenticationListener;
import org.springframework.vault.authentication.event.BeforeLoginTokenRenewedEvent;
import org.springframework.vault.authentication.event.BeforeLoginTokenRevocationEvent;
import org.springframework.vault.authentication.event.LoginFailedEvent;
import org.springframework.vault.authentication.event.LoginTokenExpiredEvent;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultToken;
import org.springframework.web.reactive.function.client.WebClient;
@@ -47,6 +58,8 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
@@ -81,6 +94,15 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
@Mock
ResponseSpec responseSpec;
@Mock
private AuthenticationListener listener;
@Mock
private AuthenticationErrorListener errorListener;
@Captor
private ArgumentCaptor<AuthenticationEvent> captor;
private ReactiveLifecycleAwareSessionManager sessionManager;
@Before
@@ -100,6 +122,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
sessionManager = new ReactiveLifecycleAwareSessionManager(tokenSupplier,
taskScheduler, webClient);
sessionManager.addAuthenticationListener(listener);
sessionManager.addErrorListener(errorListener);
}
@Test
@@ -109,6 +133,18 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
sessionManager.getSessionToken().as(StepVerifier::create)
.expectNext(LoginToken.of("login")).verifyComplete();
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
}
@Test
public void loginShouldFail() {
when(tokenSupplier.getVaultToken()).thenReturn(
Mono.error(new VaultLoginException("foo")));
sessionManager.getSessionToken().as(StepVerifier::create).verifyError();
verifyZeroInteractions(listener);
verify(errorListener).onAuthenticationError(any(LoginFailedEvent.class));
}
@Test
@@ -134,6 +170,9 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
}).verifyComplete();
verify(webClient.get()).uri("auth/token/lookup-self");
verify(listener).onAuthenticationEvent(captor.capture());
AfterLoginEvent event = (AfterLoginEvent) captor.getValue();
assertThat(event.getSource()).isInstanceOf(LoginToken.class);
}
@Test
@@ -152,6 +191,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
sessionManager.getSessionToken().as(StepVerifier::create).assertNext(it -> {
assertThat(it).isExactlyInstanceOf(VaultToken.class);
}).verifyComplete();
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
verify(errorListener).onAuthenticationError(any());
}
@Test
@@ -181,6 +222,26 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
@Test
public void shouldRevokeLoginTokenOnDestroy() {
VaultResponse vaultResponse = new VaultResponse();
vaultResponse.setData(Collections.singletonMap("ttl", 100));
mockToken(LoginToken.of("login"));
when(responseSpec.bodyToMono(String.class)).thenReturn(Mono.just("OK"));
sessionManager.getVaultToken().as(StepVerifier::create).expectNextCount(1)
.verifyComplete();
sessionManager.destroy();
verify(webClient.post()).uri("auth/token/revoke-self");
verify(listener)
.onAuthenticationEvent(any(BeforeLoginTokenRevocationEvent.class));
verify(listener).onAuthenticationEvent(any(AfterLoginTokenRevocationEvent.class));
}
@Test
public void shouldNotRevokeRegularTokenOnDestroy() {
mockToken(VaultToken.of("login"));
sessionManager.setTokenSelfLookupEnabled(false);
@@ -190,6 +251,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
verify(webClient, never()).post();
verify(webClient.post(), never()).uri("auth/token/revoke-self");
verify(listener).onAuthenticationEvent(any(AfterLoginEvent.class));
verifyNoMoreInteractions(listener);
}
@Test
@@ -245,6 +308,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
verify(webClient.post()).uri("auth/token/renew-self");
verify(tokenSupplier, times(1)).getVaultToken();
verify(listener).onAuthenticationEvent(any(BeforeLoginTokenRenewedEvent.class));
verify(listener).onAuthenticationEvent(any(AfterLoginTokenRenewedEvent.class));
}
@Test
@@ -313,6 +378,8 @@ public class ReactiveLifecycleAwareSessionManagerUnitTests {
.verifyComplete();
verify(tokenSupplier, times(2)).getVaultToken();
verify(listener, times(2)).onAuthenticationEvent(any(AfterLoginEvent.class));
verify(listener).onAuthenticationEvent(any(LoginTokenExpiredEvent.class));
}
@Test

View File

@@ -6,6 +6,7 @@
* Support for Key-Value v2 (versioned backend) secrets through `@VaultPropertySource`.
* SpEL support in `@Secret`.
* Add support for Jetty as reactive HttpClient.
* `LifecycleAwareSessionManager` and `ReactiveLifecycleAwareSessionManager` emit now ``AuthenticationEvent``s.
[[new-features.2-1-0]]
=== What's new in Spring Vault 2.1