Ltpa2AuthConverter.java
/*
* Copyright 2019 Sephiroth.
*
* 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 de.sephirothj.spring.security.ltpa2.reactive;
import de.sephirothj.spring.security.ltpa2.Ltpa2Token;
import de.sephirothj.spring.security.ltpa2.Ltpa2Utils;
import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import javax.crypto.SecretKey;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SynchronousSink;
/**
* Strategy for converting a {@link ServerWebExchange} to an {@link Authentication} based on LTPA2 tokens. The final authentication will be done by a {@link Ltpa2AuthManager}. The token is expected to be given in the header {@link #headerName} with an {@link #headerValueIdentifier optional prefix}. If the header is empty or not found the token will be searched in the cookie named {@link #cookieName}.
*
* @author Sephiroth
*/
@Slf4j
public class Ltpa2AuthConverter implements ServerAuthenticationConverter, InitializingBean
{
/**
* <p>
* the name of the cookie expected to contain the LTPA2 token</p>
* <p>
* default: {@code "LtpaToken2"}</p>
*/
@Setter
@NonNull
private String cookieName = "LtpaToken2";
/**
* <p>
* the name of header expected to contain the LTPA2 token</p>
* <p>
* default: {@value HttpHeaders#AUTHORIZATION}</p>
*/
@Setter
@NonNull
private String headerName = HttpHeaders.AUTHORIZATION;
/**
* <p>
* the prefix in {@link #headername the header} preceding the LTPA2 token</p>
* <p>
* default: {@code cookieName + " "}</p>
*
* @see #cookieName
*/
@Setter
@NonNull
private String headerValueIdentifier = cookieName + " ";
/**
* the public key from the identity provider that sends the LTPA2-tokens. required for signature validation.
*/
@Setter
@NonNull
private PublicKey signerKey;
/**
* the shared secret key that is used to encrypt LTPA2 tokens
*/
@Setter
@NonNull
private SecretKey sharedKey;
/**
* allow expired tokens
* <p>
* <b>Do not use in prodcution mode, only for testing!</b></p>
*/
@Setter
private boolean allowExpiredToken = false;
@Override
public void afterPropertiesSet()
{
Assert.hasText(cookieName, "A cookieName is required");
Assert.hasText(headerName, "A headerName is required");
Assert.notNull(headerValueIdentifier, "The headerValueIdentifier must not be null");
Assert.notNull(signerKey, "A signerKey is required");
Assert.notNull(sharedKey, "A sharedKey is required");
if (allowExpiredToken)
{
log.warn("Expired LTPA2 tokens are allowed, this should only be used for testing!");
}
}
private void checkForExpiredToken(final String ltpaToken, final SynchronousSink<String> sink)
{
try
{
if (!Ltpa2Utils.isTokenExpired(ltpaToken) || allowExpiredToken)
{
sink.next(ltpaToken);
}
else
{
throw new InsufficientAuthenticationException("token expired");
}
}
catch (AuthenticationException e)
{
// do not produce mono error, just log and produce empty mono as by contract of ServerAuthenticationConverter
log.warn(e.getLocalizedMessage(), e);
}
}
private void checkTokenSignature(final String ltpaToken, final SynchronousSink<String> sink)
{
try
{
if (Ltpa2Utils.isSignatureValid(ltpaToken, signerKey))
{
sink.next(ltpaToken);
}
else
{
throw new InsufficientAuthenticationException("token signature invalid");
}
}
catch (AuthenticationException e)
{
// do not produce mono error, just log and produce empty mono as by contract of ServerAuthenticationConverter
log.warn(e.getLocalizedMessage(), e);
}
}
/**
* Extracts an LTAP2 token from the defined {@link #headerName header} or {@link #cookieName cookie} (as fallback), validates it and if it is, creates an {@link Authentication} instance with it
*
* @param exchange The {@link ServerWebExchange}
* @return A {@link Mono} representing an {@link Authentication} with a valid {@link Ltpa2Token} as credentials or an empty Mono if the token was not found or is invalid
*/
@Override
public Mono<Authentication> convert(ServerWebExchange exchange)
{
final ServerHttpRequest request = exchange.getRequest();
return Mono.justOrEmpty(request.getHeaders().getFirst(headerName))
.filter(header -> !header.isEmpty() && header.startsWith(headerValueIdentifier))
.map(header -> header.substring(header.indexOf(headerValueIdentifier) + headerValueIdentifier.length()))
// try cookie as fallback
.switchIfEmpty(Mono.defer(() -> Mono.justOrEmpty(request.getCookies().getFirst(cookieName))
.map(HttpCookie::getValue)
.map(value -> StringUtils.uriDecode(value, StandardCharsets.ISO_8859_1))))
.doOnNext(encryptedToken -> log.debug("raw LTPA2 token: {}", encryptedToken))
.map(encryptedToken -> Ltpa2Utils.decryptLtpa2Token(encryptedToken, sharedKey))
.onErrorResume(e ->
{
log.warn(e.getLocalizedMessage(), e);
return Mono.empty();
})
.handle(this::checkForExpiredToken)
.handle(this::checkTokenSignature)
.map(Ltpa2Utils::makeInstance)
.map(token -> new PreAuthenticatedAuthenticationToken(token.getUser(), token));
}
}